pubky_common/
recovery_file.rs1use 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
11pub 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
44pub 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)]
73pub enum Error {
75 #[error("Recovery file should start with a spec line, followed by a new line character")]
77 RecoveryFileMissingSpecLine,
79
80 #[error("Recovery file should start with a spec line, followed by a new line character")]
81 RecoveryFileVersionNotSupported,
83
84 #[error("Recovery file should contain an encrypted secret key after the new line character")]
85 RecoverFileMissingEncryptedSecretKey,
87
88 #[error("Recovery file encrypted secret key should be 32 bytes, got {0}")]
89 RecoverFileInvalidSecretKeyLength(usize),
91
92 #[error(transparent)]
93 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}