engine/snapshot/migration/
v2.rs

1// Copyright 2023 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use super::*;
5
6/// Key size for the ephemeral key
7const KEY_SIZE: usize = 32;
8/// Key type alias.
9pub type Key = [u8; KEY_SIZE];
10
11/// Nonce size for XChaCha20Poly1305
12const NONCE_SIZE: usize = XChaCha20Poly1305::NONCE_LENGTH;
13/// Nonce type alias
14pub type Nonce = [u8; NONCE_SIZE];
15
16const VERSION_V2: [u8; 2] = [0x2, 0x0];
17
18/// Read ciphertext from the input, decrypts it using the specified key and the associated data
19/// specified during encryption and returns the plaintext
20pub(crate) fn read<I: Read>(input: &mut I, key: &Key, associated_data: &[u8]) -> Result<Zeroizing<Vec<u8>>, Error> {
21    // create ephemeral private key.
22    let mut ephemeral_pk = [0; x25519::PUBLIC_KEY_LENGTH];
23    // get ephemeral private key from input.
24    input.read_exact(&mut ephemeral_pk)?;
25
26    // creating the secret key now expects an array
27    let mut key_bytes = [0u8; x25519::SECRET_KEY_LENGTH];
28    key_bytes.clone_from_slice(key);
29
30    // derive public key from ephemeral private key
31    let ephemeral_pk = x25519::PublicKey::from_bytes(ephemeral_pk);
32
33    // get x25519 key pair from ephemeral private key.
34    let sk = x25519::SecretKey::from_bytes(key_bytes);
35    let pk = sk.public_key();
36
37    // diffie hellman to create the shared secret.
38    let shared = sk.diffie_hellman(&ephemeral_pk);
39
40    // compute the nonce using the ephemeral keys.
41    let nonce = {
42        let mut i = ephemeral_pk.to_bytes().to_vec();
43        i.extend_from_slice(&pk.to_bytes());
44        let res = blake2b::Blake2b256::digest(&i).to_vec();
45        let v: Nonce = res[0..NONCE_SIZE].try_into().expect("slice with incorrect length");
46        v
47    };
48
49    // create and read tag from input.
50    let mut tag = [0; XChaCha20Poly1305::TAG_LENGTH];
51    input.read_exact(&mut tag)?;
52
53    // create and read ciphertext from input.
54    let mut ct = Vec::new();
55    input.read_to_end(&mut ct)?;
56
57    // create plain text buffer.
58    let mut pt = Zeroizing::new(vec![0; ct.len()]);
59
60    // decrypt the ciphertext into the plain text buffer.
61    XChaCha20Poly1305::try_decrypt(&shared.to_bytes(), &nonce, associated_data, &mut pt, &ct, &tag)
62        .map_err(|_| Error::DecryptFailed)?;
63
64    Ok(pt)
65}
66
67/// Encrypt the opaque plaintext bytestring using the specified [`Key`] and optional associated data
68/// and writes the ciphertext to the specifed output
69#[allow(dead_code)]
70pub(crate) fn write<O: Write>(plain: &[u8], output: &mut O, key: &Key, associated_data: &[u8]) -> Result<(), Error> {
71    // create ephemeral key pair.
72    let ephemeral_key = x25519::SecretKey::generate()?;
73
74    // get public key.
75    let ephemeral_pk = ephemeral_key.public_key();
76
77    let ephemeral_pk_bytes = ephemeral_pk.to_bytes();
78
79    // write public key into output.
80    output.write_all(&ephemeral_pk_bytes)?;
81
82    // secret key now expects an array
83    let mut key_bytes = [0u8; x25519::SECRET_KEY_LENGTH];
84    key_bytes.clone_from_slice(key);
85
86    // get `x25519` secret key from public key.
87    let pk = x25519::SecretKey::from_bytes(key_bytes).public_key();
88
89    let pk_bytes = pk.to_bytes();
90
91    // do a diffie_hellman exchange to make a shared secret key.
92    let shared = ephemeral_key.diffie_hellman(&pk);
93
94    // compute the nonce using the ephemeral keys.
95    let nonce = {
96        let mut i = ephemeral_pk.to_bytes().to_vec();
97        i.extend_from_slice(&pk_bytes);
98        let res = blake2b::Blake2b256::digest(&i).to_vec();
99        let v: Nonce = res[0..NONCE_SIZE].try_into().expect("slice with incorrect length");
100        v
101    };
102
103    // create the XChaCha20Poly1305 tag.
104    let mut tag = [0; XChaCha20Poly1305::TAG_LENGTH];
105
106    // creates the ciphertext.
107    let mut ct = vec![0; plain.len()];
108
109    // decrypt the plain text into the ciphertext buffer.
110    XChaCha20Poly1305::try_encrypt(&shared.to_bytes(), &nonce, associated_data, plain, &mut ct, &mut tag)?;
111
112    // write tag and ciphertext into the output.
113    output.write_all(&tag)?;
114    output.write_all(&ct)?;
115
116    Ok(())
117}
118
119/// Read & decrypt V2wallet snapshot file.
120///
121/// Relevant references:
122///
123/// - [PBKDF2 in crypto.rs](https://github.com/iotaledger/crypto.rs/blob/3c5c415ff44a6106e4b133dd157d846363e07ebe/src/keys/pbkdf.rs#L32)
124/// - [wallet.rs style to create KeyProvider in iota.rs](https://github.com/iotaledger/iota.rs/blob/03c72133279b98f12c91b1e1bdc14965d9f48cf9/client/src/stronghold/common.rs#L33)
125/// - [KeyProvider constructor in stronghold.rs](https://github.com/iotaledger/stronghold.rs/blob/c2dfa5f9f1f32220d377ed64d934947b22943a5f/client/src/security/keyprovider.rs#L80)
126///
127/// Dependencies (should not change):
128///
129/// - `crypto.rs`
130/// - `crate::snapshot::decompress`
131pub(crate) fn read_snapshot(path: &Path, key: &[u8; 32], aad: &[u8]) -> Result<Zeroizing<Vec<u8>>, Error> {
132    let mut f: File = OpenOptions::new().read(true).open(path)?;
133
134    // check min file length
135    const MIN_LEN: u64 =
136        (MAGIC.len() + VERSION_V2.len() + x25519::PUBLIC_KEY_LENGTH + XChaCha20Poly1305::TAG_LENGTH) as u64;
137    guard(f.metadata()?.len() >= MIN_LEN, Error::BadSnapshotFormat)?;
138
139    // check the magic bytes
140    let mut magic = [0u8; 5];
141    f.read_exact(&mut magic)?;
142    guard(magic == MAGIC, Error::BadSnapshotFormat)?;
143
144    // check the version
145    let mut version = [0u8; 2];
146    f.read_exact(&mut version)?;
147    guard(version == VERSION_V2, Error::BadSnapshotVersion)?;
148
149    let pt = Zeroizing::new(read(&mut f, key, aad)?);
150
151    decompress(&pt).map(Zeroizing::new).map_err(|_| Error::DecompressFailed)
152}