···11+use aes_gcm::aead::{Aead, AeadCore, KeyInit};
22+use aes_gcm::{Aes256Gcm, Nonce};
33+use base64::engine::general_purpose::STANDARD as BASE64;
44+use base64::Engine;
55+use multibase::Base;
66+use p256::elliptic_curve::sec1::ToEncodedPoint;
77+use p256::SecretKey;
88+use rand_core::OsRng;
99+use zeroize::Zeroizing;
1010+1111+use crate::CryptoError;
1212+1313+/// P-256 multicodec varint prefix for did:key URIs.
1414+/// 0x1200 encoded as LEB128 varint = [0x80, 0x24].
1515+const P256_MULTICODEC_PREFIX: &[u8] = &[0x80, 0x24];
1616+1717+/// A generated P-256 keypair.
1818+///
1919+/// `private_key_bytes` is zeroized on drop. Callers must encrypt it with
2020+/// [`encrypt_private_key`] before storing and drop this struct promptly.
2121+pub struct P256Keypair {
2222+ /// Full `did:key:z...` URI — use as the database primary key.
2323+ pub key_id: String,
2424+ /// Multibase base58btc-encoded compressed public key point (no `did:key:` prefix).
2525+ pub public_key: String,
2626+ /// Raw 32-byte P-256 private key scalar. Zeroized on drop.
2727+ pub private_key_bytes: Zeroizing<[u8; 32]>,
2828+}
2929+3030+/// Generate a fresh P-256 keypair and derive its `did:key` identifier.
3131+pub fn generate_p256_keypair() -> Result<P256Keypair, CryptoError> {
3232+ let secret_key = SecretKey::random(&mut OsRng);
3333+ let public_key = secret_key.public_key();
3434+3535+ // Compressed point: 0x02/0x03 prefix byte + 32-byte x-coordinate = 33 bytes.
3636+ let compressed = public_key.to_encoded_point(true);
3737+ let compressed_bytes = compressed.as_bytes();
3838+3939+ // did:key multikey: P-256 multicodec varint + compressed public key bytes.
4040+ let mut multikey = Vec::with_capacity(P256_MULTICODEC_PREFIX.len() + compressed_bytes.len());
4141+ multikey.extend_from_slice(P256_MULTICODEC_PREFIX);
4242+ multikey.extend_from_slice(compressed_bytes);
4343+4444+ // multibase::encode with Base58Btc prepends the 'z' prefix automatically.
4545+ let multibase_encoded = multibase::encode(Base::Base58Btc, &multikey);
4646+ let key_id = format!("did:key:{multibase_encoded}");
4747+ let public_key_str = multibase::encode(Base::Base58Btc, compressed_bytes);
4848+4949+ // Copy private key bytes into a Zeroizing wrapper.
5050+ let raw_bytes = secret_key.to_bytes();
5151+ let mut private_key_bytes = Zeroizing::new([0u8; 32]);
5252+ private_key_bytes.copy_from_slice(raw_bytes.as_slice());
5353+5454+ Ok(P256Keypair {
5555+ key_id,
5656+ public_key: public_key_str,
5757+ private_key_bytes,
5858+ })
5959+}
6060+6161+/// Encrypt a 32-byte P-256 private key using AES-256-GCM.
6262+///
6363+/// Returns `base64( nonce(12) || ciphertext+tag(48) )` — always 80 base64 chars.
6464+/// A fresh 12-byte nonce is generated from the OS RNG on every call, so two calls
6565+/// with the same input produce different output (AC3.4).
6666+pub fn encrypt_private_key(
6767+ key_bytes: &[u8; 32],
6868+ master_key: &[u8; 32],
6969+) -> Result<String, CryptoError> {
7070+ let cipher = Aes256Gcm::new_from_slice(master_key.as_slice())
7171+ .map_err(|e| CryptoError::Encryption(format!("invalid master key length: {e}")))?;
7272+7373+ let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
7474+7575+ // encrypt() appends the 16-byte authentication tag: output = 32 + 16 = 48 bytes.
7676+ let ciphertext = cipher
7777+ .encrypt(&nonce, key_bytes.as_slice())
7878+ .map_err(|e| CryptoError::Encryption(format!("aes-gcm encryption failed: {e}")))?;
7979+8080+ // Storage format: nonce(12) || ciphertext_with_tag(48) = 60 bytes → 80 base64 chars.
8181+ let mut storage = Vec::with_capacity(12 + ciphertext.len());
8282+ storage.extend_from_slice(nonce.as_slice());
8383+ storage.extend_from_slice(&ciphertext);
8484+8585+ Ok(BASE64.encode(&storage))
8686+}
8787+8888+/// Decrypt a private key encrypted by [`encrypt_private_key`].
8989+///
9090+/// Returns `CryptoError::Decryption` for any failure — malformed base64, wrong
9191+/// length, or authentication tag mismatch. The caller cannot distinguish between
9292+/// these cases intentionally (no oracle).
9393+pub fn decrypt_private_key(
9494+ encrypted: &str,
9595+ master_key: &[u8; 32],
9696+) -> Result<Zeroizing<[u8; 32]>, CryptoError> {
9797+ let storage = BASE64
9898+ .decode(encrypted)
9999+ .map_err(|e| CryptoError::Decryption(format!("invalid base64: {e}")))?;
100100+101101+ if storage.len() != 60 {
102102+ return Err(CryptoError::Decryption(format!(
103103+ "expected 60 bytes (nonce + ciphertext + tag), got {}",
104104+ storage.len()
105105+ )));
106106+ }
107107+108108+ let nonce = Nonce::from_slice(&storage[..12]);
109109+ let ciphertext_with_tag = &storage[12..];
110110+111111+ let cipher = Aes256Gcm::new_from_slice(master_key.as_slice())
112112+ .map_err(|e| CryptoError::Decryption(format!("invalid master key length: {e}")))?;
113113+114114+ // Use decrypt() — NOT decrypt_in_place_detached — to avoid GHSA-423w-p2w9-r7vq.
115115+ // Tag is verified before plaintext is returned.
116116+ let plaintext = cipher
117117+ .decrypt(nonce, ciphertext_with_tag)
118118+ .map_err(|_| CryptoError::Decryption("authentication tag mismatch".to_string()))?;
119119+120120+ let mut out = Zeroizing::new([0u8; 32]);
121121+ out.copy_from_slice(&plaintext);
122122+ Ok(out)
123123+}
124124+125125+#[cfg(test)]
126126+mod tests {
127127+ use super::*;
128128+129129+ #[test]
130130+ fn generate_keypair_produces_valid_did_key() {
131131+ let keypair = generate_p256_keypair().unwrap();
132132+133133+ // key_id must be a valid did:key URI with P-256 multicodec prefix.
134134+ assert!(
135135+ keypair.key_id.starts_with("did:key:z"),
136136+ "key_id must start with did:key:z"
137137+ );
138138+139139+ // Decode the multibase portion and verify the multicodec prefix.
140140+ let multibase_part = keypair.key_id.strip_prefix("did:key:").unwrap();
141141+ let (_, multikey_bytes) = multibase::decode(multibase_part).unwrap();
142142+ assert_eq!(
143143+ &multikey_bytes[..2],
144144+ P256_MULTICODEC_PREFIX,
145145+ "multikey must start with P-256 multicodec varint [0x80, 0x24]"
146146+ );
147147+ // P-256 compressed point: 2 (prefix varint) + 33 (compressed) = 35 bytes.
148148+ assert_eq!(multikey_bytes.len(), 35);
149149+ }
150150+151151+ #[test]
152152+ fn generate_keypair_public_key_is_multibase_without_did_prefix() {
153153+ let keypair = generate_p256_keypair().unwrap();
154154+155155+ // public_key is multibase (starts with 'z') but has no 'did:key:' prefix.
156156+ assert!(keypair.public_key.starts_with('z'));
157157+ assert!(!keypair.public_key.starts_with("did:key:"));
158158+159159+ // Decodes to a compressed P-256 point: 33 bytes.
160160+ let (_, point_bytes) = multibase::decode(&keypair.public_key).unwrap();
161161+ assert_eq!(point_bytes.len(), 33);
162162+ assert!(
163163+ point_bytes[0] == 0x02 || point_bytes[0] == 0x03,
164164+ "compressed point prefix must be 0x02 or 0x03"
165165+ );
166166+ }
167167+168168+ #[test]
169169+ fn generate_keypair_private_key_is_32_bytes() {
170170+ let keypair = generate_p256_keypair().unwrap();
171171+ assert_eq!(keypair.private_key_bytes.len(), 32);
172172+ }
173173+174174+ /// MM-92.AC3.1: Round-trip encrypt → decrypt returns original bytes.
175175+ #[test]
176176+ fn encrypt_decrypt_round_trip() {
177177+ let master_key = [0xab_u8; 32];
178178+ let private_key = [0x42_u8; 32];
179179+180180+ let encrypted = encrypt_private_key(&private_key, &master_key).unwrap();
181181+ let decrypted = decrypt_private_key(&encrypted, &master_key).unwrap();
182182+183183+ assert_eq!(*decrypted, private_key);
184184+ }
185185+186186+ /// MM-92.AC3.2: Wrong master key returns CryptoError::Decryption.
187187+ #[test]
188188+ fn decrypt_with_wrong_master_key_fails() {
189189+ let master_key = [0xab_u8; 32];
190190+ let wrong_key = [0xcd_u8; 32];
191191+ let private_key = [0x42_u8; 32];
192192+193193+ let encrypted = encrypt_private_key(&private_key, &master_key).unwrap();
194194+ let result = decrypt_private_key(&encrypted, &wrong_key);
195195+196196+ assert!(
197197+ matches!(result, Err(CryptoError::Decryption(_))),
198198+ "expected CryptoError::Decryption, got {result:?}"
199199+ );
200200+ }
201201+202202+ /// MM-92.AC3.3: Malformed base64 returns CryptoError::Decryption.
203203+ #[test]
204204+ fn decrypt_invalid_base64_fails() {
205205+ let master_key = [0xab_u8; 32];
206206+207207+ let result = decrypt_private_key("not-valid-base64!!!", &master_key);
208208+ assert!(matches!(result, Err(CryptoError::Decryption(_))));
209209+ }
210210+211211+ /// MM-92.AC3.3 (variant): Base64 that decodes but is wrong length.
212212+ #[test]
213213+ fn decrypt_wrong_length_fails() {
214214+ let master_key = [0xab_u8; 32];
215215+ // Valid base64 of 10 bytes — decodes OK but is not 60 bytes.
216216+ let short = BASE64.encode(&[0u8; 10]);
217217+ let result = decrypt_private_key(&short, &master_key);
218218+ assert!(matches!(result, Err(CryptoError::Decryption(_))));
219219+ }
220220+221221+ /// MM-92.AC3.4: Two encryptions of the same key produce different ciphertexts (random nonce).
222222+ #[test]
223223+ fn encrypt_produces_different_ciphertexts_for_same_input() {
224224+ let master_key = [0xab_u8; 32];
225225+ let private_key = [0x42_u8; 32];
226226+227227+ let first = encrypt_private_key(&private_key, &master_key).unwrap();
228228+ let second = encrypt_private_key(&private_key, &master_key).unwrap();
229229+230230+ assert_ne!(
231231+ first, second,
232232+ "random nonce must produce distinct ciphertexts"
233233+ );
234234+ }
235235+236236+ #[test]
237237+ fn encrypted_output_is_80_base64_chars() {
238238+ let master_key = [0xab_u8; 32];
239239+ let private_key = [0x42_u8; 32];
240240+241241+ let encrypted = encrypt_private_key(&private_key, &master_key).unwrap();
242242+ assert_eq!(
243243+ encrypted.len(),
244244+ 80,
245245+ "base64(60 bytes) must be exactly 80 characters"
246246+ );
247247+ }
248248+}
+2
crates/crypto/src/lib.rs
···11// crypto: signing, Shamir secret sharing, DID operations.
2233pub mod error;
44+pub mod keys;
4556pub use error::CryptoError;
77+pub use keys::{decrypt_private_key, encrypt_private_key, generate_p256_keypair, P256Keypair};