Skip to content

Commit 6b17b9f

Browse files
committed
fix(core): zeroize the derived X25519 secret (#65)
x25519_secret_from_seed derived the X25519 private scalar from the Ed25519 seed and returned it as a bare [u8; 32]. Because [u8; 32] is Copy with no Drop (and sha2's digest output likewise has no zeroize), the scalar, the SHA-512 digest, and the returned temporary were released without being scrubbed, leaving secret-derived material in freed memory. This is the same Copy-no-Drop class fixed for the raw seed in #41, on the derived secret. Return Zeroizing<[u8; 32]>, build the scalar directly into a zeroizing buffer so no bare secret local persists, and explicitly wipe the SHA-512 digest before it drops. Callers deref the result into crypto_box::SecretKey, which already scrubs its own copy. No behavior change: the derived scalar and all decryption output are byte-identical, covered by the existing ed25519_to_x25519_keypair_agrees and seal_open_round_trip_for_recipients tests.
1 parent 2153b0b commit 6b17b9f

1 file changed

Lines changed: 10 additions & 5 deletions

File tree

‎crates/gitlawb-core/src/encrypt.rs‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use crate::identity::Keypair;
77
use anyhow::{Context, Result};
88
use ed25519_dalek::VerifyingKey;
9+
use zeroize::Zeroizing;
910

1011
/// X25519 public key (Montgomery u) for an Ed25519 verifying key.
1112
fn x25519_public(vk: &VerifyingKey) -> Result<[u8; 32]> {
@@ -18,14 +19,18 @@ fn x25519_public(vk: &VerifyingKey) -> Result<[u8; 32]> {
1819
}
1920

2021
/// X25519 secret scalar for an Ed25519 seed (SHA-512 of seed, lower 32, clamped).
21-
fn x25519_secret_from_seed(seed: &[u8; 32]) -> [u8; 32] {
22+
/// Returns the scalar wrapped in `Zeroizing`, and scrubs the intermediate
23+
/// SHA-512 digest, so no copy of this secret material lingers in freed memory.
24+
fn x25519_secret_from_seed(seed: &[u8; 32]) -> Zeroizing<[u8; 32]> {
2225
use sha2::{Digest, Sha512};
23-
let h = Sha512::digest(seed);
24-
let mut s = [0u8; 32];
26+
use zeroize::Zeroize;
27+
let mut h = Sha512::digest(seed);
28+
let mut s = Zeroizing::new([0u8; 32]);
2529
s.copy_from_slice(&h[..32]);
2630
s[0] &= 248;
2731
s[31] &= 127;
2832
s[31] |= 64;
33+
h.as_mut_slice().zeroize();
2934
s
3035
}
3136

@@ -123,7 +128,7 @@ pub fn open_blob(envelope: &[u8], keypair: &Keypair) -> Result<Vec<u8>> {
123128
.context("decode header")?;
124129
let body = &envelope[p + hlen..];
125130

126-
let my_x = XSecret::from(x25519_secret_from_seed(&keypair.to_seed()));
131+
let my_x = XSecret::from(*x25519_secret_from_seed(&keypair.to_seed()));
127132

128133
// Identities are blinded: no entry says which recipient it belongs to, so
129134
// try each one. The ChaChaBox AEAD tag authenticates, so exactly the
@@ -188,7 +193,7 @@ mod tests {
188193
let seed = kp.to_seed();
189194
let xpub_from_public = x25519_public(&kp.verifying_key()).unwrap();
190195
let xsec = x25519_secret_from_seed(&seed);
191-
let xpub_from_secret = crypto_box::SecretKey::from(xsec).public_key().to_bytes();
196+
let xpub_from_secret = crypto_box::SecretKey::from(*xsec).public_key().to_bytes();
192197
assert_eq!(xpub_from_public, xpub_from_secret);
193198
}
194199

0 commit comments

Comments
 (0)