open_envault/crypto/
value.rs1use aes_gcm::aead::consts::U32;
10use aes_gcm::{
11 AesGcm,
12 aead::{Aead, KeyInit, Nonce, Payload},
13 aes::Aes256,
14};
15use anyhow::{Context, bail};
16use base64::Engine;
17use rand::RngCore;
18
19type Cipher = AesGcm<Aes256, U32>;
20
21const PREFIX: &str = "ENC[AES256_GCM,data:";
22
23fn random_bytes<const N: usize>() -> [u8; N] {
24 let mut buf = [0u8; N];
25 rand::rngs::OsRng.fill_bytes(&mut buf);
26 buf
27}
28
29fn b64(bytes: &[u8]) -> String {
30 base64::engine::general_purpose::STANDARD.encode(bytes)
31}
32
33pub fn encrypt(plain: &[u8], key: &[u8; 32], aad: &[u8]) -> anyhow::Result<String> {
37 encrypt_kind(plain, key, aad, "str")
38}
39
40pub fn encrypt_kind(
42 plain: &[u8],
43 key: &[u8; 32],
44 aad: &[u8],
45 kind: &str,
46) -> anyhow::Result<String> {
47 if plain.is_empty() {
48 return Ok(String::new());
49 }
50 let cipher =
51 Cipher::new_from_slice(key).map_err(|_| anyhow::anyhow!("invalid AES key length"))?;
52 let iv = random_bytes::<32>();
53 let nonce = Nonce::<Cipher>::from_slice(&iv);
54 let encrypted = cipher
55 .encrypt(nonce, Payload { msg: plain, aad })
56 .map_err(|_| anyhow::anyhow!("AES-GCM encryption failed"))?;
57 let split = encrypted
58 .len()
59 .checked_sub(16)
60 .context("ciphertext shorter than GCM tag")?;
61 let (data, tag) = encrypted.split_at(split);
62 Ok(format!(
63 "ENC[AES256_GCM,data:{},iv:{},tag:{},type:{kind}]",
64 b64(data),
65 b64(&iv),
66 b64(tag)
67 ))
68}
69
70struct Parsed {
71 data: Vec<u8>,
72 iv: Vec<u8>,
73 tag: Vec<u8>,
74}
75
76fn parse(enc: &str) -> anyhow::Result<Parsed> {
77 let rest = enc
78 .strip_prefix(PREFIX)
79 .with_context(|| "value is not in SOPS ENC format")?;
80 let data_b64 = rest
81 .split_once(",iv:")
82 .context("malformed ENC value: missing iv")?
83 .0;
84 let (iv_b64, tag_and_type) = rest
85 .split_once(",iv:")
86 .context("malformed ENC value: missing iv")?
87 .1
88 .split_once(",tag:")
89 .context("malformed ENC value: missing tag")?;
90 let (tag_b64, kind) = tag_and_type
91 .split_once(",type:")
92 .context("malformed ENC value: missing type")?;
93 let kind = kind
94 .strip_suffix(']')
95 .context("malformed ENC value: missing ]")?;
96 if !matches!(kind, "str" | "comment") {
97 bail!("unsupported SOPS value type: {kind}");
98 }
99 let engine = base64::engine::general_purpose::STANDARD;
100 Ok(Parsed {
101 data: engine.decode(data_b64).context("invalid data base64")?,
102 iv: engine.decode(iv_b64).context("invalid iv base64")?,
103 tag: engine.decode(tag_b64).context("invalid tag base64")?,
104 })
105}
106
107pub fn decrypt(enc: &str, key: &[u8; 32], aad: &[u8]) -> anyhow::Result<String> {
111 if enc.is_empty() {
112 return Ok(String::new());
113 }
114 let parsed = parse(enc)?;
115 if parsed.iv.len() != 32 {
116 bail!("unexpected IV length {} (expected 32)", parsed.iv.len());
117 }
118 let cipher =
119 Cipher::new_from_slice(key).map_err(|_| anyhow::anyhow!("invalid AES key length"))?;
120 let mut combined = Vec::with_capacity(parsed.data.len() + parsed.tag.len());
121 combined.extend_from_slice(&parsed.data);
122 combined.extend_from_slice(&parsed.tag);
123 let nonce = Nonce::<Cipher>::from_slice(&parsed.iv);
124 let plain = cipher
125 .decrypt(
126 nonce,
127 Payload {
128 msg: &combined,
129 aad,
130 },
131 )
132 .map_err(|_| anyhow::anyhow!("AES-GCM decryption failed (wrong key or corrupted value)"))?;
133 String::from_utf8(plain).map_err(|_| anyhow::anyhow!("decrypted value was not UTF-8"))
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn roundtrip() {
142 let key = [7u8; 32];
143 let plain = b"production";
144 let enc = encrypt(plain, &key, b"APP_ENV:").unwrap();
145 assert!(enc.starts_with(PREFIX));
146 assert!(enc.ends_with(",type:str]"));
147 assert_eq!(decrypt(&enc, &key, b"APP_ENV:").unwrap(), "production");
148 }
149
150 #[test]
151 fn empty_plaintext_stays_empty() {
152 let key = [1u8; 32];
153 assert_eq!(encrypt(b"", &key, b"EMPTY:").unwrap(), "");
154 assert_eq!(decrypt("", &key, b"EMPTY:").unwrap(), "");
155 }
156
157 #[test]
158 fn wrong_aad_fails() {
159 let key = [2u8; 32];
160 let enc = encrypt(b"secret", &key, b"A:").unwrap();
161 assert!(decrypt(&enc, &key, b"B:").is_err());
162 }
163
164 #[test]
165 fn wrong_key_fails() {
166 let enc = encrypt(b"secret", &[3u8; 32], b"A:").unwrap();
167 assert!(decrypt(&enc, &[4u8; 32], b"A:").is_err());
168 }
169}