Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
195 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Money in billing's messages reads to the cent, and the deploy reads migrations again after a failure1//! Secrets at rest, and the signatures put on what crosses between g1t
2//! and outside systems.
3//!
4//! A secret is sealed with AES-256-GCM under its service's own key, with the
5//! id of the row it belongs to as associated data, so a sealed value copied
6//! onto another row does not open.
7
8use aes_gcm::aead::{Aead, KeyInit, Payload};
9use aes_gcm::{Aes256Gcm, Nonce};
10use base64::Engine;
11use base64::engine::general_purpose::STANDARD;
12use hmac::{Hmac, Mac};
13use sha2::{Digest, Sha256};
14
15const VERSION: &str = "v1:";
16
17pub struct Sealer {
18 cipher: Aes256Gcm,
19}
20
21impl Sealer {
22 /// From the service's key: 64 hex characters.
23 pub fn new(key_hex: &str) -> Option<Sealer> {
24 let key = hex::decode(key_hex.trim()).ok()?;
25 (key.len() == 32).then(|| Sealer {
26 cipher: Aes256Gcm::new_from_slice(&key).expect("a 32-byte key"),
27 })
28 }
29
30 pub fn seal(&self, plaintext: &str, bound_to: &str) -> String {
31 let mut nonce = [0u8; 12];
32 getrandom::getrandom(&mut nonce).expect("no source of randomness");
33 let sealed = self
34 .cipher
35 .encrypt(
36 Nonce::from_slice(&nonce),
37 Payload {
38 msg: plaintext.as_bytes(),
39 aad: bound_to.as_bytes(),
40 },
41 )
42 .expect("encrypting cannot fail");
43 let mut out = nonce.to_vec();
44 out.extend(sealed);
45 format!("{VERSION}{}", STANDARD.encode(out))
46 }
47
48 /// Bytes sealed the same way, kept as bytes: the version, the nonce,
49 /// then the ciphertext. For values stored as bytes, such as a cache's.
50 pub fn seal_bytes(&self, plaintext: &[u8], bound_to: &str) -> Vec<u8> {
51 let mut nonce = [0u8; 12];
52 getrandom::getrandom(&mut nonce).expect("no source of randomness");
53 let sealed = self
54 .cipher
55 .encrypt(
56 Nonce::from_slice(&nonce),
57 Payload {
58 msg: plaintext,
59 aad: bound_to.as_bytes(),
60 },
61 )
62 .expect("encrypting cannot fail");
63 let mut out = VERSION.as_bytes().to_vec();
64 out.extend(nonce);
65 out.extend(sealed);
66 out
67 }
68
69 /// What [`Sealer::seal_bytes`] sealed; `None` under another key, for
70 /// another row, or for anything else.
71 pub fn open_bytes(&self, sealed: &[u8], bound_to: &str) -> Option<Vec<u8>> {
72 let bytes = sealed.strip_prefix(VERSION.as_bytes())?;
73 if bytes.len() < 12 {
74 return None;
75 }
76 let (nonce, ciphertext) = bytes.split_at(12);
77 self.cipher
78 .decrypt(
79 Nonce::from_slice(nonce),
80 Payload {
81 msg: ciphertext,
82 aad: bound_to.as_bytes(),
83 },
84 )
85 .ok()
86 }
87
88 /// `None` when it was sealed under another key or for another row.
89 pub fn open(&self, sealed: &str, bound_to: &str) -> Option<String> {
90 let bytes = STANDARD.decode(sealed.strip_prefix(VERSION)?).ok()?;
91 if bytes.len() < 12 {
92 return None;
93 }
94 let (nonce, ciphertext) = bytes.split_at(12);
95 let plain = self
96 .cipher
97 .decrypt(
98 Nonce::from_slice(nonce),
99 Payload {
100 msg: ciphertext,
101 aad: bound_to.as_bytes(),
102 },
103 )
104 .ok()?;
105 String::from_utf8(plain).ok()
106 }
107}
108
109pub fn sha256_hex(value: &str) -> String {
110 sha256_hex_bytes(value.as_bytes())
111}
112
113pub fn sha256_hex_bytes(value: &[u8]) -> String {
114 hex::encode(Sha256::digest(value))
115}
116
117pub fn random_hex(bytes: usize) -> String {
118 let mut buffer = vec![0u8; bytes];
119 getrandom::getrandom(&mut buffer).expect("no source of randomness");
120 hex::encode(buffer)
121}
122
123pub fn hmac_sha256_hex(secret: &str, body: &str) -> String {
124 let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(secret.as_bytes()).expect("any key length");
125 mac.update(body.as_bytes());
126 hex::encode(mac.finalize().into_bytes())
127}
128
129/// Compares in time that does not depend on where they differ.
130pub fn same(a: &str, b: &str) -> bool {
131 a.len() == b.len() && a.bytes().zip(b.bytes()).fold(0u8, |diff, (x, y)| diff | (x ^ y)) == 0
132}
133
134/// Whether `signature` is `body` signed with `secret`: hex HMAC-SHA256,
135/// optionally written `sha256=<hex>`.
136pub fn signed(secret: &str, body: &str, signature: &str) -> bool {
137 let given = signature.trim();
138 let given = given.strip_prefix("sha256=").unwrap_or(given);
139 same(&hmac_sha256_hex(secret, body), &given.to_ascii_lowercase())
140}
141
142/// The last four characters, to tell keys apart without showing them.
143pub fn hint(secret: &str) -> String {
144 let tail: String = secret.chars().rev().take(4).collect::<Vec<_>>().into_iter().rev().collect();
145 format!("…{tail}")
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 const KEY: &str = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
153
154 #[test]
155 fn a_sealed_secret_opens_only_for_its_own_row() {
156 let sealer = Sealer::new(KEY).unwrap();
157 let sealed = sealer.seal("sk-ant-secret", "con_1");
158 assert!(!sealed.contains("sk-ant"));
159 assert_eq!(sealer.open(&sealed, "con_1").as_deref(), Some("sk-ant-secret"));
160 assert_eq!(sealer.open(&sealed, "con_2"), None);
161 }
162
163 #[test]
164 fn sealed_bytes_open_only_for_their_own_key() {
165 let sealer = Sealer::new(KEY).unwrap();
166 let plain = b"0032HEAD\0symref=HEAD:refs/heads/main\n";
167 let sealed = sealer.seal_bytes(plain, "refs:rep_1:abc");
168 assert!(!sealed.windows(4).any(|window| window == b"HEAD"));
169 assert_eq!(sealer.open_bytes(&sealed, "refs:rep_1:abc").as_deref(), Some(&plain[..]));
170 assert_eq!(sealer.open_bytes(&sealed, "refs:rep_2:abc"), None);
171 let other = Sealer::new(&"ff".repeat(32)).unwrap();
172 assert_eq!(other.open_bytes(&sealed, "refs:rep_1:abc"), None);
173 assert_eq!(sealer.open_bytes(b"v1:short", "refs:rep_1:abc"), None);
174 assert_eq!(sealer.open_bytes(plain, "refs:rep_1:abc"), None);
175 }
176
177 #[test]
178 fn a_key_of_the_wrong_length_is_refused() {
179 assert!(Sealer::new("abcd").is_none());
180 }
181
182 #[test]
183 fn signatures_are_checked_in_either_form() {
184 let signature = hmac_sha256_hex("shh", "{\"a\":1}");
185 assert!(signed("shh", "{\"a\":1}", &signature));
186 assert!(signed("shh", "{\"a\":1}", &format!("sha256={signature}")));
187 assert!(!signed("shh", "{\"a\":2}", &signature));
188 assert!(!signed("other", "{\"a\":1}", &signature));
189 }
190
191 #[test]
192 fn a_hint_shows_only_the_end() {
193 assert_eq!(hint("sk-ant-api03-abcdef"), "…cdef");
194 }
195}

This file's history is long; its oldest lines are credited to the oldest commit read.