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
187 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.

status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1use base64::Engine;
2use base64::engine::general_purpose::{STANDARD, STANDARD_NO_PAD};
3use sha2::{Digest, Sha256};
4
5// Kept at the cap the Workers WebCrypto API imposes, so hashes made by
6// either implementation verify under the other.
7const PBKDF2_ITERATIONS: u32 = 100_000;
8
9pub fn sha256_hex(value: &str) -> String {
10 hex::encode(Sha256::digest(value.as_bytes()))
11}
12
13pub fn random_hex(bytes: usize) -> String {
14 let mut buffer = vec![0u8; bytes];
15 getrandom::getrandom(&mut buffer).expect("no source of randomness");
16 hex::encode(buffer)
17}
18
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)19/// A confirmation code: `digits` decimal digits, each uniformly random
20/// (bytes of 250 and up are drawn again, so no digit is likelier).
21pub fn random_digits(digits: usize) -> String {
22 let mut code = String::with_capacity(digits);
23 let mut byte = [0u8; 1];
24 while code.len() < digits {
25 getrandom::getrandom(&mut byte).expect("no source of randomness");
26 if byte[0] < 250 {
27 code.push(char::from(b'0' + byte[0] % 10));
28 }
29 }
30 code
31}
32
33/// What is kept of a confirmation code: an HMAC-SHA256 under `key` of the
34/// code, bound to the link it was sent with (`token_id`, the hash of the
35/// link's token, which names the user and the address). Six digits are few
36/// enough to try every one, so a key nobody reading the database has is
37/// what keeps the hash from giving the code away. `key` is IDENTITY_KEY;
38/// without one (a development setup) the hash is unkeyed.
39pub fn code_hash(key: &[u8], token_id: &str, code: &str) -> String {
40 use hmac::{Hmac, Mac};
41 let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(key).expect("HMAC takes any key length");
42 mac.update(b"g1t email confirmation code\0");
43 mac.update(token_id.as_bytes());
44 mac.update(b"\0");
45 mac.update(code.as_bytes());
46 hex::encode(mac.finalize().into_bytes())
47}
48
Merge invite emails that confirm the address: the emailed link carries a proof only that email has, so signing up from it needs no code; shared links and typed codes still confirm49/// The proof an invite email's link carries that whoever follows it reads
50/// that inbox: an HMAC-SHA256 under `key` (IDENTITY_KEY) of the invite's id
51/// and the address it is bound to, trimmed and lowercased. Only the email
52/// has it: the inviter sees the code, never this, and nobody can make one
53/// without the key.
54pub fn invite_proof(key: &[u8], invite_id: &str, email: &str) -> String {
55 use hmac::{Hmac, Mac};
56 let mut mac = <Hmac<Sha256> as Mac>::new_from_slice(key).expect("HMAC takes any key length");
57 mac.update(b"g1t invite email proof\0");
58 mac.update(invite_id.as_bytes());
59 mac.update(b"\0");
60 mac.update(email.trim().to_lowercase().as_bytes());
61 hex::encode(mac.finalize().into_bytes())
62}
63
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)64/// Whether two strings are equal, in time that depends on their length only.
65pub fn same(a: &str, b: &str) -> bool {
66 a.len() == b.len() && a.bytes().zip(b.bytes()).fold(0u8, |diff, (x, y)| diff | (x ^ y)) == 0
67}
68
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas69fn derive(password: &str, salt: &[u8], iterations: u32) -> [u8; 32] {
70 let mut hash = [0u8; 32];
71 pbkdf2::pbkdf2_hmac::<Sha256>(password.as_bytes(), salt, iterations, &mut hash);
72 hash
73}
74
75/// Format: `pbkdf2$<iterations>$<salt base64>$<hash base64>`.
76pub fn hash_password(password: &str) -> String {
77 let mut salt = [0u8; 16];
78 getrandom::getrandom(&mut salt).expect("no source of randomness");
79 let hash = derive(password, &salt, PBKDF2_ITERATIONS);
80 format!(
81 "pbkdf2${PBKDF2_ITERATIONS}${}${}",
82 STANDARD.encode(salt),
83 STANDARD.encode(hash)
84 )
85}
86
87pub fn verify_password(password: &str, stored: &str) -> bool {
88 let parts: Vec<&str> = stored.split('$').collect();
89 let [scheme, iterations, salt, hash] = parts[..] else {
90 return false;
91 };
92 let (Ok(iterations), Ok(salt), Ok(expected)) = (
93 iterations.parse::<u32>(),
94 STANDARD.decode(salt),
95 STANDARD.decode(hash),
96 ) else {
97 return false;
98 };
99 if scheme != "pbkdf2" || expected.len() != 32 {
100 return false;
101 }
102 let given = derive(password, &salt, iterations);
103 // Constant-time comparison.
104 given
105 .iter()
106 .zip(&expected)
107 .fold(0u8, |diff, (a, b)| diff | (a ^ b))
108 == 0
109}
110
111pub struct ParsedKey {
112 /// `<type> <base64 blob>`, without the comment.
113 pub public_key: String,
114 /// Matches `ssh-keygen -lf`: `SHA256:` then unpadded base64.
115 pub fingerprint: String,
116 pub comment: String,
117}
118
119/// Parses one line in OpenSSH public key format.
120pub fn parse_ssh_key(line: &str) -> Option<ParsedKey> {
121 let mut parts = line.split_whitespace();
122 let kind = parts.next()?;
123 let blob = parts.next()?;
124 let supported = matches!(
125 kind,
126 "ssh-ed25519"
127 | "ssh-rsa"
128 | "ecdsa-sha2-nistp256"
129 | "ecdsa-sha2-nistp384"
130 | "ecdsa-sha2-nistp521"
131 );
132 if !supported {
133 return None;
134 }
135 let bytes = STANDARD.decode(blob).ok()?;
136 // The blob starts with its own length-prefixed copy of the key type.
137 let length = u32::from_be_bytes(bytes.get(..4)?.try_into().ok()?) as usize;
138 if bytes.get(4..4 + length)? != kind.as_bytes() {
139 return None;
140 }
141 Some(ParsedKey {
142 public_key: format!("{kind} {blob}"),
143 fingerprint: format!("SHA256:{}", STANDARD_NO_PAD.encode(Sha256::digest(&bytes))),
144 comment: parts.collect::<Vec<_>>().join(" "),
145 })
146}
Merge email confirmation gate: a code and a link, nothing until confirmed (identity 0036)147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn confirmation_codes_are_six_digits_and_every_digit_turns_up() {
154 let mut seen = [0u32; 10];
155 for _ in 0..2000 {
156 let code = random_digits(6);
157 assert_eq!(code.len(), 6);
158 for digit in code.bytes() {
159 assert!(digit.is_ascii_digit());
160 seen[usize::from(digit - b'0')] += 1;
161 }
162 }
163 // 12,000 digits: each about 1,200 times.
164 assert!(seen.iter().all(|count| (900..1500).contains(count)), "{seen:?}");
165 }
166
167 #[test]
168 fn a_code_is_kept_as_a_keyed_hash_bound_to_its_link() {
169 let hash = code_hash(b"key", "link-a", "482913");
170 assert_eq!(hash.len(), 64);
171 assert!(!hash.contains("482913"));
172 assert_eq!(hash, code_hash(b"key", "link-a", "482913"));
173 assert_ne!(hash, code_hash(b"key", "link-b", "482913"));
174 assert_ne!(hash, code_hash(b"other", "link-a", "482913"));
175 assert_ne!(hash, code_hash(b"key", "link-a", "482914"));
176 // The separator keeps "link-a1" + "23456" apart from "link-a" + "123456".
177 assert_ne!(code_hash(b"key", "link-a1", "23456"), code_hash(b"key", "link-a", "123456"));
178 }
179
180 #[test]
181 fn same_compares_whole_strings() {
182 assert!(same("abc", "abc"));
183 assert!(!same("abc", "abd"));
184 assert!(!same("abc", "abcd"));
185 assert!(same("", ""));
186 }
187}

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