An encrypted personal cloud built on the AT Protocol.

Split crypto.rs into crypto/ module directory

Break the monolithic crypto.rs (253 lines) into focused submodules:
crypto/mod.rs (types, constants, re-exports), content.rs (AES-256-GCM),
key_wrapping.rs (x25519-hkdf-a256kw), keyring_wrapping.rs (symmetric
AES-KW for group keys). Test file moved into module directory.

+256 -252
-252
crates/opake-core/src/crypto.rs
··· 1 - // Client-side encryption primitives. 2 - // 3 - // This module handles AES-256-GCM content encryption and asymmetric key 4 - // wrapping (x25519-hkdf-a256kw). It intentionally has no I/O — it takes 5 - // bytes in and returns bytes out. The calling layer (CLI or WASM) handles 6 - // reading/writing files and talking to the PDS. 7 - // 8 - // Randomness is injected via CryptoRng + RngCore parameters so the module 9 - // stays platform-agnostic — native callers pass OsRng, WASM callers pass 10 - // a crypto.getRandomValues()-backed RNG. 11 - 12 - use aes_gcm::{ 13 - aead::{Aead, AeadCore, KeyInit}, 14 - Aes256Gcm, Key, Nonce, 15 - }; 16 - use aes_kw::KekAes256; 17 - use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; 18 - use hkdf::Hkdf; 19 - use sha2::Sha256; 20 - use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret}; 21 - 22 - use crate::atproto::AtBytes; 23 - use crate::error::Error; 24 - use crate::records::{WrappedKey, SCHEMA_VERSION}; 25 - 26 - /// Re-export so callers don't need direct rand_core / x25519_dalek dependencies. 27 - pub use aes_gcm::aead::rand_core::{CryptoRng, OsRng, RngCore}; 28 - pub use x25519_dalek::{ 29 - PublicKey as X25519DalekPublicKey, StaticSecret as X25519DalekStaticSecret, 30 - }; 31 - 32 - const WRAP_ALGO: &str = "x25519-hkdf-a256kw"; 33 - const CONTENT_KEY_LEN: usize = 32; 34 - const AES_GCM_NONCE_LEN: usize = 12; 35 - const X25519_KEY_LEN: usize = 32; 36 - const AES_KW_OVERHEAD: usize = 8; 37 - const WRAPPED_KEY_LEN: usize = CONTENT_KEY_LEN + AES_KW_OVERHEAD; 38 - const CIPHERTEXT_LEN: usize = X25519_KEY_LEN + WRAPPED_KEY_LEN; 39 - 40 - /// A 256-bit AES content encryption key. 41 - #[derive(Debug)] 42 - pub struct ContentKey(pub [u8; CONTENT_KEY_LEN]); 43 - 44 - /// An X25519 public key: 32 raw bytes. 45 - pub type X25519PublicKey = [u8; X25519_KEY_LEN]; 46 - 47 - /// An X25519 private key: 32 raw bytes. 48 - pub type X25519PrivateKey = [u8; X25519_KEY_LEN]; 49 - 50 - /// A DID string paired with its X25519 public key. 51 - pub type DidPublicKey<'a> = (&'a str, &'a X25519PublicKey); 52 - 53 - /// The result of encrypting plaintext content. 54 - pub struct EncryptedPayload { 55 - pub ciphertext: Vec<u8>, 56 - pub nonce: [u8; AES_GCM_NONCE_LEN], 57 - } 58 - 59 - /// HKDF info string for domain separation — includes schema version so a 60 - /// version bump produces different derived keys from the same shared secret. 61 - fn hkdf_info() -> Vec<u8> { 62 - format!("opake-key-wrap-v{SCHEMA_VERSION}").into_bytes() 63 - } 64 - 65 - // --------------------------------------------------------------------------- 66 - // Content encryption (AES-256-GCM) 67 - // --------------------------------------------------------------------------- 68 - 69 - /// Generate a random AES-256-GCM content key. 70 - pub fn generate_content_key(rng: &mut (impl CryptoRng + RngCore)) -> ContentKey { 71 - ContentKey(Aes256Gcm::generate_key(rng).into()) 72 - } 73 - 74 - /// Encrypt plaintext bytes with a content key (AES-256-GCM). 75 - pub fn encrypt_blob( 76 - key: &ContentKey, 77 - plaintext: &[u8], 78 - rng: &mut (impl CryptoRng + RngCore), 79 - ) -> Result<EncryptedPayload, Error> { 80 - let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key.0)); 81 - let nonce = Aes256Gcm::generate_nonce(rng); 82 - let ciphertext = cipher 83 - .encrypt(&nonce, plaintext) 84 - .map_err(|e| Error::Encryption(e.to_string()))?; 85 - Ok(EncryptedPayload { 86 - ciphertext, 87 - nonce: nonce.into(), 88 - }) 89 - } 90 - 91 - /// Decrypt an encrypted payload with a content key. 92 - pub fn decrypt_blob(key: &ContentKey, payload: &EncryptedPayload) -> Result<Vec<u8>, Error> { 93 - let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key.0)); 94 - let nonce = Nonce::from_slice(&payload.nonce); 95 - cipher 96 - .decrypt(nonce, payload.ciphertext.as_ref()) 97 - .map_err(|e| Error::Decryption(e.to_string())) 98 - } 99 - 100 - // --------------------------------------------------------------------------- 101 - // Key wrapping (x25519-hkdf-a256kw) 102 - // --------------------------------------------------------------------------- 103 - // 104 - // Ciphertext layout: [32 bytes ephemeral X25519 pubkey || 40 bytes AES-KW wrapped content key] 105 - // 106 - // Flow (wrap): 107 - // 1. Generate ephemeral X25519 keypair 108 - // 2. ECDH: shared_secret = X25519(ephemeral_private, recipient_public) 109 - // 3. KDF: wrapping_key = HKDF-SHA256(shared_secret, info="opake-key-wrap-v1") 110 - // 4. Wrap: wrapped = AES-256-KW(wrapping_key, content_key) 111 - // 5. Pack: ciphertext = ephemeral_public || wrapped 112 - // 113 - // Flow (unwrap): reverse — split ciphertext, ECDH with stored private key, 114 - // same KDF, AES-KW unwrap. 115 - 116 - /// Derive a 256-bit wrapping key from an ECDH shared secret via HKDF-SHA256. 117 - fn derive_wrapping_key(shared_secret: &[u8; 32]) -> Result<[u8; 32], Error> { 118 - let hkdf = Hkdf::<Sha256>::new(None, shared_secret); 119 - let mut wrapping_key = [0u8; 32]; 120 - hkdf.expand(&hkdf_info(), &mut wrapping_key) 121 - .map_err(|_| Error::KeyWrap("HKDF expand failed".into()))?; 122 - Ok(wrapping_key) 123 - } 124 - 125 - /// Wrap a content key to a recipient's X25519 public key. 126 - /// 127 - /// Returns a `WrappedKey` whose `ciphertext` contains the ephemeral public key 128 - /// and AES-KW wrapped content key, base64-encoded for atproto storage. 129 - pub fn wrap_key( 130 - content_key: &ContentKey, 131 - recipient_public_key: &X25519PublicKey, 132 - recipient_did: &str, 133 - rng: &mut (impl CryptoRng + RngCore), 134 - ) -> Result<WrappedKey, Error> { 135 - let ephemeral_secret = EphemeralSecret::random_from_rng(rng); 136 - let ephemeral_public_key = PublicKey::from(&ephemeral_secret); 137 - 138 - let recipient_public_key = PublicKey::from(*recipient_public_key); 139 - let shared_secret = ephemeral_secret.diffie_hellman(&recipient_public_key); 140 - 141 - let wrapping_key = derive_wrapping_key(shared_secret.as_bytes())?; 142 - let kek = KekAes256::new((&wrapping_key).into()); 143 - let wrapped = kek 144 - .wrap_vec(&content_key.0) 145 - .map_err(|_| Error::KeyWrap("AES key wrap failed".into()))?; 146 - 147 - let mut ciphertext = Vec::with_capacity(CIPHERTEXT_LEN); 148 - ciphertext.extend_from_slice(ephemeral_public_key.as_bytes()); 149 - ciphertext.extend_from_slice(&wrapped); 150 - 151 - Ok(WrappedKey { 152 - did: recipient_did.to_string(), 153 - ciphertext: AtBytes { 154 - encoded: BASE64.encode(&ciphertext), 155 - }, 156 - algo: WRAP_ALGO.to_string(), 157 - }) 158 - } 159 - 160 - /// Unwrap a content key using the recipient's X25519 private key. 161 - pub fn unwrap_key( 162 - wrapped: &WrappedKey, 163 - private_key: &X25519PrivateKey, 164 - ) -> Result<ContentKey, Error> { 165 - let ciphertext = wrapped 166 - .ciphertext 167 - .decode() 168 - .map_err(|e| Error::Decryption(format!("base64 decode: {e}")))?; 169 - 170 - if ciphertext.len() != CIPHERTEXT_LEN { 171 - return Err(Error::Decryption(format!( 172 - "invalid ciphertext length: expected {CIPHERTEXT_LEN}, got {}", 173 - ciphertext.len() 174 - ))); 175 - } 176 - 177 - let mut ephemeral_public_key_bytes = [0u8; X25519_KEY_LEN]; 178 - ephemeral_public_key_bytes.copy_from_slice(&ciphertext[..X25519_KEY_LEN]); 179 - let ephemeral_public_key = PublicKey::from(ephemeral_public_key_bytes); 180 - let wrapped_key_bytes = &ciphertext[X25519_KEY_LEN..]; 181 - 182 - let secret = StaticSecret::from(*private_key); 183 - let shared_secret = secret.diffie_hellman(&ephemeral_public_key); 184 - 185 - let wrapping_key = derive_wrapping_key(shared_secret.as_bytes())?; 186 - let kek = KekAes256::new((&wrapping_key).into()); 187 - let content_key_bytes = kek 188 - .unwrap_vec(wrapped_key_bytes) 189 - .map_err(|_| Error::Decryption("AES key unwrap failed".into()))?; 190 - 191 - if content_key_bytes.len() != CONTENT_KEY_LEN { 192 - return Err(Error::Decryption(format!( 193 - "unwrapped key wrong length: expected {CONTENT_KEY_LEN}, got {}", 194 - content_key_bytes.len() 195 - ))); 196 - } 197 - 198 - let mut key = [0u8; CONTENT_KEY_LEN]; 199 - key.copy_from_slice(&content_key_bytes); 200 - Ok(ContentKey(key)) 201 - } 202 - 203 - /// Generate a random group key for a keyring, then wrap it to each member's public key. 204 - pub fn create_group_key( 205 - member_public_keys: &[DidPublicKey], 206 - rng: &mut (impl CryptoRng + RngCore), 207 - ) -> Result<(ContentKey, Vec<WrappedKey>), Error> { 208 - let group_key = generate_content_key(rng); 209 - let wrapped_keys: Result<Vec<_>, _> = member_public_keys 210 - .iter() 211 - .map(|(did, pubkey)| wrap_key(&group_key, pubkey, did, rng)) 212 - .collect(); 213 - Ok((group_key, wrapped_keys?)) 214 - } 215 - 216 - /// Wrap a per-document content key under a keyring's group key (symmetric AES-KW). 217 - /// 218 - /// Returns 40 bytes: the 32-byte content key + 8-byte AES-KW integrity tag. 219 - pub fn wrap_content_key_for_keyring( 220 - content_key: &ContentKey, 221 - group_key: &ContentKey, 222 - ) -> Result<Vec<u8>, Error> { 223 - let kek = KekAes256::new((&group_key.0).into()); 224 - kek.wrap_vec(&content_key.0) 225 - .map_err(|_| Error::KeyWrap("AES key wrap under group key failed".into())) 226 - } 227 - 228 - /// Unwrap a per-document content key using the keyring's group key. 229 - pub fn unwrap_content_key_from_keyring( 230 - wrapped: &[u8], 231 - group_key: &ContentKey, 232 - ) -> Result<ContentKey, Error> { 233 - if wrapped.len() != WRAPPED_KEY_LEN { 234 - return Err(Error::Decryption(format!( 235 - "keyring-wrapped key is {} bytes, expected {WRAPPED_KEY_LEN}", 236 - wrapped.len() 237 - ))); 238 - } 239 - 240 - let kek = KekAes256::new((&group_key.0).into()); 241 - let unwrapped = kek 242 - .unwrap_vec(wrapped) 243 - .map_err(|_| Error::Decryption("AES key unwrap under group key failed".into()))?; 244 - 245 - let mut key = [0u8; CONTENT_KEY_LEN]; 246 - key.copy_from_slice(&unwrapped); 247 - Ok(ContentKey(key)) 248 - } 249 - 250 - #[cfg(test)] 251 - #[path = "crypto_tests.rs"] 252 - mod tests;
+38
crates/opake-core/src/crypto/content.rs
··· 1 + use aes_gcm::{ 2 + aead::{Aead, AeadCore, KeyInit}, 3 + Aes256Gcm, Key, Nonce, 4 + }; 5 + 6 + use super::{ContentKey, CryptoRng, EncryptedPayload, RngCore}; 7 + use crate::error::Error; 8 + 9 + /// Generate a random AES-256-GCM content key. 10 + pub fn generate_content_key(rng: &mut (impl CryptoRng + RngCore)) -> ContentKey { 11 + ContentKey(Aes256Gcm::generate_key(rng).into()) 12 + } 13 + 14 + /// Encrypt plaintext bytes with a content key (AES-256-GCM). 15 + pub fn encrypt_blob( 16 + key: &ContentKey, 17 + plaintext: &[u8], 18 + rng: &mut (impl CryptoRng + RngCore), 19 + ) -> Result<EncryptedPayload, Error> { 20 + let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key.0)); 21 + let nonce = Aes256Gcm::generate_nonce(rng); 22 + let ciphertext = cipher 23 + .encrypt(&nonce, plaintext) 24 + .map_err(|e| Error::Encryption(e.to_string()))?; 25 + Ok(EncryptedPayload { 26 + ciphertext, 27 + nonce: nonce.into(), 28 + }) 29 + } 30 + 31 + /// Decrypt an encrypted payload with a content key. 32 + pub fn decrypt_blob(key: &ContentKey, payload: &EncryptedPayload) -> Result<Vec<u8>, Error> { 33 + let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(&key.0)); 34 + let nonce = Nonce::from_slice(&payload.nonce); 35 + cipher 36 + .decrypt(nonce, payload.ciphertext.as_ref()) 37 + .map_err(|e| Error::Decryption(e.to_string())) 38 + }
+114
crates/opake-core/src/crypto/key_wrapping.rs
··· 1 + use aes_kw::KekAes256; 2 + use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; 3 + use x25519_dalek::{EphemeralSecret, PublicKey, StaticSecret}; 4 + 5 + use super::{ 6 + hkdf_info, ContentKey, CryptoRng, DidPublicKey, RngCore, X25519PrivateKey, X25519PublicKey, 7 + CIPHERTEXT_LEN, CONTENT_KEY_LEN, WRAP_ALGO, X25519_KEY_LEN, 8 + }; 9 + use crate::atproto::AtBytes; 10 + use crate::error::Error; 11 + use crate::records::WrappedKey; 12 + 13 + /// Derive a 256-bit wrapping key from an ECDH shared secret via HKDF-SHA256. 14 + fn derive_wrapping_key(shared_secret: &[u8; 32]) -> Result<[u8; 32], Error> { 15 + use hkdf::Hkdf; 16 + use sha2::Sha256; 17 + 18 + let hkdf = Hkdf::<Sha256>::new(None, shared_secret); 19 + let mut wrapping_key = [0u8; 32]; 20 + hkdf.expand(&hkdf_info(), &mut wrapping_key) 21 + .map_err(|_| Error::KeyWrap("HKDF expand failed".into()))?; 22 + Ok(wrapping_key) 23 + } 24 + 25 + /// Wrap a content key to a recipient's X25519 public key. 26 + /// 27 + /// Returns a `WrappedKey` whose `ciphertext` contains the ephemeral public key 28 + /// and AES-KW wrapped content key, base64-encoded for atproto storage. 29 + pub fn wrap_key( 30 + content_key: &ContentKey, 31 + recipient_public_key: &X25519PublicKey, 32 + recipient_did: &str, 33 + rng: &mut (impl CryptoRng + RngCore), 34 + ) -> Result<WrappedKey, Error> { 35 + let ephemeral_secret = EphemeralSecret::random_from_rng(rng); 36 + let ephemeral_public_key = PublicKey::from(&ephemeral_secret); 37 + 38 + let recipient_public_key = PublicKey::from(*recipient_public_key); 39 + let shared_secret = ephemeral_secret.diffie_hellman(&recipient_public_key); 40 + 41 + let wrapping_key = derive_wrapping_key(shared_secret.as_bytes())?; 42 + let kek = KekAes256::new((&wrapping_key).into()); 43 + let wrapped = kek 44 + .wrap_vec(&content_key.0) 45 + .map_err(|_| Error::KeyWrap("AES key wrap failed".into()))?; 46 + 47 + let mut ciphertext = Vec::with_capacity(CIPHERTEXT_LEN); 48 + ciphertext.extend_from_slice(ephemeral_public_key.as_bytes()); 49 + ciphertext.extend_from_slice(&wrapped); 50 + 51 + Ok(WrappedKey { 52 + did: recipient_did.to_string(), 53 + ciphertext: AtBytes { 54 + encoded: BASE64.encode(&ciphertext), 55 + }, 56 + algo: WRAP_ALGO.to_string(), 57 + }) 58 + } 59 + 60 + /// Unwrap a content key using the recipient's X25519 private key. 61 + pub fn unwrap_key( 62 + wrapped: &WrappedKey, 63 + private_key: &X25519PrivateKey, 64 + ) -> Result<ContentKey, Error> { 65 + let ciphertext = wrapped 66 + .ciphertext 67 + .decode() 68 + .map_err(|e| Error::Decryption(format!("base64 decode: {e}")))?; 69 + 70 + if ciphertext.len() != CIPHERTEXT_LEN { 71 + return Err(Error::Decryption(format!( 72 + "invalid ciphertext length: expected {CIPHERTEXT_LEN}, got {}", 73 + ciphertext.len() 74 + ))); 75 + } 76 + 77 + let mut ephemeral_public_key_bytes = [0u8; X25519_KEY_LEN]; 78 + ephemeral_public_key_bytes.copy_from_slice(&ciphertext[..X25519_KEY_LEN]); 79 + let ephemeral_public_key = PublicKey::from(ephemeral_public_key_bytes); 80 + let wrapped_key_bytes = &ciphertext[X25519_KEY_LEN..]; 81 + 82 + let secret = StaticSecret::from(*private_key); 83 + let shared_secret = secret.diffie_hellman(&ephemeral_public_key); 84 + 85 + let wrapping_key = derive_wrapping_key(shared_secret.as_bytes())?; 86 + let kek = KekAes256::new((&wrapping_key).into()); 87 + let content_key_bytes = kek 88 + .unwrap_vec(wrapped_key_bytes) 89 + .map_err(|_| Error::Decryption("AES key unwrap failed".into()))?; 90 + 91 + if content_key_bytes.len() != CONTENT_KEY_LEN { 92 + return Err(Error::Decryption(format!( 93 + "unwrapped key wrong length: expected {CONTENT_KEY_LEN}, got {}", 94 + content_key_bytes.len() 95 + ))); 96 + } 97 + 98 + let mut key = [0u8; CONTENT_KEY_LEN]; 99 + key.copy_from_slice(&content_key_bytes); 100 + Ok(ContentKey(key)) 101 + } 102 + 103 + /// Generate a random group key for a keyring, then wrap it to each member's public key. 104 + pub fn create_group_key( 105 + member_public_keys: &[DidPublicKey], 106 + rng: &mut (impl CryptoRng + RngCore), 107 + ) -> Result<(ContentKey, Vec<WrappedKey>), Error> { 108 + let group_key = super::generate_content_key(rng); 109 + let wrapped_keys: Result<Vec<_>, _> = member_public_keys 110 + .iter() 111 + .map(|(did, pubkey)| wrap_key(&group_key, pubkey, did, rng)) 112 + .collect(); 113 + Ok((group_key, wrapped_keys?)) 114 + }
+38
crates/opake-core/src/crypto/keyring_wrapping.rs
··· 1 + use aes_kw::KekAes256; 2 + 3 + use super::{ContentKey, CONTENT_KEY_LEN, WRAPPED_KEY_LEN}; 4 + use crate::error::Error; 5 + 6 + /// Wrap a per-document content key under a keyring's group key (symmetric AES-KW). 7 + /// 8 + /// Returns 40 bytes: the 32-byte content key + 8-byte AES-KW integrity tag. 9 + pub fn wrap_content_key_for_keyring( 10 + content_key: &ContentKey, 11 + group_key: &ContentKey, 12 + ) -> Result<Vec<u8>, Error> { 13 + let kek = KekAes256::new((&group_key.0).into()); 14 + kek.wrap_vec(&content_key.0) 15 + .map_err(|_| Error::KeyWrap("AES key wrap under group key failed".into())) 16 + } 17 + 18 + /// Unwrap a per-document content key using the keyring's group key. 19 + pub fn unwrap_content_key_from_keyring( 20 + wrapped: &[u8], 21 + group_key: &ContentKey, 22 + ) -> Result<ContentKey, Error> { 23 + if wrapped.len() != WRAPPED_KEY_LEN { 24 + return Err(Error::Decryption(format!( 25 + "keyring-wrapped key is {} bytes, expected {WRAPPED_KEY_LEN}", 26 + wrapped.len() 27 + ))); 28 + } 29 + 30 + let kek = KekAes256::new((&group_key.0).into()); 31 + let unwrapped = kek 32 + .unwrap_vec(wrapped) 33 + .map_err(|_| Error::Decryption("AES key unwrap under group key failed".into()))?; 34 + 35 + let mut key = [0u8; CONTENT_KEY_LEN]; 36 + key.copy_from_slice(&unwrapped); 37 + Ok(ContentKey(key)) 38 + }
+64
crates/opake-core/src/crypto/mod.rs
··· 1 + // Client-side encryption primitives. 2 + // 3 + // This module handles AES-256-GCM content encryption and asymmetric key 4 + // wrapping (x25519-hkdf-a256kw). It intentionally has no I/O — it takes 5 + // bytes in and returns bytes out. The calling layer (CLI or WASM) handles 6 + // reading/writing files and talking to the PDS. 7 + // 8 + // Randomness is injected via CryptoRng + RngCore parameters so the module 9 + // stays platform-agnostic — native callers pass OsRng, WASM callers pass 10 + // a crypto.getRandomValues()-backed RNG. 11 + 12 + mod content; 13 + mod key_wrapping; 14 + mod keyring_wrapping; 15 + 16 + use crate::records::SCHEMA_VERSION; 17 + 18 + /// Re-export so callers don't need direct rand_core / x25519_dalek dependencies. 19 + pub use aes_gcm::aead::rand_core::{CryptoRng, OsRng, RngCore}; 20 + pub use x25519_dalek::{ 21 + PublicKey as X25519DalekPublicKey, StaticSecret as X25519DalekStaticSecret, 22 + }; 23 + 24 + // Re-export all public items at the `crypto::` level. 25 + pub use content::{decrypt_blob, encrypt_blob, generate_content_key}; 26 + pub use key_wrapping::{create_group_key, unwrap_key, wrap_key}; 27 + pub use keyring_wrapping::{unwrap_content_key_from_keyring, wrap_content_key_for_keyring}; 28 + 29 + const WRAP_ALGO: &str = "x25519-hkdf-a256kw"; 30 + const CONTENT_KEY_LEN: usize = 32; 31 + const AES_GCM_NONCE_LEN: usize = 12; 32 + const X25519_KEY_LEN: usize = 32; 33 + const AES_KW_OVERHEAD: usize = 8; 34 + const WRAPPED_KEY_LEN: usize = CONTENT_KEY_LEN + AES_KW_OVERHEAD; 35 + const CIPHERTEXT_LEN: usize = X25519_KEY_LEN + WRAPPED_KEY_LEN; 36 + 37 + /// A 256-bit AES content encryption key. 38 + #[derive(Debug)] 39 + pub struct ContentKey(pub [u8; CONTENT_KEY_LEN]); 40 + 41 + /// An X25519 public key: 32 raw bytes. 42 + pub type X25519PublicKey = [u8; X25519_KEY_LEN]; 43 + 44 + /// An X25519 private key: 32 raw bytes. 45 + pub type X25519PrivateKey = [u8; X25519_KEY_LEN]; 46 + 47 + /// A DID string paired with its X25519 public key. 48 + pub type DidPublicKey<'a> = (&'a str, &'a X25519PublicKey); 49 + 50 + /// The result of encrypting plaintext content. 51 + pub struct EncryptedPayload { 52 + pub ciphertext: Vec<u8>, 53 + pub nonce: [u8; AES_GCM_NONCE_LEN], 54 + } 55 + 56 + /// HKDF info string for domain separation — includes schema version so a 57 + /// version bump produces different derived keys from the same shared secret. 58 + fn hkdf_info() -> Vec<u8> { 59 + format!("opake-key-wrap-v{SCHEMA_VERSION}").into_bytes() 60 + } 61 + 62 + #[cfg(test)] 63 + #[path = "crypto_tests.rs"] 64 + mod tests;
+2
crates/opake-core/src/crypto_tests.rs crates/opake-core/src/crypto/crypto_tests.rs
··· 1 1 use super::*; 2 2 use aes_gcm::aead::rand_core::OsRng; 3 + use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; 4 + use x25519_dalek::{PublicKey, StaticSecret}; 3 5 4 6 // -- Content encryption tests (AES-256-GCM) -- 5 7