Implement encryption
This commit is contained in:
parent
cfdfcec03e
commit
14e2fc2ac6
7 changed files with 184 additions and 9 deletions
|
@ -20,5 +20,6 @@ i-understand-and-accept-the-risks = []
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64ct = { version = "1.6.0", default-features = false, features = ["std"] }
|
base64ct = { version = "1.6.0", default-features = false, features = ["std"] }
|
||||||
blake3 = { version = "1.5.0", default-features = false }
|
blake3 = { version = "1.5.0", default-features = false }
|
||||||
|
chacha20poly1305 = { version = "0.10.1", default-features = false, features = ["std"] }
|
||||||
getrandom = { version = "0.2.12", default-features = false }
|
getrandom = { version = "0.2.12", default-features = false }
|
||||||
thiserror = { version = "1.0.57", default-features = false }
|
thiserror = { version = "1.0.57", default-features = false }
|
||||||
|
|
|
@ -3,17 +3,22 @@ use base64ct::{Base64UrlUnpadded, Encoding};
|
||||||
const CANONICALIZATION_BUFFER_SIZE: usize = 1024;
|
const CANONICALIZATION_BUFFER_SIZE: usize = 1024;
|
||||||
const CANONICALIZATION_SEPARATOR: &str = ":";
|
const CANONICALIZATION_SEPARATOR: &str = ":";
|
||||||
|
|
||||||
pub(crate) fn canonicalize(key_context: &[&str]) -> String {
|
#[inline]
|
||||||
match key_context.len() {
|
pub(crate) fn join_canonicalized_str(s1: &str, s2: &str) -> String {
|
||||||
|
format!("{s1}{CANONICALIZATION_SEPARATOR}{s2}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn canonicalize(context: &[impl AsRef<[u8]>]) -> String {
|
||||||
|
match context.len() {
|
||||||
0 => String::new(),
|
0 => String::new(),
|
||||||
1 => key_context[0].to_string(),
|
1 => Base64UrlUnpadded::encode_string(context[0].as_ref()),
|
||||||
_ => {
|
_ => {
|
||||||
let mut ret = String::with_capacity(CANONICALIZATION_BUFFER_SIZE);
|
let mut ret = String::with_capacity(CANONICALIZATION_BUFFER_SIZE);
|
||||||
for (i, ctx_elem) in key_context.iter().enumerate() {
|
for (i, ctx_elem) in context.iter().enumerate() {
|
||||||
if i != 0 {
|
if i != 0 {
|
||||||
ret += CANONICALIZATION_SEPARATOR;
|
ret += CANONICALIZATION_SEPARATOR;
|
||||||
}
|
}
|
||||||
ret += &Base64UrlUnpadded::encode_string(ctx_elem.as_bytes());
|
ret += &Base64UrlUnpadded::encode_string(ctx_elem.as_ref());
|
||||||
}
|
}
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
@ -24,16 +29,18 @@ pub(crate) fn canonicalize(key_context: &[&str]) -> String {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
const EMPTY_CTX: &[[u8; 0]] = &[];
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn canonicalize_empty() {
|
fn canonicalize_empty() {
|
||||||
let canon = canonicalize(&[]);
|
let canon = canonicalize(EMPTY_CTX);
|
||||||
assert_eq!(canon, String::new());
|
assert_eq!(canon, String::new());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn canonicalize_one() {
|
fn canonicalize_one() {
|
||||||
let canon = canonicalize(&["test"]);
|
let canon = canonicalize(&["test"]);
|
||||||
assert_eq!(&canon, "test");
|
assert_eq!(&canon, "dGVzdA");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -41,4 +48,17 @@ mod tests {
|
||||||
let canon = canonicalize(&["test", "bis", "ter", ""]);
|
let canon = canonicalize(&["test", "bis", "ter", ""]);
|
||||||
assert_eq!(&canon, "dGVzdA:Ymlz:dGVy:");
|
assert_eq!(&canon, "dGVzdA:Ymlz:dGVy:");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_join_canonicalized_empty() {
|
||||||
|
assert_eq!(join_canonicalized_str("", ""), ":");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_join_canonicalized_with_data() {
|
||||||
|
assert_eq!(
|
||||||
|
join_canonicalized_str("QWO7RGDt:f-JmDPvU", "_Sfx61Fp"),
|
||||||
|
"QWO7RGDt:f-JmDPvU:_Sfx61Fp"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,15 @@
|
||||||
|
use crate::canonicalization::{canonicalize, join_canonicalized_str};
|
||||||
use crate::kdf::derive_key;
|
use crate::kdf::derive_key;
|
||||||
use crate::{Error, InputKeyMaterialList};
|
use crate::{storage, Error, InputKeyMaterialList};
|
||||||
|
use chacha20poly1305::aead::{Aead, KeyInit, Payload};
|
||||||
|
use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce};
|
||||||
|
|
||||||
|
pub(crate) type EncryptionFunction = dyn Fn(&[u8], &[u8], &str) -> Result<EncryptedData, Error>;
|
||||||
|
|
||||||
|
pub(crate) struct EncryptedData {
|
||||||
|
pub(crate) nonce: Vec<u8>,
|
||||||
|
pub(crate) ciphertext: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn encrypt(
|
pub fn encrypt(
|
||||||
ikml: &InputKeyMaterialList,
|
ikml: &InputKeyMaterialList,
|
||||||
|
@ -7,9 +17,51 @@ pub fn encrypt(
|
||||||
data: impl AsRef<[u8]>,
|
data: impl AsRef<[u8]>,
|
||||||
data_context: &[impl AsRef<[u8]>],
|
data_context: &[impl AsRef<[u8]>],
|
||||||
) -> Result<String, Error> {
|
) -> Result<String, Error> {
|
||||||
|
// Derive the key
|
||||||
let ikm = ikml.get_latest_ikm()?;
|
let ikm = ikml.get_latest_ikm()?;
|
||||||
let key = derive_key(ikm, key_context);
|
let key = derive_key(ikm, key_context);
|
||||||
unimplemented!("encrypt");
|
|
||||||
|
// Generate the AAD
|
||||||
|
let key_context_canon = canonicalize(key_context);
|
||||||
|
let data_context_canon = canonicalize(data_context);
|
||||||
|
let aad = join_canonicalized_str(&key_context_canon, &data_context_canon);
|
||||||
|
|
||||||
|
// Encrypt
|
||||||
|
let encryption_function = ikm.scheme.get_encryption();
|
||||||
|
let encrypted_data = encryption_function(&key, data.as_ref(), &aad)?;
|
||||||
|
|
||||||
|
// Encode
|
||||||
|
Ok(storage::encode(ikm.id, &encrypted_data))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn xchacha20poly1305_encrypt(
|
||||||
|
key: &[u8],
|
||||||
|
data: &[u8],
|
||||||
|
aad: &str,
|
||||||
|
) -> Result<EncryptedData, Error> {
|
||||||
|
// Adapt the key
|
||||||
|
let key = Key::from_slice(key);
|
||||||
|
|
||||||
|
// Generate a nonce
|
||||||
|
let mut nonce: [u8; 24] = [0; 24];
|
||||||
|
getrandom::getrandom(&mut nonce)?;
|
||||||
|
let nonce = XNonce::from_slice(&nonce);
|
||||||
|
|
||||||
|
// Prepare the payload
|
||||||
|
let payload = Payload {
|
||||||
|
msg: data,
|
||||||
|
aad: aad.as_bytes(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Encrypt the payload
|
||||||
|
let cipher = XChaCha20Poly1305::new(key);
|
||||||
|
let ciphertext = cipher.encrypt(nonce, payload)?;
|
||||||
|
|
||||||
|
// Return the result
|
||||||
|
Ok(EncryptedData {
|
||||||
|
nonce: nonce.to_vec(),
|
||||||
|
ciphertext,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decrypt(
|
pub fn decrypt(
|
||||||
|
@ -20,3 +72,44 @@ pub fn decrypt(
|
||||||
) -> Result<Vec<u8>, Error> {
|
) -> Result<Vec<u8>, Error> {
|
||||||
unimplemented!("decrypt");
|
unimplemented!("decrypt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const TEST_DATA: &[u8] = b"Lorem ipsum dolor sit amet.";
|
||||||
|
const TEST_KEY_CTX: &[&str] = &["db_name", "table_name", "column_name"];
|
||||||
|
const TEST_DATA_CTX: &[&str] = &["018db876-3d9d-79af-9460-55d17da991d8"];
|
||||||
|
const EMPTY_DATA_CTX: &[[u8; 0]] = &[];
|
||||||
|
|
||||||
|
fn get_ikm_lst() -> InputKeyMaterialList {
|
||||||
|
InputKeyMaterialList::import(
|
||||||
|
"AQAAAAEAAAABAAAANGFtbdYEN0s7dzCfMm7dYeQWD64GdmuKsYSiKwppAhmkz81lAAAAACQDr2cAAAAAAA",
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encrypt_decrypt_no_context() {
|
||||||
|
let lst = get_ikm_lst();
|
||||||
|
let res = encrypt(&lst, &[], TEST_DATA, EMPTY_DATA_CTX);
|
||||||
|
assert!(res.is_ok());
|
||||||
|
let ciphertext = res.unwrap();
|
||||||
|
assert!(ciphertext.starts_with("AQAAAA:"));
|
||||||
|
assert_eq!(ciphertext.len(), 98);
|
||||||
|
|
||||||
|
// TODO: decrypt
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encrypt_decrypt_with_context() {
|
||||||
|
let lst = get_ikm_lst();
|
||||||
|
let res = encrypt(&lst, TEST_KEY_CTX, TEST_DATA, TEST_DATA_CTX);
|
||||||
|
assert!(res.is_ok());
|
||||||
|
let ciphertext = res.unwrap();
|
||||||
|
assert!(ciphertext.starts_with("AQAAAA:"));
|
||||||
|
assert_eq!(ciphertext.len(), 98);
|
||||||
|
|
||||||
|
// TODO: decrypt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
#[error("cipher error: {0}")]
|
||||||
|
ChaCha20Poly1305Error(chacha20poly1305::Error),
|
||||||
#[error("ikm error: no input key material available")]
|
#[error("ikm error: no input key material available")]
|
||||||
IkmNoneAvailable,
|
IkmNoneAvailable,
|
||||||
#[error("ikm error: {0}: input key material not found")]
|
#[error("ikm error: {0}: input key material not found")]
|
||||||
|
@ -26,6 +28,12 @@ impl From<base64ct::Error> for Error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<chacha20poly1305::Error> for Error {
|
||||||
|
fn from(error: chacha20poly1305::Error) -> Self {
|
||||||
|
Error::ChaCha20Poly1305Error(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<getrandom::Error> for Error {
|
impl From<getrandom::Error> for Error {
|
||||||
fn from(error: getrandom::Error) -> Self {
|
fn from(error: getrandom::Error) -> Self {
|
||||||
Error::RandomSourceError(error)
|
Error::RandomSourceError(error)
|
||||||
|
|
|
@ -8,6 +8,8 @@ mod ikm;
|
||||||
mod kdf;
|
mod kdf;
|
||||||
#[cfg(any(feature = "encryption", feature = "ikm-management"))]
|
#[cfg(any(feature = "encryption", feature = "ikm-management"))]
|
||||||
mod scheme;
|
mod scheme;
|
||||||
|
#[cfg(feature = "encryption")]
|
||||||
|
mod storage;
|
||||||
|
|
||||||
#[cfg(feature = "encryption")]
|
#[cfg(feature = "encryption")]
|
||||||
pub use encryption::{decrypt, encrypt};
|
pub use encryption::{decrypt, encrypt};
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use crate::encryption::EncryptionFunction;
|
||||||
use crate::kdf::KdfFunction;
|
use crate::kdf::KdfFunction;
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
|
||||||
|
@ -12,6 +13,14 @@ impl Scheme {
|
||||||
Scheme::XChaCha20Poly1305WithBlake3 => Box::new(crate::kdf::blake3_derive),
|
Scheme::XChaCha20Poly1305WithBlake3 => Box::new(crate::kdf::blake3_derive),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn get_encryption(&self) -> Box<EncryptionFunction> {
|
||||||
|
match self {
|
||||||
|
Scheme::XChaCha20Poly1305WithBlake3 => {
|
||||||
|
Box::new(crate::encryption::xchacha20poly1305_encrypt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<u32> for Scheme {
|
impl TryFrom<u32> for Scheme {
|
||||||
|
|
42
src/storage.rs
Normal file
42
src/storage.rs
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
use crate::encryption::EncryptedData;
|
||||||
|
use crate::Error;
|
||||||
|
use base64ct::{Base64UrlUnpadded, Encoding};
|
||||||
|
|
||||||
|
const STORAGE_SEPARATOR: &str = ":";
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn encode_data(data: &[u8]) -> String {
|
||||||
|
Base64UrlUnpadded::encode_string(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn encode(ikm_id: u32, encrypted_data: &EncryptedData) -> String {
|
||||||
|
let mut ret = String::new();
|
||||||
|
ret += &encode_data(&ikm_id.to_le_bytes());
|
||||||
|
ret += STORAGE_SEPARATOR;
|
||||||
|
ret += &encode_data(&encrypted_data.nonce);
|
||||||
|
ret += STORAGE_SEPARATOR;
|
||||||
|
ret += &encode_data(&encrypted_data.ciphertext);
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::storage::EncryptedData;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn encode() {
|
||||||
|
let data = EncryptedData {
|
||||||
|
nonce: vec![
|
||||||
|
0x6b, 0x94, 0xa9, 0x8c, 0x0a, 0x2a, 0x86, 0xfb, 0x88, 0xf6, 0x7d, 0xc6, 0x3e, 0x10,
|
||||||
|
0xca, 0xba, 0x8b, 0x6a, 0xa0, 0xb6, 0xdf, 0xef, 0xf1, 0x5b,
|
||||||
|
],
|
||||||
|
ciphertext: vec![
|
||||||
|
0x4c, 0x8d, 0xb8, 0x5a, 0xbf, 0xe0, 0xf9, 0x95, 0x7b, 0xfd, 0x7d, 0x68, 0x1e, 0xa5,
|
||||||
|
0x4a, 0x6a, 0x4f, 0x62, 0x46, 0x54, 0x12, 0x9d, 0xe6, 0x15, 0x38, 0xc5, 0x81, 0xfb,
|
||||||
|
0x72, 0xe9, 0xfa, 0x11, 0x47, 0x29, 0xfc, 0x5f, 0x9d, 0x8f, 0xb3, 0x47, 0xf6, 0xcd,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
let s = super::encode(42, &data);
|
||||||
|
assert_eq!(&s, "KgAAAA:a5SpjAoqhvuI9n3GPhDKuotqoLbf7_Fb:TI24Wr_g-ZV7_X1oHqVKak9iRlQSneYVOMWB-3Lp-hFHKfxfnY-zR_bN");
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue