alien_bindings/providers/key/
mod.rs1use crate::error::{ErrorData, Result};
2use alien_error::AlienError;
3use sha2::{Digest, Sha256};
4use std::collections::BTreeMap;
5
6#[cfg(feature = "aws")]
7pub mod aws;
8#[cfg(feature = "azure")]
9pub mod azure;
10#[cfg(feature = "gcp")]
11pub mod gcp;
12
13const MAGIC: &[u8; 4] = b"AKBF";
14const VERSION: u8 = 1;
15const HEADER_LENGTH: usize = 39;
16const MAX_PLAINTEXT_LENGTH: usize = 128;
17const MAX_CONTEXT_LENGTH: usize = 16 * 1024;
18
19pub(crate) fn encode_context(context: Option<&BTreeMap<String, String>>) -> Result<Vec<u8>> {
20 let empty_context = BTreeMap::new();
21 let context = context.unwrap_or(&empty_context);
22 let mut encoded = Vec::new();
23 encoded.extend_from_slice(&(context.len() as u32).to_be_bytes());
24 for (key, value) in context {
25 let key = key.as_bytes();
26 let value = value.as_bytes();
27 encoded.extend_from_slice(&(key.len() as u32).to_be_bytes());
28 encoded.extend_from_slice(key);
29 encoded.extend_from_slice(&(value.len() as u32).to_be_bytes());
30 encoded.extend_from_slice(value);
31 if encoded.len() > MAX_CONTEXT_LENGTH {
32 return Err(AlienError::new(ErrorData::KeyInputInvalid {
33 reason: "canonical context exceeds 16 KiB".to_string(),
34 }));
35 }
36 }
37 Ok(encoded)
38}
39
40pub(crate) fn frame(plaintext: &[u8], canonical_context: &[u8]) -> Result<Vec<u8>> {
41 if plaintext.len() > MAX_PLAINTEXT_LENGTH {
42 return Err(AlienError::new(ErrorData::KeyInputInvalid {
43 reason: "plaintext exceeds the portable 128-byte limit".to_string(),
44 }));
45 }
46 let mut frame = Vec::with_capacity(HEADER_LENGTH + plaintext.len());
47 frame.extend_from_slice(MAGIC);
48 frame.push(VERSION);
49 frame.extend_from_slice(&(plaintext.len() as u16).to_be_bytes());
50 frame.extend_from_slice(&Sha256::digest(canonical_context));
51 frame.extend_from_slice(plaintext);
52 Ok(frame)
53}
54
55pub(crate) fn unframe(frame: &[u8], canonical_context: &[u8]) -> Result<Vec<u8>> {
56 if frame.len() < HEADER_LENGTH || &frame[..4] != MAGIC || frame[4] != VERSION {
57 return Err(AlienError::new(ErrorData::KeyCiphertextInvalid {
58 reason: "decrypted data is not an Alien Key frame".to_string(),
59 }));
60 }
61 let plaintext_length = u16::from_be_bytes([frame[5], frame[6]]) as usize;
62 if plaintext_length > MAX_PLAINTEXT_LENGTH || frame.len() != HEADER_LENGTH + plaintext_length {
63 return Err(AlienError::new(ErrorData::KeyCiphertextInvalid {
64 reason: "decrypted frame has an invalid plaintext length".to_string(),
65 }));
66 }
67 let expected_context_hash = Sha256::digest(canonical_context);
68 if frame[7..39] != expected_context_hash[..] {
69 return Err(AlienError::new(ErrorData::KeyCiphertextInvalid {
70 reason: "context does not match the encrypted value".to_string(),
71 }));
72 }
73 Ok(frame[HEADER_LENGTH..].to_vec())
74}
75
76#[cfg(test)]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn frame_enforces_limits_and_context() {
82 let context = BTreeMap::from([
83 ("project".to_string(), "example".to_string()),
84 ("purpose".to_string(), "root".to_string()),
85 ]);
86 let canonical = encode_context(Some(&context)).unwrap();
87 let framed = frame(&[7; 128], &canonical).unwrap();
88 assert_eq!(unframe(&framed, &canonical).unwrap(), vec![7; 128]);
89 assert!(frame(&[0; 129], &canonical).is_err());
90 assert!(unframe(&framed, &encode_context(None).unwrap()).is_err());
91 }
92
93 #[test]
94 fn canonical_context_and_frame_match_the_published_vector() {
95 let context = BTreeMap::from([
96 ("purpose".to_string(), "root".to_string()),
97 ("project".to_string(), "example".to_string()),
98 ]);
99 let canonical = encode_context(Some(&context)).unwrap();
100 assert_eq!(
101 hex::encode(&canonical),
102 "000000020000000770726f6a656374000000076578616d706c6500000007707572706f736500000004726f6f74"
103 );
104 assert_eq!(
105 hex::encode(frame(b"hello", &canonical).unwrap()),
106 "414b4246010005faf89ea5b4228220f52708c3fc60f67ede69c57333979676767bf5715253e4a768656c6c6f"
107 );
108 }
109}