diff --git a/src/dewif.rs b/src/dewif.rs index b386fc699b7b6e876d11c1b64ca62d058d05707b..e7a8c89510eee1107e4e595b5c102af80fc7b849 100644 --- a/src/dewif.rs +++ b/src/dewif.rs @@ -18,8 +18,9 @@ //! # Write ed25519 key-pair in DEWIF file //! //! ``` -//! use dup_crypto::dewif::write_dewif_v1_content; +//! use dup_crypto::dewif::{Currency, G1_TEST_CURRENCY, write_dewif_v1_content}; //! use dup_crypto::keys::ed25519::{KeyPairFromSaltedPasswordGenerator, SaltedPassword}; +//! use std::num::NonZeroU32; //! //! // Get user credentials (from cli prompt or gui) //! let credentials = SaltedPassword::new("user salt".to_owned(), "user password".to_owned()); @@ -31,10 +32,10 @@ //! let encryption_passphrase = "toto titi tata"; //! //! // Serialize keypair in DEWIF format -//! let dewif_content = write_dewif_v1_content(&keypair, encryption_passphrase); +//! let dewif_content = write_dewif_v1_content(Currency::from(G1_TEST_CURRENCY), &keypair, encryption_passphrase); //! //! assert_eq!( -//! "AAAAATHfJ3vTvEPcXm22NwhJtnNdGuSjikpSYIMgX96Z9xVT0y8GoIlBL1HaxaWpu0jVDfuwtCGSP9bu2pj6HGbuYVA=", +//! "AAAAARAAAAEx3yd707xD3F5ttjcISbZzXRrko4pKUmCDIF/emfcVU9MvBqCJQS9R2sWlqbtI1Q37sLQhkj/W7tqY+hxm7mFQ", //! dewif_content //! ) //! ``` @@ -42,18 +43,23 @@ //! # Read DEWIF file //! //! ``` -//! use dup_crypto::dewif::read_dewif_file_content; +//! use dup_crypto::dewif::{Currency, ExpectedCurrency, read_dewif_file_content}; //! use dup_crypto::keys::{KeyPair, Signator}; +//! use std::num::NonZeroU32; +//! use std::str::FromStr; //! //! // Get DEWIF file content (Usually from disk) -//! let dewif_file_content = "AAAAATHfJ3vTvEPcXm22NwhJtnNdGuSjikpSYIMgX96Z9xVT0y8GoIlBL1HaxaWpu0jVDfuwtCGSP9bu2pj6HGbuYVA="; +//! let dewif_file_content = "AAAAARAAAAEx3yd707xD3F5ttjcISbZzXRrko4pKUmCDIF/emfcVU9MvBqCJQS9R2sWlqbtI1Q37sLQhkj/W7tqY+hxm7mFQ"; //! //! // Get user passphrase for DEWIF decryption (from cli prompt or gui) //! let encryption_passphrase = "toto titi tata"; //! +//! // Expected currency +//! let expected_currency = ExpectedCurrency::Specific(Currency::from_str("g1-test").expect("unknown currency")); +//! //! // Read DEWIF file content //! // If the file content is correct, we get a key-pair iterator. -//! let mut key_pair_iter = read_dewif_file_content(dewif_file_content, encryption_passphrase)?; +//! let mut key_pair_iter = read_dewif_file_content(expected_currency, dewif_file_content, encryption_passphrase)?; //! //! // Get first key-pair //! let key_pair = key_pair_iter @@ -81,9 +87,11 @@ //! ``` //! +mod currency; mod read; mod write; +pub use currency::{Currency, ExpectedCurrency, G1_CURRENCY, G1_TEST_CURRENCY}; pub use read::{read_dewif_file_content, DewifReadError}; pub use write::{write_dewif_v1_content, write_dewif_v2_content}; @@ -92,17 +100,17 @@ use crate::seeds::Seed32; use arrayvec::ArrayVec; use unwrap::unwrap; -const VERSION_BYTES: usize = 4; +const UNENCRYPTED_BYTES_LEN: usize = 8; // v1 static VERSION_V1: &[u8] = &[0, 0, 0, 1]; -const V1_BYTES_LEN: usize = 68; +const V1_BYTES_LEN: usize = 72; const V1_ENCRYPTED_BYTES_LEN: usize = 64; const V1_AES_BLOCKS_COUNT: usize = 4; // v2 static VERSION_V2: &[u8] = &[0, 0, 0, 2]; -const V2_BYTES_LEN: usize = 132; +const V2_BYTES_LEN: usize = 136; const V2_ENCRYPTED_BYTES_LEN: usize = 128; fn gen_aes_seed(passphrase: &str) -> Seed32 { @@ -133,11 +141,13 @@ mod tests { #[test] fn dewif_v1() { let written_keypair = KeyPairFromSeed32Generator::generate(Seed32::new([0u8; 32])); + let currency = Currency::from(G1_TEST_CURRENCY); - let dewif_content = write_dewif_v1_content(&written_keypair, "toto"); + let dewif_content = write_dewif_v1_content(currency, &written_keypair, "toto"); - let mut keypairs_iter = read_dewif_file_content(&dewif_content, "toto") - .expect("dewif content must be readed successfully"); + let mut keypairs_iter = + read_dewif_file_content(ExpectedCurrency::Specific(currency), &dewif_content, "toto") + .expect("dewif content must be readed successfully"); let keypair_read = keypairs_iter.next().expect("Must read one keypair"); assert_eq!(KeyPairEnum::Ed25519(written_keypair), keypair_read,) @@ -146,15 +156,16 @@ mod tests { #[test] fn dewif_v1_corrupted() -> Result<(), ()> { let written_keypair = KeyPairFromSeed32Generator::generate(Seed32::new([0u8; 32])); + let currency = Currency::from(G1_TEST_CURRENCY); - let mut dewif_content = write_dewif_v1_content(&written_keypair, "toto"); + let mut dewif_content = write_dewif_v1_content(currency, &written_keypair, "toto"); // Corrupt one byte in dewif_content let dewif_bytes_mut = unsafe { dewif_content.as_bytes_mut() }; dewif_bytes_mut[13] = 0x52; if let Err(DewifReadError::CorruptedContent) = - read_dewif_file_content(&dewif_content, "toto") + read_dewif_file_content(ExpectedCurrency::Specific(currency), &dewif_content, "toto") { Ok(()) } else { @@ -166,11 +177,14 @@ mod tests { fn dewif_v2() { let written_keypair1 = KeyPairFromSeed32Generator::generate(Seed32::new([0u8; 32])); let written_keypair2 = KeyPairFromSeed32Generator::generate(Seed32::new([1u8; 32])); + let currency = Currency::from(G1_TEST_CURRENCY); - let dewif_content = write_dewif_v2_content(&written_keypair1, &written_keypair2, "toto"); + let dewif_content = + write_dewif_v2_content(currency, &written_keypair1, &written_keypair2, "toto"); - let mut keypairs_iter = read_dewif_file_content(&dewif_content, "toto") - .expect("dewif content must be readed successfully"); + let mut keypairs_iter = + read_dewif_file_content(ExpectedCurrency::Specific(currency), &dewif_content, "toto") + .expect("dewif content must be readed successfully"); let keypair1_read = keypairs_iter.next().expect("Must read one keypair"); let keypair2_read = keypairs_iter.next().expect("Must read one keypair"); @@ -182,16 +196,17 @@ mod tests { fn dewif_v2_corrupted() -> Result<(), ()> { let written_keypair1 = KeyPairFromSeed32Generator::generate(Seed32::new([0u8; 32])); let written_keypair2 = KeyPairFromSeed32Generator::generate(Seed32::new([1u8; 32])); + let currency = Currency::from(G1_TEST_CURRENCY); let mut dewif_content = - write_dewif_v2_content(&written_keypair1, &written_keypair2, "toto"); + write_dewif_v2_content(currency, &written_keypair1, &written_keypair2, "toto"); // Corrupt one byte in dewif_content let dewif_bytes_mut = unsafe { dewif_content.as_bytes_mut() }; dewif_bytes_mut[13] = 0x52; if let Err(DewifReadError::CorruptedContent) = - read_dewif_file_content(&dewif_content, "toto") + read_dewif_file_content(ExpectedCurrency::Specific(currency), &dewif_content, "toto") { Ok(()) } else { diff --git a/src/dewif/currency.rs b/src/dewif/currency.rs new file mode 100644 index 0000000000000000000000000000000000000000..329550ed00bf33d806c7e9535b54dac69cc2c9d5 --- /dev/null +++ b/src/dewif/currency.rs @@ -0,0 +1,155 @@ +// Copyright (C) 2020 Éloïs SANCHEZ. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see <https://www.gnu.org/licenses/>. + +//! Define DEWIF currency field + +use std::fmt::Display; +use std::num::NonZeroU32; +use std::str::FromStr; + +/// Ğ1 currency +pub const G1_CURRENCY: u32 = 1; +const G1_CURRENCY_STR: &str = "g1"; + +/// Ğ1-Test currency +pub const G1_TEST_CURRENCY: u32 = 268_435_457; +const G1_TEST_CURRENCY_STR: &str = "g1-test"; + +#[derive(Copy, Clone, Debug, PartialEq)] +/// Expected DEWIF currency +pub enum ExpectedCurrency { + /// Expected any currency (no limitation) + Any, + /// Expected specific currency + Specific(Currency), +} + +impl ExpectedCurrency { + pub(crate) fn is_valid(self, currency: Currency) -> bool { + match self { + Self::Any => true, + Self::Specific(expected_currency) => expected_currency == currency, + } + } +} + +impl Display for ExpectedCurrency { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + match self { + Self::Any => write!(f, "Any"), + Self::Specific(expected_currency) => expected_currency.fmt(f), + } + } +} + +/// DEWIF currency +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct Currency(Option<NonZeroU32>); + +impl Currency { + /// None currency + pub fn none() -> Self { + Currency(None) + } +} + +impl Display for Currency { + fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> { + if let Some(currency_code) = self.0 { + match currency_code.get() { + G1_CURRENCY => write!(f, "{}", G1_CURRENCY_STR), + G1_TEST_CURRENCY => write!(f, "{}", G1_TEST_CURRENCY_STR), + other => write!(f, "{}", other), + } + } else { + write!(f, "None") + } + } +} + +impl From<u32> for Currency { + fn from(source: u32) -> Self { + Self(NonZeroU32::new(source)) + } +} + +impl Into<u32> for Currency { + fn into(self) -> u32 { + if let Some(currency_code) = self.0 { + currency_code.get() + } else { + 0u32 + } + } +} + +/// Unknown currency name +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct UnknownCurrencyName; + +impl FromStr for Currency { + type Err = UnknownCurrencyName; + + fn from_str(source: &str) -> Result<Self, Self::Err> { + match source { + "" => Ok(Currency(None)), + G1_CURRENCY_STR => Ok(Currency(NonZeroU32::new(G1_CURRENCY))), + G1_TEST_CURRENCY_STR => Ok(Currency(NonZeroU32::new(G1_TEST_CURRENCY))), + _ => Err(UnknownCurrencyName), + } + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn display_expected_currency() { + assert_eq!( + "None", + &format!("{}", ExpectedCurrency::Specific(Currency::from(0))), + ); + assert_eq!("Any", &format!("{}", ExpectedCurrency::Any)); + } + + #[test] + fn display_currency() { + assert_eq!(G1_CURRENCY_STR, &format!("{}", Currency::from(G1_CURRENCY)),); + assert_eq!( + G1_TEST_CURRENCY_STR, + &format!("{}", Currency::from(G1_TEST_CURRENCY)), + ); + assert_eq!("42", &format!("{}", Currency::from(42)),); + assert_eq!("None", &format!("{}", Currency::from(0)),); + } + + #[test] + fn currency_from_str() { + assert_eq!( + Currency::from(G1_CURRENCY), + Currency::from_str(G1_CURRENCY_STR).expect("unknown currency"), + ); + assert_eq!( + Currency::from(G1_TEST_CURRENCY), + Currency::from_str(G1_TEST_CURRENCY_STR).expect("unknown currency"), + ); + assert_eq!( + Err(UnknownCurrencyName), + Currency::from_str("unknown currency"), + ); + } +} diff --git a/src/dewif/read.rs b/src/dewif/read.rs index ce94a98e9bcb3caf185276fb15e3b2f6b2d68c63..aa671d3a6aebbb286f3733d027f4c3b451658852 100644 --- a/src/dewif/read.rs +++ b/src/dewif/read.rs @@ -15,6 +15,7 @@ //! Read [DEWIF](https://git.duniter.org/nodes/common/doc/blob/dewif/rfc/0013_Duniter_Encrypted_Wallet_Import_Format.md) file content +use super::{Currency, ExpectedCurrency}; use crate::keys::ed25519::{KeyPairFromSeed32Generator, PublicKey, PUBKEY_SIZE_IN_BYTES}; use crate::keys::KeyPairEnum; use crate::seeds::Seed32; @@ -32,22 +33,30 @@ type KeyPairsIter = arrayvec::IntoIter<[KeyPairEnum; MAX_KEYPAIRS_COUNT]>; #[derive(Clone, Debug, Error)] pub enum DewifReadError { /// DEWIF file content is corrupted - #[error("DEWIF file content is corrupted")] + #[error("DEWIF file content is corrupted.")] CorruptedContent, /// Invalid base 64 string - #[error("Invalid base 64 string: {0}")] + #[error("Invalid base 64 string: {0}.")] InvalidBase64Str(base64::DecodeError), /// Invalid format - #[error("Invalid format")] + #[error("Invalid format.")] InvalidFormat, /// Too short content - #[error("Too short content")] + #[error("Too short content.")] TooShortContent, /// Too long content - #[error("Too long content")] + #[error("Too long content.")] TooLongContent, + /// Unexpected currency + #[error("Unexpected currency '{actual}', expected: '{expected}'.")] + UnexpectedCurrency { + /// Expected currency + expected: ExpectedCurrency, + /// Actual currency + actual: Currency, + }, /// Unsupported version - #[error("Version {actual:?} is not supported. Supported versions: [1, 2].")] + #[error("Version {actual} is not supported. Supported versions: [1, 2].")] UnsupportedVersion { /// Actual version actual: u32, @@ -56,24 +65,33 @@ pub enum DewifReadError { /// read dewif file content with user passphrase pub fn read_dewif_file_content( + expected_currency: ExpectedCurrency, file_content: &str, passphrase: &str, ) -> Result<impl Iterator<Item = KeyPairEnum>, DewifReadError> { let mut bytes = base64::decode(file_content).map_err(DewifReadError::InvalidBase64Str)?; - if bytes.len() < 4 { + if bytes.len() < 8 { return Err(DewifReadError::TooShortContent); } let version = byteorder::BigEndian::read_u32(&bytes[0..4]); + let currency = Currency::from(byteorder::BigEndian::read_u32(&bytes[4..8])); + + if !expected_currency.is_valid(currency) { + return Err(DewifReadError::UnexpectedCurrency { + expected: expected_currency, + actual: currency, + }); + } match version { 1 => Ok({ let mut array_keypairs = KeyPairsArray::new(); - array_keypairs.push(read_dewif_v1(&mut bytes[4..], passphrase)?); + array_keypairs.push(read_dewif_v1(&mut bytes[8..], passphrase)?); array_keypairs.into_iter() }), - 2 => read_dewif_v2(&mut bytes[4..], passphrase), + 2 => read_dewif_v2(&mut bytes[8..], passphrase), other_version => Err(DewifReadError::UnsupportedVersion { actual: other_version, }), @@ -142,7 +160,8 @@ mod tests { #[test] fn read_unsupported_version() -> Result<(), ()> { if let Err(DewifReadError::UnsupportedVersion { .. }) = read_dewif_file_content( - "ABAAAfKjMzOFfhwgypF3mAx0QDXyozMzhX4cIMqRd5gMdEA1WZwQjCR49iZDK2QhYfdTbPz9AGB01edt4iRSzdTp3c4=", + ExpectedCurrency::Any, + "ABAAAb30ng3kI9QGMbR7TYCqPhS99J4N5CPUBjG0e02Aqj4U1UmOemI6pweNm1Ab1AR4V6ZWFnwkkp9QPxppVeMv7aaLWdop", "toto" ) { Ok(()) @@ -153,7 +172,9 @@ mod tests { #[test] fn read_too_short_content() -> Result<(), ()> { - if let Err(DewifReadError::TooShortContent) = read_dewif_file_content("AAA", "toto") { + if let Err(DewifReadError::TooShortContent) = + read_dewif_file_content(ExpectedCurrency::Any, "AAA", "toto") + { Ok(()) } else { panic!("Read must be fail with error TooShortContent.") @@ -161,19 +182,39 @@ mod tests { } #[test] - fn tmp() { + fn read_unexpected_currency() -> Result<(), ()> { + if let Err(DewifReadError::UnexpectedCurrency { .. }) = read_dewif_file_content( + ExpectedCurrency::Specific(Currency::from(42)), + "AAAAARAAAAEx3yd707xD3F5ttjcISbZzXRrko4pKUmCDIF/emfcVU9MvBqCJQS9R2sWlqbtI1Q37sLQhkj/W7tqY+hxm7mFQ", + "toto titi tata" + ) { + Ok(()) + } else { + panic!("Read must be fail with error UnexpectedCurrency.") + } + } + + #[test] + fn read_ok() { + use crate::dewif::Currency; use crate::keys::{KeyPair, Signator}; + use std::str::FromStr; // Get DEWIF file content (Usually from disk) - let dewif_file_content = "AAAAATHfJ3vTvEPcXm22NwhJtnNdGuSjikpSYIMgX96Z9xVT0y8GoIlBL1HaxaWpu0jVDfuwtCGSP9bu2pj6HGbuYVA="; + let dewif_file_content = "AAAAARAAAAEx3yd707xD3F5ttjcISbZzXRrko4pKUmCDIF/emfcVU9MvBqCJQS9R2sWlqbtI1Q37sLQhkj/W7tqY+hxm7mFQ"; // Get user passphrase for DEWIF decryption (from cli prompt or gui) let encryption_passphrase = "toto titi tata"; + // Expected currency + let expected_currency = + ExpectedCurrency::Specific(Currency::from_str("g1-test").expect("unknown currency")); + // Read DEWIF file content // If the file content is correct, we get a key-pair iterator. - let mut key_pair_iter = read_dewif_file_content(dewif_file_content, encryption_passphrase) - .expect("invalid DEWIF file."); + let mut key_pair_iter = + read_dewif_file_content(expected_currency, dewif_file_content, encryption_passphrase) + .expect("invalid DEWIF file"); // Get first key-pair let key_pair = key_pair_iter diff --git a/src/dewif/write.rs b/src/dewif/write.rs index f009f37e968b53d299e570d4249d178fcc5692f5..1e6161a9ccb0ff453ed8a681bddf3e61f2f7976c 100644 --- a/src/dewif/write.rs +++ b/src/dewif/write.rs @@ -15,38 +15,52 @@ //! Write [DEWIF](https://git.duniter.org/nodes/common/doc/blob/dewif/rfc/0013_Duniter_Encrypted_Wallet_Import_Format.md) file content +use super::Currency; use crate::keys::ed25519::Ed25519KeyPair; use arrayvec::ArrayVec; use unwrap::unwrap; /// Write dewif v1 file content with user passphrase -pub fn write_dewif_v1_content(keypair: &Ed25519KeyPair, passphrase: &str) -> String { +pub fn write_dewif_v1_content( + currency: Currency, + keypair: &Ed25519KeyPair, + passphrase: &str, +) -> String { let mut bytes = ArrayVec::<[u8; super::V1_BYTES_LEN]>::new(); unwrap!(bytes.try_extend_from_slice(super::VERSION_V1)); + let currency_code: u32 = currency.into(); + unwrap!(bytes.try_extend_from_slice(¤cy_code.to_be_bytes())); unwrap!(bytes.try_extend_from_slice(keypair.seed().as_ref())); unwrap!(bytes.try_extend_from_slice(keypair.pubkey().as_ref())); let cipher = crate::aes256::new_cipher(super::gen_aes_seed(passphrase)); - crate::aes256::encrypt::encrypt_n_blocks(&cipher, &mut bytes[4..], super::V1_AES_BLOCKS_COUNT); + crate::aes256::encrypt::encrypt_n_blocks( + &cipher, + &mut bytes[super::UNENCRYPTED_BYTES_LEN..], + super::V1_AES_BLOCKS_COUNT, + ); base64::encode(bytes.as_ref()) } /// Write dewif v2 file content with user passphrase pub fn write_dewif_v2_content( + currency: Currency, keypair1: &Ed25519KeyPair, keypair2: &Ed25519KeyPair, passphrase: &str, ) -> String { let mut bytes = ArrayVec::<[u8; super::V2_BYTES_LEN]>::new(); unwrap!(bytes.try_extend_from_slice(super::VERSION_V2)); + let currency_code: u32 = currency.into(); + unwrap!(bytes.try_extend_from_slice(¤cy_code.to_be_bytes())); unwrap!(bytes.try_extend_from_slice(keypair1.seed().as_ref())); unwrap!(bytes.try_extend_from_slice(keypair1.pubkey().as_ref())); unwrap!(bytes.try_extend_from_slice(keypair2.seed().as_ref())); unwrap!(bytes.try_extend_from_slice(keypair2.pubkey().as_ref())); let cipher = crate::aes256::new_cipher(super::gen_aes_seed(passphrase)); - crate::aes256::encrypt::encrypt_8_blocks(&cipher, &mut bytes[super::VERSION_BYTES..]); + crate::aes256::encrypt::encrypt_8_blocks(&cipher, &mut bytes[super::UNENCRYPTED_BYTES_LEN..]); base64::encode(bytes.as_ref()) } @@ -57,15 +71,44 @@ mod tests { use super::*; use crate::keys::ed25519::KeyPairFromSeed32Generator; use crate::seeds::Seed32; + use std::str::FromStr; #[test] fn write_dewif_v1() { let keypair = KeyPairFromSeed32Generator::generate(Seed32::new([0u8; 32])); - let dewif_content = write_dewif_v1_content(&keypair, "toto"); + let dewif_content = write_dewif_v1_content(Currency::none(), &keypair, "toto"); println!("{}", dewif_content); assert_eq!( - "AAAAAb30ng3kI9QGMbR7TYCqPhS99J4N5CPUBjG0e02Aqj4UElionaHOt0kv+eaWgGSGkrP1LQfuwivuvg7+9n0gd18=", + "AAAAAQAAAAC99J4N5CPUBjG0e02Aqj4UvfSeDeQj1AYxtHtNgKo+FBJYqJ2hzrdJL/nmloBkhpKz9S0H7sIr7r4O/vZ9IHdf", + dewif_content + ) + } + + #[test] + fn write_ok() { + use crate::dewif::write_dewif_v1_content; + use crate::keys::ed25519::{KeyPairFromSaltedPasswordGenerator, SaltedPassword}; + + // Get user credentials (from cli prompt or gui) + let credentials = SaltedPassword::new("user salt".to_owned(), "user password".to_owned()); + + // Generate ed25519 keypair + let keypair = + KeyPairFromSaltedPasswordGenerator::with_default_parameters().generate(credentials); + + // Get user passphrase for DEWIF encryption + let encryption_passphrase = "toto titi tata"; + + // Expected currency + let expected_currency = Currency::from_str("g1-test").expect("unknown currency"); + + // Serialize keypair in DEWIF format + let dewif_content = + write_dewif_v1_content(expected_currency, &keypair, encryption_passphrase); + + assert_eq!( + "AAAAARAAAAEx3yd707xD3F5ttjcISbZzXRrko4pKUmCDIF/emfcVU9MvBqCJQS9R2sWlqbtI1Q37sLQhkj/W7tqY+hxm7mFQ", dewif_content ) }