dup_crypto/
dewif.rs

1//  Copyright (C) 2020 Éloïs SANCHEZ.
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU Affero General Public License as
5// published by the Free Software Foundation, either version 3 of the
6// License, or (at your option) any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11// GNU Affero General Public License for more details.
12//
13// You should have received a copy of the GNU Affero General Public License
14// along with this program.  If not, see <https://www.gnu.org/licenses/>.
15
16//! Handle [DEWIF][dewif-spec] format
17//!
18//! See [DEWIF format specifications][dewif-spec].
19//!
20//! [dewif-spec]: https://git.duniter.org/documents/rfcs/blob/dewif/rfc/0013_Duniter_Encrypted_Wallet_Import_Format.md
21//!
22//! # Summary
23//!
24//! * [Write DEWIF file](#write-ed25519-key-pair-in-dewif-file)
25//! * [Read DEWIF file](#read-dewif-file)
26//!
27//!
28//! ## Write Bip32-Ed25519 key-pair in DEWIF file
29//!
30//! ```
31//! use dup_crypto::dewif::{Currency, G1_TEST_CURRENCY, create_dewif_v1};
32//! use dup_crypto::keys::ed25519::bip32::KeyPair;
33//! use dup_crypto::mnemonic::{Language, Mnemonic};
34//! use std::num::NonZeroU32;
35//!
36//! // Get user mnemonic (from cli prompt or gui)
37//! let mnemonic = Mnemonic::from_phrase(
38//!     "crop cash unable insane eight faith inflict route frame loud box vibrant",
39//!     Language::English,
40//! ).expect("wrong mnemonic");
41//!
42//! // Generate ed25519 keypair
43//! let keypair = KeyPair::from_mnemonic(&mnemonic);
44//!
45//! // Get user passphrase for DEWIF encryption
46//! let encryption_passphrase = "toto titi tata";
47//!
48//! // Serialize keypair in DEWIF format
49//! let dewif_content = create_dewif_v1(Currency::from(G1_TEST_CURRENCY), 12u8, &mnemonic, encryption_passphrase)?;
50//!
51//! // Then store `dewif_content` somewhere (It is a base64 string)
52//!
53//! # Ok::<(), dup_crypto::rand::UnspecifiedRandError>(())
54//! ```
55//!
56//! ## Read DEWIF file
57//!
58//! ```
59//! use dup_crypto::dewif::{Currency, G1_TEST_CURRENCY, DewifContent, DewifMeta, DewifPayload, ExpectedCurrency, read_dewif_content};
60//! use dup_crypto::keys::{KeysAlgo, KeyPair, Signator};
61//! use dup_crypto::mnemonic::{Language, Mnemonic};
62//! use std::num::NonZeroU32;
63//! use std::str::FromStr;
64//!
65//! // Get DEWIF content (Usually from disk)
66//! let dewif_content = "AAAAARAAAAEOASoqKioqKioqKioqKkIx/qkP1PWhtDNca4MdsvxPWAtvCd7nYriwMOHKxIFO8GJy9ElNngbSVQ==";
67//!
68//! // Get user passphrase for DEWIF decryption (from cli prompt or gui)
69//! let encryption_passphrase = "toto titi tata";
70//!
71//! // Expected currency
72//! let expected_currency = ExpectedCurrency::Specific(Currency::from(G1_TEST_CURRENCY));
73//!
74//! // Read DEWIF file content
75//! let DewifContent { meta, payload } = read_dewif_content(expected_currency, dewif_content, encryption_passphrase)?;
76//!
77//! assert_eq!(
78//!     meta,
79//!     DewifMeta {
80//!         algo: KeysAlgo::Bip32Ed25519,
81//!         currency: Currency::from(G1_TEST_CURRENCY),
82//!         log_n: 14,
83//!         version: 1
84//!     }
85//! );
86//!
87//! if let DewifPayload::Bip32Ed25519(mnemonic) = payload {
88//!     let key_pair = dup_crypto::keys::ed25519::bip32::KeyPair::from_mnemonic(&mnemonic);
89//!
90//!    // Generate signator
91//!    // `Signator` is a non-copiable and non-clonable type,
92//!    // so only generate it when you are in the scope where you effectively sign.
93//!    let signator = key_pair.generate_signator();
94//!
95//!    // Sign a message with keypair
96//!    let sig = signator.sign(b"message");
97//!
98//!    assert_eq!(
99//!        "N1+7Dzjde71hBCkoqSWRc3Ywn4+z7FChKjCgG8OtIlki4BH9w6QLXQ8Pkb7uyoCa9N9VuUgtJDgYSn09ll6yCg==",
100//!        &sig.to_string()
101//!    );
102//! } else {
103//!     panic!("corrupted dewif")
104//! }
105//!
106//! # Ok::<(), dup_crypto::dewif::DewifReadError>(())
107//! ```
108//!
109
110mod currency;
111mod read;
112mod write;
113
114pub use currency::{Currency, ExpectedCurrency, G1_CURRENCY, G1_TEST_CURRENCY};
115pub use read::{read_dewif_content, read_dewif_log_n, read_dewif_meta, DewifReadError};
116pub use write::create_dewif_v1;
117#[allow(deprecated)]
118pub use write::create_dewif_v1_legacy;
119
120#[cfg(feature = "bip32-ed25519")]
121use crate::keys::{KeyPair as _, KeysAlgo};
122use crate::scrypt::{params::ScryptParams, scrypt};
123use crate::seeds::{Seed42, Seed64};
124use crate::{hashs::Hash, rand::UnspecifiedRandError};
125
126const HEADERS_LEN: usize = 8;
127
128// v1
129static VERSION_V1: &[u8] = &[0, 0, 0, 1];
130const V1_CHECKSUM_LEN: usize = 8;
131const V1_NONCE_LEN: usize = 12;
132const V1_CLEAR_HEADERS_LEN: usize = 2 + V1_NONCE_LEN;
133
134// v1 Ed25519
135const V1_ED25519_ENCRYPTED_BYTES_LEN: usize = 64;
136const V1_ED25519_DATA_LEN: usize = V1_ED25519_ENCRYPTED_BYTES_LEN + V1_CLEAR_HEADERS_LEN; // 64 + 14 = 78
137const V1_ED25519_BYTES_LEN: usize = HEADERS_LEN + V1_ED25519_DATA_LEN; // 8 + 78 = 86
138const V1_ED25519_UNENCRYPTED_BYTES_LEN: usize =
139    V1_ED25519_BYTES_LEN - V1_ED25519_ENCRYPTED_BYTES_LEN; // 86 - 64 = 22
140
141// v1 Bip32-Ed25519
142const V1_BIP32_ED25519_ENCRYPTED_BYTES_LEN: usize = 42;
143const V1_BIP32_ED25519_DATA_LEN: usize =
144    V1_BIP32_ED25519_ENCRYPTED_BYTES_LEN + V1_CLEAR_HEADERS_LEN;
145const V1_BIP32_ED25519_BYTES_LEN: usize = HEADERS_LEN + V1_BIP32_ED25519_DATA_LEN;
146const V1_BIP32_ED25519_UNENCRYPTED_BYTES_LEN: usize =
147    V1_BIP32_ED25519_BYTES_LEN - V1_BIP32_ED25519_ENCRYPTED_BYTES_LEN;
148
149type Checksum = [u8; V1_CHECKSUM_LEN];
150type Nonce = [u8; V1_NONCE_LEN];
151
152#[derive(Debug, PartialEq)]
153/// DEWIF content
154pub struct DewifContent {
155    /// DEWIF meta data
156    pub meta: DewifMeta,
157    /// DEWIF payload
158    pub payload: DewifPayload,
159}
160
161/// DEWIF meta data
162#[derive(Clone, Copy, Debug, PartialEq)]
163pub struct DewifMeta {
164    /// Algorithm
165    pub algo: KeysAlgo,
166    /// Currency
167    pub currency: Currency,
168    /// Scrypt parameter log N
169    pub log_n: u8,
170    /// DEWIF version
171    pub version: u32,
172}
173
174#[derive(Debug, PartialEq)]
175/// DEWIF payload
176pub enum DewifPayload {
177    /// Ed25519 algo payload
178    Ed25519(crate::keys::ed25519::Ed25519KeyPair),
179    /// Bip32-Ed25519 algo payload
180    Bip32Ed25519(crate::mnemonic::Mnemonic),
181}
182
183/// Change DEWIF passphrase
184pub fn change_dewif_passphrase(
185    file_content: &str,
186    old_passphrase: &str,
187    new_passphrase: &str,
188) -> Result<String, DewifReadError> {
189    let DewifContent {
190        meta:
191            DewifMeta {
192                algo: _,
193                currency,
194                log_n,
195                version,
196            },
197        payload,
198    } = read_dewif_content(ExpectedCurrency::Any, file_content, old_passphrase)?;
199    if version == 1 {
200        let mut bytes = base64::decode(file_content).map_err(DewifReadError::InvalidBase64Str)?;
201        match payload {
202            DewifPayload::Ed25519(kp) => {
203                let seed = read::get_dewif_seed_unchecked(&mut bytes[8..], old_passphrase);
204                write::write_dewif_v1_ed25519(
205                    currency,
206                    log_n,
207                    new_passphrase,
208                    &kp.public_key(),
209                    &seed,
210                )
211                .map_err(|_| DewifReadError::UnspecifiedRandError)
212            }
213            #[cfg(feature = "bip32-ed25519")]
214            DewifPayload::Bip32Ed25519(mnemonic) => {
215                write::write_dewif_v1_bip_ed25519(currency, log_n, new_passphrase, &mnemonic)
216                    .map_err(|_| DewifReadError::UnspecifiedRandError)
217            }
218        }
219    } else {
220        Err(DewifReadError::UnsupportedVersion { actual: version })
221    }
222}
223
224fn compute_checksum(nonce: &Nonce, language_code: u8, mnemonic_entropy: &[u8]) -> Checksum {
225    let mut cs_bytes = [0u8; V1_CHECKSUM_LEN];
226    let hash = crate::hashs::Hash::compute_multipart(&[
227        &nonce[..],
228        &[language_code, mnemonic_entropy.len() as u8],
229        mnemonic_entropy,
230    ]);
231    cs_bytes.copy_from_slice(&hash.0[..8]);
232    cs_bytes
233}
234
235#[cfg(not(test))]
236fn gen_nonce() -> Result<Nonce, UnspecifiedRandError> {
237    let mut nonce = [0u8; V1_NONCE_LEN];
238    crate::rand::gen_random_bytes(&mut nonce[..])?;
239    Ok(nonce)
240}
241#[cfg(test)]
242#[allow(clippy::unnecessary_wraps)]
243fn gen_nonce() -> Result<Nonce, UnspecifiedRandError> {
244    Ok([42u8; V1_NONCE_LEN])
245}
246
247fn gen_xor_seed42(log_n: u8, nonce: Nonce, passphrase: &str) -> Seed42 {
248    let salt = Hash::compute_multipart(&[b"dewif", &nonce[..], passphrase.as_bytes()]);
249    let mut seed_bytes = [0u8; 42];
250    scrypt(
251        passphrase.as_bytes(),
252        salt.as_ref(),
253        &ScryptParams { log_n, r: 16, p: 1 },
254        &mut seed_bytes,
255    );
256    Seed42::new(seed_bytes)
257}
258
259fn gen_xor_seed64(log_n: u8, nonce: Nonce, passphrase: &str) -> Seed64 {
260    let salt = Hash::compute_multipart(&[b"dewif", &nonce[..], passphrase.as_bytes()]);
261    let mut seed_bytes = [0u8; 64];
262    scrypt(
263        passphrase.as_bytes(),
264        salt.as_ref(),
265        &ScryptParams { log_n, r: 16, p: 1 },
266        &mut seed_bytes,
267    );
268    Seed64::new(seed_bytes)
269}
270
271fn read_nonce(bytes: &[u8]) -> Nonce {
272    let mut nonce = [0u8; V1_NONCE_LEN];
273    nonce.copy_from_slice(bytes);
274    nonce
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use crate::bases::b58::ToBase58 as _;
281    use crate::keys::ed25519::KeyPairFromSeed32Generator;
282    use crate::seeds::Seed32;
283    use unwrap::unwrap;
284
285    #[test]
286    fn dewif_v1() {
287        let written_keypair = KeyPairFromSeed32Generator::generate(Seed32::new([0u8; 32]));
288        let currency = Currency::from(G1_TEST_CURRENCY);
289
290        let dewif_content_str = unwrap!(write::write_dewif_v1_ed25519(
291            currency,
292            12,
293            "toto",
294            &written_keypair.public_key(),
295            &written_keypair.seed(),
296        ));
297
298        let dewif_content = unwrap!(read_dewif_content(
299            ExpectedCurrency::Specific(currency),
300            &dewif_content_str,
301            "toto"
302        ));
303
304        assert_eq!(
305            DewifPayload::Ed25519(written_keypair.clone()),
306            dewif_content.payload,
307        );
308
309        // Change DEWIF passphrase
310        let new_dewif_content =
311            unwrap!(change_dewif_passphrase(&dewif_content_str, "toto", "titi"));
312
313        let dewif_content = unwrap!(read_dewif_content(
314            ExpectedCurrency::Specific(currency),
315            &new_dewif_content,
316            "titi"
317        ));
318
319        assert_eq!(
320            DewifPayload::Ed25519(written_keypair),
321            dewif_content.payload,
322        );
323    }
324
325    #[test]
326    fn dewif_v1_corrupted() -> Result<(), ()> {
327        let written_keypair = KeyPairFromSeed32Generator::generate(Seed32::new([0u8; 32]));
328        let currency = Currency::from(G1_TEST_CURRENCY);
329
330        let mut dewif_content = unwrap!(write::write_dewif_v1_ed25519(
331            currency,
332            12,
333            "toto",
334            &written_keypair.public_key(),
335            &written_keypair.seed(),
336        ));
337
338        // Corrupt one byte in dewif_content
339        let dewif_bytes_mut = unsafe { dewif_content.as_bytes_mut() };
340        dewif_bytes_mut[13] = 0x52;
341
342        if let Err(DewifReadError::CorruptedContent) =
343            read_dewif_content(ExpectedCurrency::Specific(currency), &dewif_content, "toto")
344        {
345            Ok(())
346        } else {
347            panic!("dewif content must be corrupted.")
348        }
349    }
350
351    #[test]
352    fn dewif_v1_bip32() -> Result<(), crate::mnemonic::MnemonicError> {
353        let mnemonic = crate::mnemonic::Mnemonic::from_phrase(
354            "crop cash unable insane eight faith inflict route frame loud box vibrant",
355            crate::mnemonic::Language::English,
356        )?;
357        let mnemonic_copy = crate::mnemonic::Mnemonic::from_phrase(
358            "crop cash unable insane eight faith inflict route frame loud box vibrant",
359            crate::mnemonic::Language::English,
360        )?;
361        let currency = Currency::from(G1_TEST_CURRENCY);
362
363        let dewif_content_str = unwrap!(write::write_dewif_v1_bip_ed25519(
364            currency, 12, "toto", &mnemonic
365        ));
366
367        let dewif_content = unwrap!(read_dewif_content(
368            ExpectedCurrency::Specific(currency),
369            &dewif_content_str,
370            "toto"
371        ));
372
373        assert_eq!(DewifPayload::Bip32Ed25519(mnemonic), dewif_content.payload,);
374
375        // Change DEWIF passphrase
376        let new_dewif_content_str =
377            unwrap!(change_dewif_passphrase(&dewif_content_str, "toto", "titi"));
378
379        let new_dewif_content = unwrap!(read_dewif_content(
380            ExpectedCurrency::Specific(currency),
381            &new_dewif_content_str,
382            "titi"
383        ));
384
385        assert_eq!(
386            DewifPayload::Bip32Ed25519(mnemonic_copy),
387            new_dewif_content.payload,
388        );
389
390        Ok(())
391    }
392
393    #[test]
394    #[allow(deprecated)]
395    fn dewif_v1_legacy() -> Result<(), DewifReadError> {
396        let currency = Currency::from(G1_CURRENCY);
397        let dewif = unwrap!(create_dewif_v1_legacy(
398            currency,
399            12,
400            "pass".to_owned(),
401            "salt".to_owned(),
402            "toto"
403        ));
404
405        if let DewifContent {
406            payload: DewifPayload::Ed25519(key_pair),
407            ..
408        } = read_dewif_content(ExpectedCurrency::Specific(currency), &dewif, "toto")?
409        {
410            assert_eq!(
411                "3YumN7F7D8c2hmkHLHf3ZD8wc3tBHiECEK9zLPkaJtAF",
412                &key_pair.public_key().to_base58()
413            );
414        } else {
415            panic!("corrupted dewif");
416        }
417
418        Ok(())
419    }
420}