note_to_self_lib/
crypto.rs1use aes_gcm::aead::{Aead, KeyInit};
2use aes_gcm::{Aes256Gcm, Nonce};
3use anyhow::{anyhow, Context, Result};
4use argon2::{Algorithm, Argon2, Params, Version};
5use base64::engine::general_purpose::URL_SAFE_NO_PAD;
6use base64::Engine;
7use hkdf::Hkdf;
8use rand::{rngs::OsRng, RngCore};
9use sha2::{Digest, Sha256};
10use zeroize::Zeroize;
11
12const ARGON2_MEMORY_KIB: u32 = 16 * 1024;
13const ARGON2_ITERATIONS: u32 = 1;
14const ARGON2_PARALLELISM: u32 = 4;
15const KEY_LEN: usize = 32;
16const NONCE_LEN: usize = 12;
17
18#[derive(Clone, Zeroize)]
19#[zeroize(drop)]
20pub struct DerivedKeys {
21 pub encryption_key: [u8; KEY_LEN],
22 pub auth_key: [u8; KEY_LEN],
23}
24
25pub fn derive_keys(username: &str, password: &str) -> Result<DerivedKeys> {
26 let username = username.trim().to_ascii_lowercase();
27 if username.is_empty() {
28 return Err(anyhow!("username cannot be empty"));
29 }
30 if password.is_empty() {
31 return Err(anyhow!("password cannot be empty"));
32 }
33
34 let salt = Sha256::digest(username.as_bytes());
35 let params = Params::new(
36 ARGON2_MEMORY_KIB,
37 ARGON2_ITERATIONS,
38 ARGON2_PARALLELISM,
39 Some(KEY_LEN),
40 )
41 .map_err(|err| anyhow!("invalid Argon2 parameters: {err}"))?;
42 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
43 let mut master = [0u8; KEY_LEN];
44 argon2
45 .hash_password_into(password.as_bytes(), &salt, &mut master)
46 .map_err(|err| anyhow!("key derivation failed: {err}"))?;
47
48 let hkdf = Hkdf::<Sha256>::new(None, &master);
49 let mut encryption_key = [0u8; KEY_LEN];
50 let mut auth_key = [0u8; KEY_LEN];
51 hkdf.expand(b"note-to-self-encryption", &mut encryption_key)
52 .map_err(|_| anyhow!("failed to derive encryption key"))?;
53 hkdf.expand(b"note-to-self-auth", &mut auth_key)
54 .map_err(|_| anyhow!("failed to derive auth key"))?;
55 master.zeroize();
56
57 Ok(DerivedKeys {
58 encryption_key,
59 auth_key,
60 })
61}
62
63pub fn derive_journal_key(username: &str, journal: &str, password: &str) -> Result<[u8; KEY_LEN]> {
64 let username = username.trim().to_ascii_lowercase();
65 if username.is_empty() {
66 return Err(anyhow!("username cannot be empty"));
67 }
68 if journal.is_empty() {
69 return Err(anyhow!("journal name cannot be empty"));
70 }
71 if password.is_empty() {
72 return Err(anyhow!("journal password cannot be empty"));
73 }
74
75 let salt = Sha256::digest(format!("note-to-self-locked-journal:{username}:{journal}"));
76 let params = Params::new(
77 ARGON2_MEMORY_KIB,
78 ARGON2_ITERATIONS,
79 ARGON2_PARALLELISM,
80 Some(KEY_LEN),
81 )
82 .map_err(|err| anyhow!("invalid Argon2 parameters: {err}"))?;
83 let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
84 let mut master = [0u8; KEY_LEN];
85 argon2
86 .hash_password_into(password.as_bytes(), &salt, &mut master)
87 .map_err(|err| anyhow!("journal key derivation failed: {err}"))?;
88
89 let hkdf = Hkdf::<Sha256>::new(None, &master);
90 let mut journal_key = [0u8; KEY_LEN];
91 hkdf.expand(b"note-to-self-journal-lock", &mut journal_key)
92 .map_err(|_| anyhow!("failed to derive journal key"))?;
93 master.zeroize();
94
95 Ok(journal_key)
96}
97
98pub fn auth_token(auth_key: &[u8; KEY_LEN]) -> String {
99 URL_SAFE_NO_PAD.encode(auth_key)
100}
101
102pub fn encode_bytes(bytes: &[u8]) -> String {
103 URL_SAFE_NO_PAD.encode(bytes)
104}
105
106pub fn decode_bytes(encoded: &str) -> Result<Vec<u8>> {
107 URL_SAFE_NO_PAD
108 .decode(encoded)
109 .context("invalid base64-encoded bytes")
110}
111
112pub fn seal(encryption_key: &[u8; KEY_LEN], plaintext: &[u8]) -> Result<Vec<u8>> {
113 let cipher = Aes256Gcm::new_from_slice(encryption_key).context("invalid encryption key")?;
114 let mut nonce = [0u8; NONCE_LEN];
115 OsRng.fill_bytes(&mut nonce);
116 let ciphertext = cipher
117 .encrypt(Nonce::from_slice(&nonce), plaintext)
118 .map_err(|_| anyhow!("encryption failed"))?;
119
120 let mut sealed = Vec::with_capacity(NONCE_LEN + ciphertext.len());
121 sealed.extend_from_slice(&nonce);
122 sealed.extend_from_slice(&ciphertext);
123 Ok(sealed)
124}
125
126pub fn open(encryption_key: &[u8; KEY_LEN], sealed: &[u8]) -> Result<Vec<u8>> {
127 if sealed.len() < NONCE_LEN {
128 return Err(anyhow!("sealed blob is too short"));
129 }
130 let (nonce, ciphertext) = sealed.split_at(NONCE_LEN);
131 let cipher = Aes256Gcm::new_from_slice(encryption_key).context("invalid encryption key")?;
132 cipher
133 .decrypt(Nonce::from_slice(nonce), ciphertext)
134 .map_err(|_| anyhow!("decryption failed"))
135}
136
137pub fn checksum(bytes: &[u8]) -> String {
138 blake3::hash(bytes).to_hex().to_string()
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn derivation_is_deterministic() {
147 let a = derive_keys("User@example.com", "correct horse").unwrap();
148 let b = derive_keys("user@example.com", "correct horse").unwrap();
149 assert_eq!(a.encryption_key, b.encryption_key);
150 assert_eq!(a.auth_key, b.auth_key);
151 }
152
153 #[test]
154 fn journal_key_depends_on_user_journal_and_password() {
155 let key = derive_journal_key("alice", "private", "secret").unwrap();
156 assert_eq!(
157 key,
158 derive_journal_key("Alice", "private", "secret").unwrap()
159 );
160 assert_ne!(key, derive_journal_key("alice", "work", "secret").unwrap());
161 assert_ne!(key, derive_journal_key("bob", "private", "secret").unwrap());
162 assert_ne!(
163 key,
164 derive_journal_key("alice", "private", "different").unwrap()
165 );
166 }
167
168 #[test]
169 fn seal_round_trips_and_uses_random_nonce() {
170 let keys = derive_keys("alice", "password").unwrap();
171 let a = seal(&keys.encryption_key, b"hello").unwrap();
172 let b = seal(&keys.encryption_key, b"hello").unwrap();
173 assert_ne!(a, b);
174 assert_eq!(open(&keys.encryption_key, &a).unwrap(), b"hello");
175 assert_eq!(open(&keys.encryption_key, &b).unwrap(), b"hello");
176 }
177}