pubky_common/
recovery_file.rs

1//! Tools for encrypting and decrypting a recovery file storing user's root key's secret.
2
3use argon2::Argon2;
4
5use crate::crypto::{decrypt, encrypt, Keypair};
6
7static SPEC_NAME: &str = "recovery";
8static SPEC_LINE: &str = "pubky.org/recovery";
9
10/// Decrypt a recovery file.
11pub fn decrypt_recovery_file(recovery_file: &[u8], passphrase: &str) -> Result<Keypair, Error> {
12    let encryption_key = recovery_file_encryption_key_from_passphrase(passphrase);
13
14    let newline_index = recovery_file
15        .iter()
16        .position(|&r| r == 10)
17        .ok_or(())
18        .map_err(|_| Error::RecoveryFileMissingSpecLine)?;
19
20    let spec_line = &recovery_file[..newline_index];
21
22    if !(spec_line.starts_with(SPEC_LINE.as_bytes())
23        || spec_line.starts_with(b"pkarr.org/recovery"))
24    {
25        return Err(Error::RecoveryFileVersionNotSupported);
26    }
27
28    let encrypted = &recovery_file[newline_index + 1..];
29
30    if encrypted.is_empty() {
31        return Err(Error::RecoverFileMissingEncryptedSecretKey);
32    };
33
34    let decrypted = decrypt(encrypted, &encryption_key)?;
35    let length = decrypted.len();
36    let secret_key: [u8; 32] = decrypted
37        .try_into()
38        .map_err(|_| Error::RecoverFileInvalidSecretKeyLength(length))?;
39
40    Ok(Keypair::from_secret(&secret_key))
41}
42
43/// Encrypt a recovery file.
44pub fn create_recovery_file(keypair: &Keypair, passphrase: &str) -> Vec<u8> {
45    let encryption_key = recovery_file_encryption_key_from_passphrase(passphrase);
46    let secret_key = keypair.secret();
47
48    let encrypted_secret_key = encrypt(&secret_key, &encryption_key);
49
50    let mut out = Vec::with_capacity(SPEC_LINE.len() + 1 + encrypted_secret_key.len());
51
52    out.extend_from_slice(SPEC_LINE.as_bytes());
53    out.extend_from_slice(b"\n");
54    out.extend_from_slice(&encrypted_secret_key);
55
56    out
57}
58
59fn recovery_file_encryption_key_from_passphrase(passphrase: &str) -> [u8; 32] {
60    let argon2id = Argon2::default();
61
62    let mut out = [0; 32];
63
64    argon2id
65        .hash_password_into(passphrase.as_bytes(), SPEC_NAME.as_bytes(), &mut out)
66        .expect("Output is the correct length, so this should be infallible");
67
68    out
69}
70
71#[derive(thiserror::Error, Debug)]
72/// Error decrypting a recovery file
73pub enum Error {
74    // === Recovery file ==
75    #[error("Recovery file should start with a spec line, followed by a new line character")]
76    /// Recovery file should start with a spec line, followed by a new line character
77    RecoveryFileMissingSpecLine,
78
79    #[error("Recovery file should start with a spec line, followed by a new line character")]
80    /// Recovery file should start with a spec line, followed by a new line character
81    RecoveryFileVersionNotSupported,
82
83    #[error("Recovery file should contain an encrypted secret key after the new line character")]
84    /// Recovery file should contain an encrypted secret key after the new line character
85    RecoverFileMissingEncryptedSecretKey,
86
87    #[error("Recovery file encrypted secret key should be 32 bytes, got {0}")]
88    /// Recovery file encrypted secret key should be 32 bytes, got {0}
89    RecoverFileInvalidSecretKeyLength(usize),
90
91    #[error(transparent)]
92    /// Error while decrypting a message
93    DecryptError(#[from] crate::crypto::DecryptError),
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn encrypt_decrypt_recovery_file() {
102        let passphrase = "very secure password";
103        let keypair = Keypair::random();
104
105        let recovery_file = create_recovery_file(&keypair, passphrase);
106        let recovered = decrypt_recovery_file(&recovery_file, passphrase).unwrap();
107
108        assert_eq!(recovered.public_key(), keypair.public_key());
109    }
110}