// Licensed under the Apache License, Version 2.0 or the MIT license // , at your // option. This file may not be copied, modified, or distributed // except according to those terms. use std::mem; use neqo_common::{Encoder, hex, qinfo, qtrace}; use crate::{ Aead, aead::Aead as _, constants::{Cipher, Version}, err::{Error, Res}, hkdf, p11::{SymKey, random}, }; #[derive(Debug)] pub struct SelfEncrypt { version: Version, cipher: Cipher, key_id: u8, key: SymKey, old_key: Option, } impl SelfEncrypt { const VERSION: u8 = 1; const SALT_LENGTH: usize = 16; /// # Errors /// /// Failure to generate a new HKDF key using NSS results in an error. pub fn new(version: Version, cipher: Cipher) -> Res { let key = hkdf::generate_key(version, cipher)?; Ok(Self { version, cipher, key_id: 0, key, old_key: None, }) } fn make_aead(&self, k: &SymKey, salt: &[u8]) -> Res { debug_assert_eq!(salt.len(), Self::SALT_LENGTH); let salt = hkdf::import_key(self.version, salt)?; let secret = hkdf::extract(self.version, self.cipher, Some(&salt), k)?; Aead::new(self.version, self.cipher, &secret, "neqo self") } /// Rotate keys. This causes any previous key that is being held to be replaced by the current /// key. /// /// # Errors /// /// Failure to generate a new HKDF key using NSS results in an error. pub fn rotate(&mut self) -> Res<()> { let new_key = hkdf::generate_key(self.version, self.cipher)?; self.old_key = Some(mem::replace(&mut self.key, new_key)); let (kid, _) = self.key_id.overflowing_add(1); self.key_id = kid; qinfo!("[SelfEncrypt] Rotated keys to {}", self.key_id); Ok(()) } /// Seal an item using the underlying key. This produces a single buffer that contains /// the encrypted `plaintext`, plus a version number and salt. /// `aad` is only used as input to the AEAD, it is not included in the output; the /// caller is responsible for carrying the AAD as appropriate. /// /// # Errors /// /// Failure to protect using NSS AEAD APIs produces an error. pub fn seal(&self, aad: &[u8], plaintext: &[u8]) -> Res> { // Format is: // struct { // uint8 version; // uint8 key_id; // uint8 salt[16]; // opaque aead_encrypted(plaintext)[length as expanded]; // }; // AAD covers the entire header, plus the value of the AAD parameter that is provided. let salt = random::<{ Self::SALT_LENGTH }>(); let cipher = self.make_aead(&self.key, &salt)?; let encoded_len = 2 + salt.len() + plaintext.len() + cipher.expansion(); let mut enc = Encoder::with_capacity(encoded_len); enc.encode_byte(Self::VERSION); enc.encode_byte(self.key_id); enc.encode(salt); let mut extended_aad = enc.clone(); extended_aad.encode(aad); let offset = enc.len(); let mut output: Vec = enc.into(); output.resize(encoded_len, 0); cipher.encrypt(0, extended_aad.as_ref(), plaintext, &mut output[offset..])?; qtrace!( "[SelfEncrypt] seal {} {} -> {}", hex(aad), hex(plaintext), hex(&output) ); Ok(output) } const fn select_key(&self, kid: u8) -> Option<&SymKey> { if kid == self.key_id { Some(&self.key) } else { let (prev_key_id, _) = self.key_id.overflowing_sub(1); if kid == prev_key_id { self.old_key.as_ref() } else { None } } } /// Open the protected `ciphertext`. /// /// # Errors /// /// Returns an error when the self-encrypted object is invalid; /// when the keys have been rotated; or when NSS fails. #[expect(clippy::similar_names, reason = "aad is similar to aead.")] pub fn open(&self, aad: &[u8], ciphertext: &[u8]) -> Res> { const OFFSET: usize = 2 + SelfEncrypt::SALT_LENGTH; if *ciphertext.first().ok_or(Error::SelfEncrypt)? != Self::VERSION { return Err(Error::SelfEncrypt); } let Some(key) = self.select_key(*ciphertext.get(1).ok_or(Error::SelfEncrypt)?) else { return Err(Error::SelfEncrypt); }; let salt = ciphertext.get(2..OFFSET).ok_or(Error::SelfEncrypt)?; let mut extended_aad = Encoder::with_capacity(OFFSET + aad.len()); extended_aad.encode(&ciphertext[..OFFSET]); extended_aad.encode(aad); let aead = self.make_aead(key, salt)?; // NSS insists on having extra space available for decryption. let padded_len = ciphertext.len() - OFFSET; let mut output = vec![0; padded_len]; let decrypted = aead.decrypt(0, extended_aad.as_ref(), &ciphertext[OFFSET..], &mut output)?; let final_len = decrypted.len(); output.truncate(final_len); qtrace!( "[SelfEncrypt] open {} {} -> {}", hex(aad), hex(ciphertext), hex(&output) ); Ok(output) } }