1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
// Copyright (C) 2019-2023 Aleo Systems Inc.
// This file is part of the Aleo SDK library.

// The Aleo SDK library is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// The Aleo SDK library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with the Aleo SDK library. If not, see <https://www.gnu.org/licenses/>.

use crate::types::native::{
    CiphertextNative,
    CurrentNetwork,
    FieldNative,
    IdentifierNative,
    LiteralNative,
    Network,
    PlaintextNative,
    PrivateKeyNative,
    Uniform,
};

use once_cell::sync::OnceCell;
use std::str::FromStr;

/// Tool for encrypting and decrypting Aleo key material into ciphertext
pub struct Encryptor;

impl Encryptor {
    /// Encrypt a private key into ciphertext using a secret
    pub(crate) fn encrypt_private_key_with_secret(
        private_key: &PrivateKeyNative,
        secret: &str,
    ) -> Result<CiphertextNative, String> {
        Self::encrypt_field(&private_key.seed(), secret, "private_key")
    }

    /// Decrypt a private key from ciphertext using a secret
    pub(crate) fn decrypt_private_key_with_secret(
        ciphertext: &CiphertextNative,
        secret: &str,
    ) -> Result<PrivateKeyNative, String> {
        let seed = Self::decrypt_field(ciphertext, secret, "private_key")?;
        PrivateKeyNative::try_from(seed).map_err(|e| e.to_string())
    }

    // Encrypted a field element into a ciphertext representation
    fn encrypt_field(field: &FieldNative, secret: &str, domain: &str) -> Result<CiphertextNative, String> {
        // Derive the domain separators and the secret.
        let domain = FieldNative::new_domain_separator(domain);
        let secret = FieldNative::new_domain_separator(secret);

        // Generate a nonce
        let mut rng = rand::thread_rng();
        let nonce = Uniform::rand(&mut rng);

        // Derive a blinding factor and create an encryption target
        let blinding = CurrentNetwork::hash_psd2(&[domain, nonce, secret]).map_err(|e| e.to_string())?;
        let key = blinding * field;
        let plaintext = PlaintextNative::Struct(
            indexmap::IndexMap::from_iter(vec![
                (
                    IdentifierNative::from_str("key").map_err(|e| e.to_string())?,
                    PlaintextNative::from(LiteralNative::Field(key)),
                ),
                (
                    IdentifierNative::from_str("nonce").map_err(|e| e.to_string())?,
                    PlaintextNative::from(LiteralNative::Field(nonce)),
                ),
            ]),
            OnceCell::new(),
        );
        plaintext.encrypt_symmetric(secret).map_err(|e| e.to_string())
    }

    // Recover a field element encrypted within ciphertext
    fn decrypt_field(ciphertext: &CiphertextNative, secret: &str, domain: &str) -> Result<FieldNative, String> {
        let domain = FieldNative::new_domain_separator(domain);
        let secret = FieldNative::new_domain_separator(secret);
        let decrypted = ciphertext.decrypt_symmetric(secret).map_err(|e| e.to_string())?;
        let recovered_key = Self::extract_value(&decrypted, "key")?;
        let recovered_nonce = Self::extract_value(&decrypted, "nonce")?;
        let recovered_blinding =
            CurrentNetwork::hash_psd2(&[domain, recovered_nonce, secret]).map_err(|e| e.to_string())?;
        Ok(recovered_key / recovered_blinding)
    }

