hyperion_vault_core/
crypto.rs1use chacha20poly1305::aead::{Aead, KeyInit, Payload};
2use chacha20poly1305::{Key, XChaCha20Poly1305, XNonce};
3use zeroize::Zeroizing;
4
5use crate::error::{Error, Result};
6
7pub const DEK_LEN: usize = 32;
8pub const NONCE_LEN: usize = 24;
9
10pub type Dek = Zeroizing<[u8; DEK_LEN]>;
11
12pub fn fill_random(buf: &mut [u8]) {
13 getrandom::fill(buf).expect("operating system CSPRNG is unavailable");
14}
15
16pub fn generate_dek() -> Dek {
17 let mut k = [0u8; DEK_LEN];
18 fill_random(&mut k);
19 Zeroizing::new(k)
20}
21
22pub fn generate_nonce() -> [u8; NONCE_LEN] {
23 let mut n = [0u8; NONCE_LEN];
24 fill_random(&mut n);
25 n
26}
27
28pub fn dek_from_slice(bytes: &[u8]) -> Result<Dek> {
29 if bytes.len() != DEK_LEN {
30 return Err(Error::KeyLength {
31 expected: DEK_LEN,
32 got: bytes.len(),
33 });
34 }
35 let mut k = [0u8; DEK_LEN];
36 k.copy_from_slice(bytes);
37 Ok(Zeroizing::new(k))
38}
39
40pub fn seal(
41 dek: &[u8; DEK_LEN],
42 nonce: &[u8; NONCE_LEN],
43 aad: &[u8],
44 plaintext: &[u8],
45) -> Result<Vec<u8>> {
46 let cipher = XChaCha20Poly1305::new(Key::from_slice(&dek[..]));
47 cipher
48 .encrypt(
49 XNonce::from_slice(&nonce[..]),
50 Payload {
51 msg: plaintext,
52 aad,
53 },
54 )
55 .map_err(|_| Error::Encryption)
56}
57
58pub fn open(
59 dek: &[u8; DEK_LEN],
60 nonce: &[u8; NONCE_LEN],
61 aad: &[u8],
62 ciphertext: &[u8],
63) -> Result<Vec<u8>> {
64 let cipher = XChaCha20Poly1305::new(Key::from_slice(&dek[..]));
65 cipher
66 .decrypt(
67 XNonce::from_slice(&nonce[..]),
68 Payload {
69 msg: ciphertext,
70 aad,
71 },
72 )
73 .map_err(|_| Error::Decryption)
74}
75
76pub struct DataKey {
77 pub plaintext: Dek,
78 pub wrapped: Vec<u8>,
79 pub key_id: String,
80}
81
82pub trait KeyWrapper {
83 fn generate_data_key(&self) -> Result<DataKey>;
84 fn unwrap_data_key(&self, wrapped: &[u8], key_id: &str) -> Result<Dek>;
85}
86
87#[derive(Clone)]
88pub struct Envelope {
89 pub key_id: String,
90 pub wrapped_dek: Vec<u8>,
91 pub nonce: [u8; NONCE_LEN],
92 pub ciphertext: Vec<u8>,
93}
94
95pub fn seal_envelope<W: KeyWrapper>(wrapper: &W, aad: &[u8], plaintext: &[u8]) -> Result<Envelope> {
96 let dk = wrapper.generate_data_key()?;
97 let nonce = generate_nonce();
98 let ciphertext = seal(&dk.plaintext, &nonce, aad, plaintext)?;
99 Ok(Envelope {
100 key_id: dk.key_id,
101 wrapped_dek: dk.wrapped,
102 nonce,
103 ciphertext,
104 })
105}
106
107pub fn open_envelope<W: KeyWrapper>(wrapper: &W, env: &Envelope, aad: &[u8]) -> Result<Vec<u8>> {
108 let dek = wrapper.unwrap_data_key(&env.wrapped_dek, &env.key_id)?;
109 open(&dek, &env.nonce, aad, &env.ciphertext)
110}
111
112const WRAP_AAD: &[u8] = b"pg_vault:dek-wrap:v1";
113
114pub struct LocalKeyWrapper {
115 master: Zeroizing<[u8; DEK_LEN]>,
116 key_id: String,
117}
118
119impl LocalKeyWrapper {
120 pub fn new(master: [u8; DEK_LEN], key_id: impl Into<String>) -> Self {
121 Self {
122 master: Zeroizing::new(master),
123 key_id: key_id.into(),
124 }
125 }
126
127 pub fn random() -> Self {
128 let mut master = [0u8; DEK_LEN];
129 fill_random(&mut master);
130 Self::new(master, "local-dev")
131 }
132
133 pub fn from_base64(encoded: &str, key_id: impl Into<String>) -> Result<Self> {
134 use base64::Engine;
135 let raw = base64::engine::general_purpose::STANDARD
136 .decode(encoded.trim())
137 .map_err(|_| Error::KeyUnwrap)?;
138 if raw.len() != DEK_LEN {
139 return Err(Error::KeyLength {
140 expected: DEK_LEN,
141 got: raw.len(),
142 });
143 }
144 let mut master = [0u8; DEK_LEN];
145 master.copy_from_slice(&raw);
146 Ok(Self::new(master, key_id))
147 }
148}
149
150impl KeyWrapper for LocalKeyWrapper {
151 fn generate_data_key(&self) -> Result<DataKey> {
152 let dek = generate_dek();
153 let nonce = generate_nonce();
154 let ct = seal(&self.master, &nonce, WRAP_AAD, &dek[..]).map_err(|_| Error::KeyWrap)?;
155 let mut wrapped = Vec::with_capacity(NONCE_LEN + ct.len());
156 wrapped.extend_from_slice(&nonce);
157 wrapped.extend_from_slice(&ct);
158 Ok(DataKey {
159 plaintext: dek,
160 wrapped,
161 key_id: self.key_id.clone(),
162 })
163 }
164
165 fn unwrap_data_key(&self, wrapped: &[u8], _key_id: &str) -> Result<Dek> {
166 if wrapped.len() < NONCE_LEN {
167 return Err(Error::KeyUnwrap);
168 }
169 let (nonce, ct) = wrapped.split_at(NONCE_LEN);
170 let nonce: &[u8; NONCE_LEN] = nonce.try_into().map_err(|_| Error::KeyUnwrap)?;
171 let pt = open(&self.master, nonce, WRAP_AAD, ct).map_err(|_| Error::KeyUnwrap)?;
172 dek_from_slice(&pt)
173 }
174}