diff --git a/Cargo.lock b/Cargo.lock index 5694b4f6..58e0e5c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5521,6 +5521,7 @@ dependencies = [ "tempfile", "test-case", "thiserror 2.0.20", + "zeroize", ] [[package]] diff --git a/crates/k1util/Cargo.toml b/crates/k1util/Cargo.toml index 5179c0a7..254262ac 100644 --- a/crates/k1util/Cargo.toml +++ b/crates/k1util/Cargo.toml @@ -11,6 +11,7 @@ thiserror.workspace = true k256.workspace = true hex.workspace = true libp2p.workspace = true +zeroize.workspace = true [dev-dependencies] criterion.workspace = true diff --git a/crates/k1util/src/k1util.rs b/crates/k1util/src/k1util.rs index 208e49d7..d75e4012 100644 --- a/crates/k1util/src/k1util.rs +++ b/crates/k1util/src/k1util.rs @@ -9,6 +9,7 @@ use k256::{ ecdsa::{self, RecoveryId, Signature, SigningKey, hazmat::VerifyPrimitive}, }; use libp2p::identity::PublicKey as Libp2pPublicKey; +use zeroize::Zeroizing; /// `SCALAR_LEN` is the length of secp256k1 scalar. pub const SCALAR_LEN: usize = 32; @@ -212,10 +213,15 @@ pub fn recover(hash: &[u8], sig: &[u8]) -> Result { } /// Load loads a secret key from a file. +/// +/// The hex-encoded file contents and the decoded scalar are intermediate +/// buffers holding the raw secret, so both are wrapped in [`Zeroizing`] and +/// wiped once the key has been parsed. pub fn load(file: &Path) -> Result { - let contents = std::fs::read_to_string(file).map_err(K1UtilError::FailedToReadFile)?; + let contents = + Zeroizing::new(std::fs::read_to_string(file).map_err(K1UtilError::FailedToReadFile)?); - let decoded = hex::decode(contents.trim())?; + let decoded = Zeroizing::new(hex::decode(contents.trim())?); let key = SecretKey::from_slice(&decoded).map_err(K1UtilError::FailedToParseSecretKey)?; @@ -228,8 +234,13 @@ pub fn load(file: &Path) -> Result { /// matching Charon's `app/k1util/k1util.go` `Save` which writes via /// `os.WriteFile(file, ..., 0o600)`. This prevents the private key from being /// world-readable. +/// +/// The serialized scalar and its hex encoding are intermediate buffers holding +/// the raw secret, so both are wrapped in [`Zeroizing`] and wiped once the file +/// has been written. pub fn save(key: &SecretKey, file: &Path) -> Result<()> { - let encoded = hex::encode(key.to_bytes()); + let raw = Zeroizing::new(key.to_bytes()); + let encoded = Zeroizing::new(hex::encode(raw.as_slice())); #[cfg(unix)] { @@ -258,7 +269,7 @@ pub fn save(key: &SecretKey, file: &Path) -> Result<()> { #[cfg(not(unix))] { - std::fs::write(file, encoded).map_err(K1UtilError::FailedToWriteFile)?; + std::fs::write(file, encoded.as_bytes()).map_err(K1UtilError::FailedToWriteFile)?; } Ok(())