    // Extract a field element from a plaintext
    fn extract_value(plaintext: &PlaintextNative, identifier: &str) -> Result<FieldNative, String> {
        let identity = IdentifierNative::from_str(identifier).map_err(|e| e.to_string())?;
        let value = plaintext.find(&[identity]).map_err(|e| e.to_string())?;
        match value {
            PlaintextNative::Literal(literal, ..) => match literal {
                LiteralNative::Field(recovered_value) => Ok(recovered_value),
                _ => Err("Wrong literal type".to_string()),
            },
            _ => Err("Expected literal".to_string()),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    use snarkvm_console::prelude::TestRng;
    use wasm_bindgen_test::*;

    #[wasm_bindgen_test]
    fn test_encryptor_encrypt_and_decrypt() {
        let mut rng = TestRng::default();
        let private_key = PrivateKeyNative::new(&mut rng).unwrap();
        let enc = Encryptor::encrypt_private_key_with_secret(&private_key, "mypassword").unwrap();
        let recovered_private_key = Encryptor::decrypt_private_key_with_secret(&enc, "mypassword").unwrap();
        assert_eq!(private_key, recovered_private_key);
    }

    #[wasm_bindgen_test]
    fn test_encryptor_wrong_private_key_doesnt_decrypt() {
        let mut rng = TestRng::default();
        let private_key = PrivateKeyNative::new(&mut rng).unwrap();
        let enc = Encryptor::encrypt_private_key_with_secret(&private_key, "mypassword").unwrap();
        let recovered_private_key = Encryptor::decrypt_private_key_with_secret(&enc, "wrong_password");
        assert!(recovered_private_key.is_err())
    }

    #[wasm_bindgen_test]
    fn test_encryptor_same_secret_doesnt_produce_same_ciphertext_on_different_runs() {
        let mut rng = TestRng::default();
        let private_key = PrivateKeyNative::new(&mut rng).unwrap();
        let enc = Encryptor::encrypt_private_key_with_secret(&private_key, "mypassword").unwrap();
        let enc2 = Encryptor::encrypt_private_key_with_secret(&private_key, "mypassword").unwrap();
        assert_ne!(enc, enc2);

        // Assert that we can decrypt both ciphertexts with the same secret despite being different
        let recovered_key_1 = Encryptor::decrypt_private_key_with_secret(&enc, "mypassword").unwrap();
        let recovered_key_2 = Encryptor::decrypt_private_key_with_secret(&enc, "mypassword").unwrap();
        assert_eq!(recovered_key_1, recovered_key_2);
    }

    #[wasm_bindgen_test]
    fn test_encryptor_private_keys_encrypted_with_different_passwords_match() {
        let mut rng = TestRng::default();
        let private_key = PrivateKeyNative::new(&mut rng).unwrap();
        let enc = Encryptor::encrypt_private_key_with_secret(&private_key, "mypassword").unwrap();
        let enc2 = Encryptor::encrypt_private_key_with_secret(&private_key, "mypassword2").unwrap();
        assert_ne!(enc, enc2);

        // Assert that we can decrypt both ciphertexts with to the same key
        let recovered_key_1 = Encryptor::decrypt_private_key_with_secret(&enc, "mypassword").unwrap();
        let recovered_key_2 = Encryptor::decrypt_private_key_with_secret(&enc2, "mypassword2").unwrap();
        assert_eq!(recovered_key_1, recovered_key_2);
    }

    #[wasm_bindgen_test]
    fn test_encryptor_different_private_keys_encrypted_with_same_password_dont_match() {
        let mut rng = TestRng::default();
        let private_key = PrivateKeyNative::new(&mut rng).unwrap();
        let private_key2 = PrivateKeyNative::new(&mut rng).unwrap();
        let enc = Encryptor::encrypt_private_key_with_secret(&private_key, "mypassword").unwrap();
        let enc2 = Encryptor::encrypt_private_key_with_secret(&private_key2, "mypassword").unwrap();
        assert_ne!(enc, enc2);

        // Assert that private key plaintexts don't match
        let recovered_key_1 = Encryptor::decrypt_private_key_with_secret(&enc, "mypassword").unwrap();
        let recovered_key_2 = Encryptor::decrypt_private_key_with_secret(&enc2, "mypassword").unwrap();
        assert_ne!(recovered_key_1, recovered_key_2);
    }
}