Skip to main content

alien_bindings/providers/key/
mod.rs

1use 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}