Add AES-256-GCM
encrypt/decrypt to Rust FFI for Dart cache pipeline Exposes salvium_aes256gcm_encrypt and salvium_aes256gcm_decrypt via the native shared library. Encrypt generates a random 12-byte nonce internally; output is nonce||ciphertext||tag (input_len + 28 bytes). Dart side: gzip → FFI encrypt → disk, disk → FFI decrypt → gunzip.
This commit is contained in:
Generated
+103
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user