pubky_common/
recovery_file.rs1use argon2::Argon2;
4
5use crate::crypto::{decrypt, encrypt, Keypair};
6
7static SPEC_NAME: &str = "recovery";
8static SPEC_LINE: &str = "pubky.org/recovery";
9
10pub 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
43pub 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)]
72pub enum Error {
74 #[error("Recovery file should start with a spec line, followed by a new line character")]
76 RecoveryFileMissingSpecLine,
78
79 #[error("Recovery file should start with a spec line, followed by a new line character")]
80 RecoveryFileVersionNotSupported,
82
83 #[error("Recovery file should contain an encrypted secret key after the new line character")]
84 RecoverFileMissingEncryptedSecretKey,
86
87 #[error("Recovery file encrypted secret key should be 32 bytes, got {0}")]
88 RecoverFileInvalidSecretKeyLength(usize),
90
91 #[error(transparent)]
92 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}