git_xcrypt/crypto/
cipher.rs1use aes_siv::KeyInit;
9use aes_siv::siv::Aes256Siv;
10
11use crate::crypto::format::{Header, KEY_ID_LEN, SUITE_AES_256_SIV};
12use crate::crypto::key::{MasterKey, SuiteKey};
13use crate::{Error, Result};
14
15fn cipher_key(key: &SuiteKey) -> Result<&aes_siv::Key<Aes256Siv>> {
24 key.expose_bytes()
25 .as_slice()
26 .try_into()
27 .map_err(|_| Error::Crypto("the suite key does not fit the cipher".into()))
28}
29
30pub fn encrypt(key: &MasterKey, flags: u8, plaintext: &[u8]) -> Result<Vec<u8>> {
41 let header = Header::new(flags, key.key_id()).to_bytes();
42 let suite_key = key.suite_key(SUITE_AES_256_SIV)?;
43
44 let mut cipher = Aes256Siv::new(cipher_key(&suite_key)?);
45 let sealed = cipher
46 .encrypt([header.as_slice()], plaintext)
47 .map_err(|_| Error::Crypto("encryption failed".into()))?;
48
49 let mut blob = Vec::with_capacity(header.len() + sealed.len());
50 blob.extend_from_slice(&header);
51 blob.extend_from_slice(&sealed);
52 Ok(blob)
53}
54
55pub fn decrypt(key: &MasterKey, blob: &[u8]) -> Result<(u8, Vec<u8>)> {
65 let header = Header::parse(blob)?;
66 let our_key_id = key.key_id();
67 if header.key_id != our_key_id {
68 return Err(Error::KeyMismatch {
69 wanted: header.key_id,
70 have: our_key_id,
71 });
72 }
73
74 let suite_key = key.suite_key(header.suite)?;
75 let (header_bytes, body) = blob.split_at(crate::crypto::format::HEADER_LEN);
78
79 let mut cipher = Aes256Siv::new(cipher_key(&suite_key)?);
80 let plaintext = cipher
81 .decrypt([header_bytes], body)
82 .map_err(|_| Error::Crypto("authentication failed; the file has been altered".into()))?;
83
84 Ok((header.flags, plaintext))
85}
86
87pub fn blob_key_id(blob: &[u8]) -> Result<[u8; KEY_ID_LEN]> {
96 Ok(Header::parse(blob)?.key_id)
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102 use crate::crypto::format::{FLAG_LF_NORMALIZED, OVERHEAD};
103 use crate::crypto::key::MASTER_KEY_LEN;
104
105 fn key() -> MasterKey {
106 MasterKey::from_bytes([42u8; MASTER_KEY_LEN])
107 }
108
109 fn samples() -> Vec<Vec<u8>> {
110 vec![
111 Vec::new(),
112 b"x".to_vec(),
113 b"api_key = do-not-commit-me\n".to_vec(),
114 (0u8..=255).cycle().take(4096).collect(),
115 ]
116 }
117
118 #[test]
119 fn round_trips_every_shape_of_input() {
120 for plaintext in samples() {
121 let blob = encrypt(&key(), 0, &plaintext).expect("encryption must succeed");
122 let (flags, recovered) = decrypt(&key(), &blob).expect("decryption must succeed");
123 assert_eq!(flags, 0);
124 assert_eq!(recovered, plaintext);
125 }
126 }
127
128 #[test]
129 fn encryption_is_deterministic() {
130 for plaintext in samples() {
131 let first = encrypt(&key(), 0, &plaintext).expect("encryption must succeed");
132 let second = encrypt(&key(), 0, &plaintext).expect("encryption must succeed");
133 assert_eq!(first, second, "the same plaintext must give the same bytes");
134 }
135 }
136
137 proptest::proptest! {
138 #![proptest_config(proptest::prelude::ProptestConfig::with_cases(256))]
144
145 #[test]
146 fn decrypting_what_we_encrypted_gives_the_plaintext_back(
147 plaintext in proptest::collection::vec(proptest::num::u8::ANY, 0..8192),
148 flags in proptest::prelude::prop_oneof![
149 proptest::prelude::Just(0u8),
150 proptest::prelude::Just(FLAG_LF_NORMALIZED),
151 ],
152 ) {
153 let blob = encrypt(&key(), flags, &plaintext).expect("encryption must succeed");
154 let (recovered_flags, recovered) =
155 decrypt(&key(), &blob).expect("decryption must succeed");
156 proptest::prop_assert_eq!(recovered_flags, flags);
157 proptest::prop_assert_eq!(&recovered, &plaintext);
158 proptest::prop_assert_eq!(blob.len(), plaintext.len() + OVERHEAD);
160 }
161
162 #[test]
163 fn encrypting_the_same_bytes_twice_gives_the_same_blob(
164 plaintext in proptest::collection::vec(proptest::num::u8::ANY, 0..8192),
165 ) {
166 let first = encrypt(&key(), 0, &plaintext).expect("encryption must succeed");
167 let second = encrypt(&key(), 0, &plaintext).expect("encryption must succeed");
168 proptest::prop_assert_eq!(first, second);
169 }
170 }
171
172 #[test]
181 fn a_flipped_flags_byte_fails_the_tag_instead_of_changing_the_conversion() {
182 let blob = encrypt(&key(), 0, b"one\ntwo\n").expect("encryption must succeed");
183 let mut flipped = blob.clone();
184 flipped[13] ^= FLAG_LF_NORMALIZED;
185 assert!(
186 crate::crypto::format::Header::parse(&flipped).is_ok(),
187 "the flipped byte must still parse, or this test asks nothing of \
188 the tag"
189 );
190 assert!(
191 matches!(decrypt(&key(), &flipped), Err(crate::Error::Crypto(_))),
192 "a header byte was altered and the tag did not notice"
193 );
194 }
195
196 #[test]
203 fn the_crate_matches_rfc_5297_appendix_a1() {
204 use aes_siv::siv::Aes128Siv;
205
206 let key = hex_bytes("fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff");
207 let associated_data = hex_bytes("101112131415161718191a1b1c1d1e1f2021222324252627");
208 let plaintext = hex_bytes("112233445566778899aabbccddee");
209 let expected = hex_bytes("85632d07c6e8f37f950acd320a2ecc9340c02b9690c4dc04daef7f6afe5c");
210
211 let mut cipher = Aes128Siv::new(key.as_slice().try_into().expect("a 32-byte RFC key"));
212 let sealed = cipher
213 .encrypt([associated_data.as_slice()], plaintext.as_slice())
214 .expect("the RFC vector must encrypt");
215 assert_eq!(sealed, expected, "aes-siv diverged from RFC 5297");
216
217 let mut cipher = Aes128Siv::new(key.as_slice().try_into().expect("a 32-byte RFC key"));
218 let recovered = cipher
219 .decrypt([associated_data.as_slice()], sealed.as_slice())
220 .expect("the RFC vector must decrypt");
221 assert_eq!(recovered, plaintext);
222 }
223
224 fn hex_bytes(text: &str) -> Vec<u8> {
226 assert!(text.len().is_multiple_of(2), "hex needs an even length");
227 (0..text.len())
228 .step_by(2)
229 .map(|index| {
230 u8::from_str_radix(&text[index..index + 2], 16).expect("test vectors must be hex")
231 })
232 .collect()
233 }
234}