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