diff --git a/crates/salvium-crypto/Cargo.lock b/crates/salvium-crypto/Cargo.lock index cdf0365..0609b08 100644 --- a/crates/salvium-crypto/Cargo.lock +++ b/crates/salvium-crypto/Cargo.lock @@ -2,6 +2,41 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "argon2" version = "0.5.3" @@ -85,6 +120,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "const-oid" version = "0.9.6" @@ -131,9 +176,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -273,6 +328,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "group" version = "0.13.0" @@ -293,6 +358,15 @@ dependencies = [ "digest", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "js-sys" version = "0.3.85" @@ -375,6 +449,12 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "p256" version = "0.13.2" @@ -417,6 +497,18 @@ dependencies = [ "spki", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -511,6 +603,7 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" name = "salvium-crypto" version = "0.1.0" dependencies = [ + "aes-gcm", "argon2", "blake2b_simd", "curve25519-dalek", @@ -626,6 +719,16 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "version_check" version = "0.9.5" diff --git a/crates/salvium-crypto/Cargo.toml b/crates/salvium-crypto/Cargo.toml index 7c8ceee..742da3b 100644 --- a/crates/salvium-crypto/Cargo.toml +++ b/crates/salvium-crypto/Cargo.toml @@ -21,6 +21,9 @@ getrandom = { version = "0.2", features = ["js"] } sha2 = "0.10" argon2 = "0.5" +# AES-256-GCM for wallet cache encryption +aes-gcm = "0.10" + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] p256 = { version = "0.13", features = ["ecdsa"] } dsa = "0.6" diff --git a/crates/salvium-crypto/include/salvium_crypto.h b/crates/salvium-crypto/include/salvium_crypto.h index 7b04912..418fee7 100644 --- a/crates/salvium-crypto/include/salvium_crypto.h +++ b/crates/salvium-crypto/include/salvium_crypto.h @@ -266,6 +266,37 @@ int32_t salvium_bulletproof_plus_verify( const uint8_t *commitments /* commitment_count * 32 */, uint32_t commitment_count); +/* ─── AES-256-GCM Encryption ───────────────────────────────────────────── */ + +/** + * AES-256-GCM encrypt. + * Rust generates a random 12-byte nonce internally. + * Output: nonce(12) || ciphertext || tag(16). Size = plaintext_len + 28. + * out must be at least plaintext_len + 28 bytes. + * out_len receives actual output length. + * Returns 0 on success, -1 on error. + */ +int32_t salvium_aes256gcm_encrypt( + const uint8_t *key /* 32 */, + const uint8_t *plaintext, + size_t plaintext_len, + uint8_t *out, + size_t *out_len); + +/** + * AES-256-GCM decrypt. + * Input: nonce(12) || ciphertext || tag(16). + * out must be at least ciphertext_len - 28 bytes. + * out_len receives actual output length (plaintext size). + * Returns 0 on success, -1 on error (authentication failure or bad input). + */ +int32_t salvium_aes256gcm_decrypt( + const uint8_t *key /* 32 */, + const uint8_t *ciphertext, + size_t ciphertext_len, + uint8_t *out, + size_t *out_len); + #ifdef __cplusplus } #endif diff --git a/crates/salvium-crypto/src/ffi.rs b/crates/salvium-crypto/src/ffi.rs index 3379c3e..6f5105d 100644 --- a/crates/salvium-crypto/src/ffi.rs +++ b/crates/salvium-crypto/src/ffi.rs @@ -450,6 +450,107 @@ pub unsafe extern "C" fn salvium_argon2id( } } +// ─── AES-256-GCM Encryption ───────────────────────────────────────────────── + +/// AES-256-GCM encrypt. +/// Rust generates a random 12-byte nonce internally. +/// Output layout: nonce(12) || ciphertext || tag(16). +/// out must be at least plaintext_len + 28 bytes. +/// out_len receives actual output length. +/// Returns 0 on success, -1 on error. +#[no_mangle] +pub unsafe extern "C" fn salvium_aes256gcm_encrypt( + key: *const u8, + plaintext: *const u8, + plaintext_len: usize, + out: *mut u8, + out_len: *mut usize, +) -> i32 { + catch_ffi(|| { + use aes_gcm::{Aes256Gcm, KeyInit, AeadInPlace, Nonce}; + use aes_gcm::aead::OsRng; + use aes_gcm::aead::rand_core::RngCore; + + let key_slice = slice::from_raw_parts(key, 32); + let plaintext_slice = slice::from_raw_parts(plaintext, plaintext_len); + + let cipher = match Aes256Gcm::new_from_slice(key_slice) { + Ok(c) => c, + Err(_) => return -1, + }; + + // Generate random 12-byte nonce + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + + // Encrypt in-place: copy plaintext to output buffer after nonce + let out_slice = slice::from_raw_parts_mut(out, plaintext_len + 28); + // Write nonce first + ptr::copy_nonoverlapping(nonce_bytes.as_ptr(), out_slice.as_mut_ptr(), 12); + // Copy plaintext after nonce + ptr::copy_nonoverlapping(plaintext_slice.as_ptr(), out_slice[12..].as_mut_ptr(), plaintext_len); + + // Encrypt in-place (appends 16-byte tag) + let mut buffer = out_slice[12..12 + plaintext_len].to_vec(); + match cipher.encrypt_in_place(nonce, b"", &mut buffer) { + Ok(_) => {}, + Err(_) => return -1, + } + + // buffer is now ciphertext + tag + ptr::copy_nonoverlapping(buffer.as_ptr(), out_slice[12..].as_mut_ptr(), buffer.len()); + *out_len = 12 + buffer.len(); // nonce + ciphertext + tag + 0 + }) +} + +/// AES-256-GCM decrypt. +/// Input layout: nonce(12) || ciphertext || tag(16). +/// out must be at least ciphertext_len - 28 bytes. +/// out_len receives actual output length. +/// Returns 0 on success, -1 on error (authentication failure or bad input). +#[no_mangle] +pub unsafe extern "C" fn salvium_aes256gcm_decrypt( + key: *const u8, + ciphertext: *const u8, + ciphertext_len: usize, + out: *mut u8, + out_len: *mut usize, +) -> i32 { + catch_ffi(|| { + use aes_gcm::{Aes256Gcm, KeyInit, AeadInPlace, Nonce}; + + if ciphertext_len < 28 { + return -1; // Too short: need at least nonce(12) + tag(16) + } + + let key_slice = slice::from_raw_parts(key, 32); + let input = slice::from_raw_parts(ciphertext, ciphertext_len); + + let cipher = match Aes256Gcm::new_from_slice(key_slice) { + Ok(c) => c, + Err(_) => return -1, + }; + + // Read nonce from first 12 bytes + let nonce = Nonce::from_slice(&input[..12]); + + // Decrypt in-place: ciphertext + tag is input[12..] + let mut buffer = input[12..].to_vec(); + match cipher.decrypt_in_place(nonce, b"", &mut buffer) { + Ok(_) => {}, + Err(_) => return -1, + } + + // buffer is now plaintext + let out_slice = slice::from_raw_parts_mut(out, buffer.len()); + ptr::copy_nonoverlapping(buffer.as_ptr(), out_slice.as_mut_ptr(), buffer.len()); + *out_len = buffer.len(); + 0 + }) +} + // ─── X25519 Montgomery-curve Scalar Multiplication ────────────────────────── /// X25519 scalar multiplication with Salvium's non-standard clamping. @@ -1268,4 +1369,134 @@ mod tests { // Should return 0 (invalid sig), not crash assert_eq!(rc, 0); } + + #[test] + fn test_aes256gcm_roundtrip() { + let key = [0x42u8; 32]; + let plaintext = b"Hello, Salvium wallet cache encryption!"; + let mut encrypted = [0u8; 256]; + let mut enc_len: usize = 0; + let rc = unsafe { + salvium_aes256gcm_encrypt( + key.as_ptr(), + plaintext.as_ptr(), + plaintext.len(), + encrypted.as_mut_ptr(), + &mut enc_len as *mut usize, + ) + }; + assert_eq!(rc, 0); + assert_eq!(enc_len, plaintext.len() + 28); // nonce(12) + data + tag(16) + + let mut decrypted = [0u8; 256]; + let mut dec_len: usize = 0; + let rc = unsafe { + salvium_aes256gcm_decrypt( + key.as_ptr(), + encrypted.as_ptr(), + enc_len, + decrypted.as_mut_ptr(), + &mut dec_len as *mut usize, + ) + }; + assert_eq!(rc, 0); + assert_eq!(dec_len, plaintext.len()); + assert_eq!(&decrypted[..dec_len], &plaintext[..]); + } + + #[test] + fn test_aes256gcm_wrong_key() { + let key = [0x42u8; 32]; + let wrong_key = [0x43u8; 32]; + let plaintext = b"secret data"; + let mut encrypted = [0u8; 256]; + let mut enc_len: usize = 0; + let rc = unsafe { + salvium_aes256gcm_encrypt( + key.as_ptr(), plaintext.as_ptr(), plaintext.len(), + encrypted.as_mut_ptr(), &mut enc_len as *mut usize, + ) + }; + assert_eq!(rc, 0); + + let mut decrypted = [0u8; 256]; + let mut dec_len: usize = 0; + let rc = unsafe { + salvium_aes256gcm_decrypt( + wrong_key.as_ptr(), encrypted.as_ptr(), enc_len, + decrypted.as_mut_ptr(), &mut dec_len as *mut usize, + ) + }; + assert_eq!(rc, -1); // Authentication failure + } + + #[test] + fn test_aes256gcm_tampered_ciphertext() { + let key = [0x42u8; 32]; + let plaintext = b"important data"; + let mut encrypted = [0u8; 256]; + let mut enc_len: usize = 0; + let rc = unsafe { + salvium_aes256gcm_encrypt( + key.as_ptr(), plaintext.as_ptr(), plaintext.len(), + encrypted.as_mut_ptr(), &mut enc_len as *mut usize, + ) + }; + assert_eq!(rc, 0); + + // Flip a byte in the ciphertext portion + encrypted[15] ^= 0xff; + + let mut decrypted = [0u8; 256]; + let mut dec_len: usize = 0; + let rc = unsafe { + salvium_aes256gcm_decrypt( + key.as_ptr(), encrypted.as_ptr(), enc_len, + decrypted.as_mut_ptr(), &mut dec_len as *mut usize, + ) + }; + assert_eq!(rc, -1); // Authentication failure + } + + #[test] + fn test_aes256gcm_too_short() { + let key = [0x42u8; 32]; + let short = [0u8; 20]; // Less than 28 bytes + let mut decrypted = [0u8; 256]; + let mut dec_len: usize = 0; + let rc = unsafe { + salvium_aes256gcm_decrypt( + key.as_ptr(), short.as_ptr(), short.len(), + decrypted.as_mut_ptr(), &mut dec_len as *mut usize, + ) + }; + assert_eq!(rc, -1); + } + + #[test] + fn test_aes256gcm_empty_plaintext() { + let key = [0xABu8; 32]; + let plaintext = b""; + let mut encrypted = [0u8; 64]; + let mut enc_len: usize = 0; + let rc = unsafe { + salvium_aes256gcm_encrypt( + key.as_ptr(), plaintext.as_ptr(), 0, + encrypted.as_mut_ptr(), &mut enc_len as *mut usize, + ) + }; + assert_eq!(rc, 0); + assert_eq!(enc_len, 28); // nonce(12) + tag(16), no ciphertext body + + let mut decrypted = [0u8; 64]; + let mut dec_len: usize = 0; + let rc = unsafe { + salvium_aes256gcm_decrypt( + key.as_ptr(), encrypted.as_ptr(), enc_len, + decrypted.as_mut_ptr(), &mut dec_len as *mut usize, + ) + }; + assert_eq!(rc, 0); + assert_eq!(dec_len, 0); + } } diff --git a/src/crypto/backend-ffi.js b/src/crypto/backend-ffi.js index b327035..f58c05e 100644 --- a/src/crypto/backend-ffi.js +++ b/src/crypto/backend-ffi.js @@ -153,6 +153,10 @@ const FFI_SYMBOLS = { // BP+ salvium_bulletproof_plus_prove: { args: [ptr, ptr, u32, ptr, usize, ptr], returns: i32 }, salvium_bulletproof_plus_verify: { args: [ptr, usize, ptr, u32], returns: i32 }, + + // AES-256-GCM + salvium_aes256gcm_encrypt: { args: [ptr, ptr, usize, ptr, ptr], returns: i32 }, + salvium_aes256gcm_decrypt: { args: [ptr, ptr, usize, ptr, ptr], returns: i32 }, }; // ─── Backend class ────────────────────────────────────────────────────────── @@ -576,4 +580,35 @@ export class FfiCryptoBackend { ); return rc === 1; } + + // ─── AES-256-GCM Encryption ───────────────────────────────────────────── + + aes256gcmEncrypt(key, plaintext) { + const bKey = ensureBuffer(key); + const bPlain = ensureBuffer(plaintext); + const outSize = bPlain.length + 28; // nonce(12) + ciphertext + tag(16) + const out = Buffer.alloc(outSize); + const outLenBuf = Buffer.alloc(8); // size_t = 8 bytes on 64-bit + const rc = this.lib.symbols.salvium_aes256gcm_encrypt( + bKey, bPlain, bPlain.length, out, outLenBuf + ); + if (rc !== 0) throw new Error('aes256gcm_encrypt failed'); + const actualLen = Number(outLenBuf.readBigUInt64LE(0)); + return new Uint8Array(out.slice(0, actualLen)); + } + + aes256gcmDecrypt(key, ciphertext) { + const bKey = ensureBuffer(key); + const bCipher = ensureBuffer(ciphertext); + if (bCipher.length < 28) throw new Error('ciphertext too short'); + const outSize = bCipher.length - 28; + const out = Buffer.alloc(outSize); + const outLenBuf = Buffer.alloc(8); + const rc = this.lib.symbols.salvium_aes256gcm_decrypt( + bKey, bCipher, bCipher.length, out, outLenBuf + ); + if (rc !== 0) throw new Error('aes256gcm_decrypt failed (authentication or key error)'); + const actualLen = Number(outLenBuf.readBigUInt64LE(0)); + return new Uint8Array(out.slice(0, actualLen)); + } }