Skip to content

Commit 50ffa95

Browse files
authored
feat: CRP-2847 Add AES-GCM encryption helpers to the Rust library (#220)
This matches the behavior of the functions in the TS frontend library
1 parent 25b5030 commit 50ffa95

File tree

4 files changed

+286
-0
lines changed

4 files changed

+286
-0
lines changed

Cargo.lock

Lines changed: 103 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

backend/rs/ic_vetkeys/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ readme = "README.md"
2424
crate-type = ["lib"]
2525

2626
[dependencies]
27+
aes-gcm = "0.10"
2728
anyhow = { workspace = true }
2829
candid = { workspace = true }
2930
ic_bls12_381 = { version = "0.10.1", default-features = false, features = [

backend/rs/ic_vetkeys/src/utils/mod.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,16 @@ impl VetKey {
529529
derive_symmetric_key(self.serialize(), domain_sep, output_len)
530530
}
531531

532+
/**
533+
* Return a DerivedKeyMaterial
534+
*
535+
* This class allows further key derivation and encryption but the underlying
536+
* secret key cannot be extracted.
537+
*/
538+
pub fn as_derived_key_material(&self) -> DerivedKeyMaterial {
539+
DerivedKeyMaterial::new(self)
540+
}
541+
532542
/**
533543
* Deserialize a VetKey from the byte encoding
534544
*
@@ -547,6 +557,109 @@ impl VetKey {
547557
}
548558
}
549559

560+
/// Key material derived from a VetKey
561+
///
562+
/// This struct allows deriving further keys from the VetKey without
563+
/// allowing direct access to the VetKey secret key, preventing it
564+
/// from being reused inappropriately.
565+
///
566+
/// As a convenience this struct also offers AES-GCM encryption/decryption
567+
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
568+
pub struct DerivedKeyMaterial {
569+
key: Vec<u8>,
570+
}
571+
572+
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
573+
/// An error while encrypting
574+
pub enum EncryptionError {
575+
/// The provided message was too long to be encrypted
576+
PlaintextTooLong,
577+
}
578+
579+
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
580+
/// An error while decrypting
581+
pub enum DecryptionError {
582+
/// The ciphertext was too short to possibly be valid
583+
MessageTooShort,
584+
/// The GCM tag did not validate
585+
InvalidCiphertext,
586+
}
587+
588+
impl DerivedKeyMaterial {
589+
const GCM_KEY_SIZE: usize = 32;
590+
const GCM_TAG_SIZE: usize = 16;
591+
const GCM_NONCE_SIZE: usize = 12;
592+
593+
/// Create a new DerivedKeyMaterial
594+
fn new(vetkey: &VetKey) -> Self {
595+
Self {
596+
key: vetkey.serialize().to_vec(),
597+
}
598+
}
599+
600+
fn derive_aes_gcm_key(&self, domain_sep: &str) -> Vec<u8> {
601+
derive_symmetric_key(&self.key, domain_sep, Self::GCM_KEY_SIZE)
602+
}
603+
604+
/// Encrypt a message
605+
///
606+
/// The decryption used here is interoperable with the TypeScript
607+
/// library ic_vetkeys function `DerivedKeyMaterial.decryptMessage`
608+
pub fn encrypt_message<R: rand::RngCore + rand::CryptoRng>(
609+
&self,
610+
message: &[u8],
611+
domain_sep: &str,
612+
rng: &mut R,
613+
) -> Result<Vec<u8>, EncryptionError> {
614+
use aes_gcm::{aead::Aead, aead::AeadCore, Aes256Gcm, Key, KeyInit};
615+
let key = self.derive_aes_gcm_key(domain_sep);
616+
let key = Key::<Aes256Gcm>::from_slice(&key);
617+
// aes_gcm::Aes256Gcm only supports/uses 12 byte nonces
618+
let nonce = Aes256Gcm::generate_nonce(rng);
619+
assert_eq!(nonce.len(), Self::GCM_NONCE_SIZE);
620+
let gcm = Aes256Gcm::new(key);
621+
622+
// The function returns an opaque `Error` with no details, but upon
623+
// examination, the only way it can fail is if the plaintext is larger
624+
// than GCM's maximum input length of 2^36 bytes.
625+
let ctext = gcm
626+
.encrypt(&nonce, message)
627+
.map_err(|_| EncryptionError::PlaintextTooLong)?;
628+
629+
let mut res = vec![];
630+
res.extend_from_slice(nonce.as_slice());
631+
res.extend_from_slice(ctext.as_slice());
632+
Ok(res)
633+
}
634+
635+
/// Decrypt a message
636+
///
637+
/// The decryption used here is interoperable with the TypeScript
638+
/// library ic_vetkeys function `DerivedKeyMaterial.encryptMessage`
639+
pub fn decrypt_message(
640+
&self,
641+
ctext: &[u8],
642+
domain_sep: &str,
643+
) -> Result<Vec<u8>, DecryptionError> {
644+
use aes_gcm::{aead::Aead, Aes256Gcm, Key, KeyInit};
645+
let key = self.derive_aes_gcm_key(domain_sep);
646+
let key = Key::<Aes256Gcm>::from_slice(&key);
647+
648+
// Minimum possible length 12 bytes nonce plus 16 bytes GCM tag
649+
if ctext.len() < Self::GCM_NONCE_SIZE + Self::GCM_TAG_SIZE {
650+
return Err(DecryptionError::MessageTooShort);
651+
}
652+
let nonce = aes_gcm::Nonce::from_slice(&ctext[0..Self::GCM_NONCE_SIZE]);
653+
let gcm = Aes256Gcm::new(key);
654+
655+
let ptext = gcm
656+
.decrypt(nonce, &ctext[Self::GCM_NONCE_SIZE..])
657+
.map_err(|_| DecryptionError::InvalidCiphertext)?;
658+
659+
Ok(ptext.as_slice().to_vec())
660+
}
661+
}
662+
550663
#[derive(Copy, Clone, Debug)]
551664
/// Error indicating that deserializing an encrypted key failed
552665
pub enum EncryptedVetKeyDeserializationError {

backend/rs/ic_vetkeys/tests/utils.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,72 @@ fn vrf_from_prod_can_be_verified() {
285285
"a484fc1e8a2b0dca99beb6f4409370f5c6932a931e47a7625c3bfe9e1f9af37f"
286286
);
287287
}
288+
289+
#[test]
290+
fn aes_gcm_encryption() {
291+
let dkm = VetKey::deserialize(&hex!("ad19676dd92f116db11f326ff0822f295d87cc00cf65d9f132b5a618bb7381e5b0c3cb814f15e4a0f015359dcfa8a1da")).unwrap().as_derived_key_material();
292+
293+
let test_message = b"stay calm, this is only a test";
294+
let domain_sep = "ic-test-domain-sep";
295+
296+
// Test string encryption path, then decryption
297+
298+
let mut rng = reproducible_rng();
299+
300+
let ctext = dkm
301+
.encrypt_message(test_message, domain_sep, &mut rng)
302+
.unwrap();
303+
assert_eq!(
304+
dkm.decrypt_message(&ctext, domain_sep).unwrap(),
305+
test_message,
306+
);
307+
308+
// Test decryption of known ciphertext encrypted with the derived key
309+
let fixed_ctext = hex!("476f440e30bb95fff1420ce41ba6a07e03c3fcc0a751cfb23e64a8dcb0fc2b1eb74e2d4768f5c4dccbf2526609156664046ad27a6e78bd93bb8b");
310+
311+
assert_eq!(
312+
dkm.decrypt_message(&fixed_ctext, domain_sep).unwrap(),
313+
test_message,
314+
);
315+
316+
// Test decryption of various mutated or truncated ciphertexts: all should fail
317+
318+
// Test sequentially flipping each bit
319+
320+
for i in 0..ctext.len() * 8 {
321+
let mod_ctext = {
322+
let mut m = ctext.clone();
323+
m[i / 8] ^= 0x80 >> i % 8;
324+
m
325+
};
326+
327+
assert!(dkm.decrypt_message(&mod_ctext, domain_sep).is_err());
328+
}
329+
330+
// Test truncating
331+
332+
for i in 0..ctext.len() - 1 {
333+
let mod_ctext = {
334+
let mut m = ctext.clone();
335+
m.truncate(i);
336+
m
337+
};
338+
339+
assert!(dkm.decrypt_message(&mod_ctext, domain_sep).is_err());
340+
}
341+
342+
// Test appending random bytes
343+
344+
for i in 1..32 {
345+
let mod_ctext = {
346+
use rand::RngCore;
347+
let mut m = ctext.clone();
348+
let mut extra = vec![0u8; i];
349+
rng.fill_bytes(&mut extra);
350+
m.extend_from_slice(&extra);
351+
m
352+
};
353+
354+
assert!(dkm.decrypt_message(&mod_ctext, domain_sep).is_err());
355+
}
356+
}

0 commit comments

Comments
 (0)