wamu_core/
share_recovery_backup.rs

1//! Share recovery with encrypted backup implementation.
2//!
3//! Ref: <https://wamu.tech/specification#share-recovery-backup>.
4//!
5//! [HKDF (HMAC-based Extract-and-Expand Key Derivation Function)](https://tools.ietf.org/html/rfc5869) and
6//! [AES-GCM (Advanced Encryption Standard Galois/Counter Mode)](https://en.wikipedia.org/wiki/Galois/Counter_Mode)
7//! are the key derivation function and symmetric encryption algorithm used respectively.
8
9use aes_gcm::aead::consts::U12;
10use aes_gcm::aes::Aes256;
11use aes_gcm::{
12    aead::{Aead, AeadCore, KeyInit},
13    Aes256Gcm, AesGcm,
14};
15use crypto_bigint::{Encoding, U256};
16use hkdf::Hkdf;
17use sha2::Sha256;
18
19use crate::errors::ShareBackupRecoveryError;
20use crate::payloads::EncryptedShareBackup;
21use crate::share::{SigningShare, SubShare};
22use crate::traits::IdentityProvider;
23
24/// Given an entropy seed (i.e typically a standardized phrase), "signing share", "sub-share" and identity provider,
25/// returns an ok result including the encrypted share backup (i.e an encrypted "signing share" and "sub-share", and a random nonce)
26/// or an encryption error result.
27///
28/// Ref: <https://wamu.tech/specification#share-recovery-backup-encrypt>.
29pub fn backup(
30    entropy_seed: &[u8],
31    signing_share: &SigningShare,
32    sub_share: &SubShare,
33    identity_provider: &impl IdentityProvider,
34) -> Result<EncryptedShareBackup, ShareBackupRecoveryError> {
35    // Generates nonce.
36    let nonce = Aes256Gcm::generate_nonce(&mut rand::thread_rng());
37
38    // Encrypts the "signing share" and "sub-share".
39    let cipher = generate_encryption_cipher(entropy_seed, identity_provider);
40    let encrypted_signing_share = cipher.encrypt(&nonce, signing_share.to_be_bytes().as_ref())?;
41    let encrypted_sub_share = (
42        cipher.encrypt(&nonce, sub_share.x().to_be_bytes().as_ref())?,
43        cipher.encrypt(&nonce, sub_share.y().to_be_bytes().as_ref())?,
44    );
45
46    // Returns the encrypted share backup.
47    Ok(EncryptedShareBackup {
48        signing_share: encrypted_signing_share,
49        sub_share: encrypted_sub_share,
50        nonce: nonce.to_vec(),
51    })
52}
53
54/// Given an entropy seed (i.e typically a standardized phrase), encrypted share backup
55/// (i.e an encrypted "signing share" and "sub-share", and a random nonce) and an identity provider,
56/// returns the decrypted "signing share" and "sub-share".
57///
58/// Ref: <https://wamu.tech/specification#share-recovery-backup-decrypt>.
59pub fn recover(
60    entropy_seed: &[u8],
61    encrypted_share_backup: &EncryptedShareBackup,
62    identity_provider: &impl IdentityProvider,
63) -> Result<(SigningShare, SubShare), ShareBackupRecoveryError> {
64    // Generates nonce.
65    let nonce = aes_gcm::Nonce::from_slice(&encrypted_share_backup.nonce);
66
67    // Decrypts the "signing share" and "sub-share".
68    let cipher = generate_encryption_cipher(entropy_seed, identity_provider);
69    let signing_share_bytes =
70        cipher.decrypt(nonce, encrypted_share_backup.signing_share.as_ref())?;
71    let signing_share = SigningShare::try_from(signing_share_bytes.as_ref())
72        .map_err(|_| ShareBackupRecoveryError::InvalidSigningShare)?;
73    let sub_share = SubShare::new(
74        U256::from_be_bytes(
75            cipher
76                .decrypt(nonce, encrypted_share_backup.sub_share.0.as_ref())?
77                .try_into()
78                .map_err(|_| ShareBackupRecoveryError::InvalidSubShare)?,
79        ),
80        U256::from_be_bytes(
81            cipher
82                .decrypt(nonce, encrypted_share_backup.sub_share.1.as_ref())?
83                .try_into()
84                .map_err(|_| ShareBackupRecoveryError::InvalidSubShare)?,
85        ),
86    )
87    .map_err(|_| ShareBackupRecoveryError::InvalidSubShare)?;
88
89    Ok((signing_share, sub_share))
90}
91
92/// Given an entropy seed (i.e typically a standardized phrase) and an identity provider, returns an encryption cipher.
93fn generate_encryption_cipher(
94    entropy_seed: &[u8],
95    identity_provider: &impl IdentityProvider,
96) -> AesGcm<Aes256, U12> {
97    // Generates encryption key.
98    let key_bytes = generate_encryption_key(entropy_seed, identity_provider);
99    let key = aes_gcm::Key::<Aes256Gcm>::from_slice(&key_bytes);
100
101    // Generates and returns cipher.
102    Aes256Gcm::new(key)
103}
104
105/// Given an entropy seed (i.e typically a standardized phrase) and an identity provider, returns a 256 bit encryption secret.
106fn generate_encryption_key(
107    entropy_seed: &[u8],
108    identity_provider: &impl IdentityProvider,
109) -> [u8; 32] {
110    // Generates entropy as the signature of the entropy seed phrase.
111    let entropy = identity_provider.sign(entropy_seed);
112
113    // Generates encryption key.
114    let mut output_key = [0u8; 32];
115    Hkdf::<Sha256>::new(None, &entropy.sig)
116        .expand(&[], &mut output_key)
117        .expect("32 is a valid length for Sha256 to output");
118
119    // Returns generated encryption key.
120    output_key
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::crypto::Random32Bytes;
127    use crate::share::SecretShare;
128    use crate::share_split_reconstruct;
129    use crate::test_utils::MockECDSAIdentityProvider;
130
131    #[test]
132    fn share_recovery_with_encrypted_backup_works() {
133        // Generates identity provider.
134        let identity_provider = MockECDSAIdentityProvider::generate();
135
136        // Set entropy seed.
137        let entropy_seed = b"Hello, world!";
138
139        // Generates secret share.
140        let secret_share = SecretShare::from(Random32Bytes::generate_mod_q());
141
142        // Computes "signing share" and "sub-share".
143        let (signing_share, sub_share) =
144            share_split_reconstruct::split(&secret_share, &identity_provider).unwrap();
145
146        // Generates encryption share backup.
147        let backup_result = backup(entropy_seed, &signing_share, &sub_share, &identity_provider);
148
149        // Verifies backup result.
150        assert!(backup_result.is_ok());
151
152        // Unwraps encrypted share backup.
153        let encrypted_share_backup = backup_result.unwrap();
154
155        // Generates encryption share backup.
156        let recover_result = recover(entropy_seed, &encrypted_share_backup, &identity_provider);
157
158        // Verifies recover result.
159        assert!(recover_result.is_ok());
160
161        // Unwraps "signing share" and "sub-share" from recover result.
162        let (recovered_signing_share, recovered_sub_share) = recover_result.unwrap();
163
164        // Verifies recovered "signing share" and "sub-share".
165        assert_eq!(
166            &recovered_signing_share.to_be_bytes(),
167            &signing_share.to_be_bytes()
168        );
169        assert_eq!(recovered_sub_share.as_tuple(), sub_share.as_tuple());
170    }
171
172    #[test]
173    fn generate_encryption_key_works() {
174        // Generates identity provider.
175        let identity_provider = MockECDSAIdentityProvider::generate();
176
177        // Set entropy seed.
178        let entropy_seed = b"Hello, world!";
179
180        // Generates encryption key.
181        let encryption_key = generate_encryption_key(entropy_seed, &identity_provider);
182
183        // Verifies that generated encryption key is deterministic based on the entropy seed and identity provider.
184        assert_eq!(
185            encryption_key,
186            generate_encryption_key(entropy_seed, &identity_provider)
187        );
188
189        // Verifies that different inputs (entropy seed and identity provider) permutations produce different encryption keys.
190        assert_ne!(
191            encryption_key,
192            generate_encryption_key(entropy_seed, &MockECDSAIdentityProvider::generate())
193        );
194        assert_ne!(
195            encryption_key,
196            generate_encryption_key(b"Another phrase.", &identity_provider)
197        );
198    }
199}