Skip to main content

note_to_self_lib/
crypto.rs

1use 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}