diff --git a/Cargo.lock b/Cargo.lock index 5b071e7547513ba25f339c3c80c3ea962264e89e..2070f461a5bc3eedf1eb52b934f7ab464356b699 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,12 @@ version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7825f6833612eb2414095684fcf6c635becf3ce97fe48cf6421321e93bfbd53c" +[[package]] +name = "arrayvec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" + [[package]] name = "base64" version = "0.11.0" @@ -144,6 +150,7 @@ name = "dup-crypto" version = "0.8.0" dependencies = [ "aes", + "arrayvec", "base64", "bincode", "bs58", diff --git a/Cargo.toml b/Cargo.toml index fdad2fef16838d265192b173e9ada262992df257..877d1f3d4bc08b711a6ef3fe0c69ca971b7f5d3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ path = "src/lib.rs" [dependencies] aes = { version = "0.3.2", optional = true } +arrayvec = { version = "0.5.1", features = ["array-sizes-33-128", "array-sizes-129-255"], optional = true } base64 = "0.11.0" bs58 = "0.3.0" byteorder = "1.3.2" @@ -28,6 +29,8 @@ zeroize = { version = "1.1.0", features = ["zeroize_derive"] } bincode = "1.2.0" [features] -default = ["ser"] +default = ["dewip", "ser"] + aes256 = ["aes"] +dewip = ["aes256", "arrayvec"] ser = ["serde"] diff --git a/src/dewip.rs b/src/dewip.rs new file mode 100644 index 0000000000000000000000000000000000000000..74babd59b015604f993ba8f0cdf9771cecc1c631 --- /dev/null +++ b/src/dewip.rs @@ -0,0 +1,204 @@ +// 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/>. + +//! Handle [DEWIP](https://git.duniter.org/nodes/common/doc/blob/dewif/rfc/0013_Duniter_Encrypted_Wallet_Import_Format.md) format +//! +//! # Write ed25519 key-pair in DEWIF file +//! +//! ``` +//! use dup_crypto::dewip::write_dewif_v1_content; +//! use dup_crypto::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"; +//! +//! // Serialize keypair in DEWIF format +//! let dewif_content = write_dewif_v1_content(&keypair, encryption_passphrase); +//! +//! assert_eq!( +//! "AAAAATHfJ3vTvEPcXm22NwhJtnNdGuSjikpSYIMgX96Z9xVT0y8GoIlBL1HaxaWpu0jVDfuwtCGSP9bu2pj6HGbuYVA=", +//! dewif_content +//! ) +//! ``` +//! +//! # Read DEWIF file +//! +//! ``` +//! use dup_crypto::dewip::read_dewip_file_content; +//! use dup_crypto::keys::{KeyPair, Signator}; +//! +//! // Get DEWIP file content (Usually from disk) +//! let dewip_file_content = "AAAAATHfJ3vTvEPcXm22NwhJtnNdGuSjikpSYIMgX96Z9xVT0y8GoIlBL1HaxaWpu0jVDfuwtCGSP9bu2pj6HGbuYVA="; +//! +//! // Get user passphrase for DEWIF decryption (from cli prompt or gui) +//! let encryption_passphrase = "toto titi tata"; +//! +//! // Read DEWIP file content +//! // If the file content is correct, we get a key-pair iterator. +//! let mut key_pair_iter = read_dewip_file_content(dewip_file_content, encryption_passphrase) +//! .expect("invalid DEWIF file.") +//! .into_iter(); +//! +//! // Get first key-pair +//! let key_pair = key_pair_iter +//! .next() +//! .expect("DEWIF file must contain at least one keypair"); +//! +//! assert_eq!( +//! "2cC9FrvRiN3uHHcd8S7wuureDS8CAmD5y4afEgSCLHtU", +//! &key_pair.public_key().to_string() +//! ); +//! +//! // Generate signator +//! // `Signator` is a non-copiable and non-clonable type, +//! // so only generate it when you are in the scope where you effectively sign. +//! let signator = key_pair.generate_signator(); +//! +//! // Sign a message with keypair +//! let sig = signator.sign(b"message"); +//! +//! assert_eq!( +//! "nCWl7jtCa/nCMKKnk2NJN7daVxd/ER+e1wsFbofdh/pUvDuHxFaa7S5eUMGiqPTJ4uJQOvrmF/BOfOsYIoI2Bg==", +//! &sig.to_string() +//! ) +//! ``` +//! + +mod read; +mod write; + +pub use read::{read_dewip_file_content, DewipReadError}; +pub use write::{write_dewif_v1_content, write_dewif_v2_content}; + +use crate::hashs::Hash; +use crate::seeds::Seed32; +use arrayvec::ArrayVec; +use unwrap::unwrap; + +const VERSION_BYTES: usize = 4; + +// v1 +static VERSION_V1: &[u8] = &[0, 0, 0, 1]; +const V1_BYTES_LEN: usize = 68; +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_ENCRYPTED_BYTES_LEN: usize = 128; + +fn gen_aes_seed(passphrase: &str) -> Seed32 { + let mut salt = ArrayVec::<[u8; 37]>::new(); + unwrap!(salt.try_extend_from_slice(b"dewif")); + let hash = Hash::compute(passphrase.as_bytes()); + unwrap!(salt.try_extend_from_slice(hash.as_ref())); + + let mut aes_seed_bytes = [0u8; 32]; + scrypt::scrypt( + passphrase.as_bytes(), + salt.as_ref(), + &scrypt::ScryptParams::new(12, 16, 1).expect("dev error: invalid scrypt params"), + &mut aes_seed_bytes, + ) + .expect("dev error: invalid seed len"); + Seed32::new(aes_seed_bytes) +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::keys::ed25519::KeyPairFromSeed32Generator; + use crate::keys::KeyPairEnum; + use crate::seeds::Seed32; + + #[test] + fn dewip_v1() { + let written_keypair = KeyPairFromSeed32Generator::generate(Seed32::new([0u8; 32])); + + let dewif_content = write_dewif_v1_content(&written_keypair, "toto"); + + let mut keypairs_iter = read_dewip_file_content(&dewif_content, "toto") + .expect("dewip content must be readed successfully") + .into_iter(); + let keypair_read = keypairs_iter.next().expect("Must read one keypair"); + + assert_eq!(KeyPairEnum::Ed25519(written_keypair), keypair_read,) + } + + #[test] + fn dewip_v1_corrupted() -> Result<(), ()> { + let written_keypair = KeyPairFromSeed32Generator::generate(Seed32::new([0u8; 32])); + + let mut dewif_content = write_dewif_v1_content(&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(DewipReadError::CorruptedContent) = + read_dewip_file_content(&dewif_content, "toto") + { + Ok(()) + } else { + panic!("dewif content must be corrupted.") + } + } + + #[test] + fn dewip_v2() { + let written_keypair1 = KeyPairFromSeed32Generator::generate(Seed32::new([0u8; 32])); + let written_keypair2 = KeyPairFromSeed32Generator::generate(Seed32::new([1u8; 32])); + + let dewif_content = write_dewif_v2_content(&written_keypair1, &written_keypair2, "toto"); + + let mut keypairs_iter = read_dewip_file_content(&dewif_content, "toto") + .expect("dewip content must be readed successfully") + .into_iter(); + let keypair1_read = keypairs_iter.next().expect("Must read one keypair"); + let keypair2_read = keypairs_iter.next().expect("Must read one keypair"); + + assert_eq!(KeyPairEnum::Ed25519(written_keypair1), keypair1_read,); + assert_eq!(KeyPairEnum::Ed25519(written_keypair2), keypair2_read,); + } + + #[test] + fn dewip_v2_corrupted() -> Result<(), ()> { + let written_keypair1 = KeyPairFromSeed32Generator::generate(Seed32::new([0u8; 32])); + let written_keypair2 = KeyPairFromSeed32Generator::generate(Seed32::new([1u8; 32])); + + let mut dewif_content = + write_dewif_v2_content(&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(DewipReadError::CorruptedContent) = + read_dewip_file_content(&dewif_content, "toto") + { + Ok(()) + } else { + panic!("dewif content must be corrupted.") + } + } +} diff --git a/src/dewip/read.rs b/src/dewip/read.rs new file mode 100644 index 0000000000000000000000000000000000000000..6f190a67bad62134f351c6c1067a5f0ad903c2b2 --- /dev/null +++ b/src/dewip/read.rs @@ -0,0 +1,202 @@ +// 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/>. + +//! Read [DEWIP](https://git.duniter.org/nodes/common/doc/blob/dewif/rfc/0013_Duniter_Encrypted_Wallet_Import_Format.md) file content + +use crate::keys::ed25519::{KeyPairFromSeed32Generator, PublicKey, PUBKEY_SIZE_IN_BYTES}; +use crate::keys::KeyPairEnum; +use crate::seeds::Seed32; +use arrayvec::ArrayVec; +use byteorder::ByteOrder; +use std::convert::{TryFrom, TryInto}; +use thiserror::Error; + +const MAX_KEYPAIRS_COUNT: usize = 2; + +/// Error when try to read DEWIP file content +#[derive(Clone, Debug, Error)] +pub enum DewipReadError { + /// DEWIP file content is corrupted + #[error("DEWIP file content is corrupted")] + CorruptedContent, + /// Invalid base 64 string + #[error("Invalid base 64 string: {0}")] + InvalidBase64Str(base64::DecodeError), + /// Invalid format + #[error("Invalid format")] + InvalidFormat, + /// Too short content + #[error("Too short content")] + TooShortContent, + /// Too long content + #[error("Too long content")] + TooLongContent, + /// Unsupported version + #[error("Version {actual:?} is not supported. Supported versions: [1, 2].")] + UnsupportedVersion { + /// Actual version + actual: u32, + }, +} + +/// read dewip file content with user passphrase +pub fn read_dewip_file_content( + file_content: &str, + passphrase: &str, +) -> Result<impl IntoIterator<Item = KeyPairEnum>, DewipReadError> { + let mut bytes = base64::decode(file_content).map_err(DewipReadError::InvalidBase64Str)?; + + if bytes.len() < 4 { + return Err(DewipReadError::TooShortContent); + } + + let version = byteorder::BigEndian::read_u32(&bytes[0..4]); + + match version { + 1 => Ok({ + let mut array_keypairs = ArrayVec::new(); + array_keypairs.push(read_dewip_v1(&mut bytes[4..], passphrase)?); + array_keypairs + }), + 2 => read_dewip_v2(&mut bytes[4..], passphrase), + other_version => Err(DewipReadError::UnsupportedVersion { + actual: other_version, + }), + } +} + +fn read_dewip_v1(bytes: &mut [u8], passphrase: &str) -> Result<KeyPairEnum, DewipReadError> { + match bytes.len() { + len if len < super::V1_ENCRYPTED_BYTES_LEN => return Err(DewipReadError::TooShortContent), + len if len > super::V1_ENCRYPTED_BYTES_LEN => return Err(DewipReadError::TooLongContent), + _ => (), + } + + // Decrypt bytes + let cipher = crate::aes256::new_cipher(super::gen_aes_seed(passphrase)); + crate::aes256::decrypt::decrypt_n_blocks(&cipher, bytes, super::V1_AES_BLOCKS_COUNT); + + // Get checked keypair + bytes_to_checked_keypair(bytes) +} + +fn read_dewip_v2( + bytes: &mut [u8], + passphrase: &str, +) -> Result<ArrayVec<[KeyPairEnum; MAX_KEYPAIRS_COUNT]>, DewipReadError> { + let mut array_keypairs = ArrayVec::new(); + + match bytes.len() { + len if len < super::V2_ENCRYPTED_BYTES_LEN => return Err(DewipReadError::TooShortContent), + len if len > super::V2_ENCRYPTED_BYTES_LEN => return Err(DewipReadError::TooLongContent), + _ => (), + } + + // Decrypt bytes + let cipher = crate::aes256::new_cipher(super::gen_aes_seed(passphrase)); + crate::aes256::decrypt::decrypt_8_blocks(&cipher, bytes); + + array_keypairs.push(bytes_to_checked_keypair(&bytes[..64])?); + array_keypairs.push(bytes_to_checked_keypair(&bytes[64..])?); + + Ok(array_keypairs) +} + +fn bytes_to_checked_keypair(bytes: &[u8]) -> Result<KeyPairEnum, DewipReadError> { + // Wrap bytes into Seed32 and PublicKey + let seed = Seed32::new( + (&bytes[..PUBKEY_SIZE_IN_BYTES]) + .try_into() + .expect("dev error"), + ); + let expected_pubkey = PublicKey::try_from(&bytes[PUBKEY_SIZE_IN_BYTES..]).expect("dev error"); + + // Get keypair + let keypair = KeyPairFromSeed32Generator::generate(seed); + + // Check pubkey + if keypair.pubkey() != expected_pubkey { + Err(DewipReadError::CorruptedContent) + } else { + Ok(KeyPairEnum::Ed25519(keypair)) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn read_unsupported_version() -> Result<(), ()> { + if let Err(DewipReadError::UnsupportedVersion { .. }) = read_dewip_file_content( + "ABAAAfKjMzOFfhwgypF3mAx0QDXyozMzhX4cIMqRd5gMdEA1WZwQjCR49iZDK2QhYfdTbPz9AGB01edt4iRSzdTp3c4=", + "toto" + ) { + Ok(()) + } else { + panic!("Read must be fail with error UnsupportedVersion.") + } + } + + #[test] + fn read_too_short_content() -> Result<(), ()> { + if let Err(DewipReadError::TooShortContent) = read_dewip_file_content("AAA", "toto") { + Ok(()) + } else { + panic!("Read must be fail with error TooShortContent.") + } + } + + #[test] + fn tmp() { + use crate::keys::{KeyPair, Signator}; + + // Get DEWIP file content (Usually from disk) + let dewip_file_content = "AAAAATHfJ3vTvEPcXm22NwhJtnNdGuSjikpSYIMgX96Z9xVT0y8GoIlBL1HaxaWpu0jVDfuwtCGSP9bu2pj6HGbuYVA="; + + // Get user passphrase for DEWIF decryption (from cli prompt or gui) + let encryption_passphrase = "toto titi tata"; + + // Read DEWIP file content + // If the file content is correct, we get a key-pair iterator. + let mut key_pair_iter = read_dewip_file_content(dewip_file_content, encryption_passphrase) + .expect("invalid DEWIF file.") + .into_iter(); + + // Get first key-pair + let key_pair = key_pair_iter + .next() + .expect("DEWIF file must contain at least one keypair"); + + assert_eq!( + "2cC9FrvRiN3uHHcd8S7wuureDS8CAmD5y4afEgSCLHtU", + &key_pair.public_key().to_string() + ); + + // Generate signator + // `Signator` is a non-copiable and non-clonable type, + // so only generate it when you are in the scope where you effectively sign. + let signator = key_pair.generate_signator(); + + // Sign a message with keypair + let sig = signator.sign(b"message"); + + assert_eq!( + "nCWl7jtCa/nCMKKnk2NJN7daVxd/ER+e1wsFbofdh/pUvDuHxFaa7S5eUMGiqPTJ4uJQOvrmF/BOfOsYIoI2Bg==", + &sig.to_string() + ) + } +} diff --git a/src/dewip/write.rs b/src/dewip/write.rs new file mode 100644 index 0000000000000000000000000000000000000000..4ce1408021bb602b4ea21382a50100f9ebf16ae4 --- /dev/null +++ b/src/dewip/write.rs @@ -0,0 +1,72 @@ +// 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/>. + +//! Write [DEWIP](https://git.duniter.org/nodes/common/doc/blob/dewif/rfc/0013_Duniter_Encrypted_Wallet_Import_Format.md) file content + +use crate::keys::ed25519::Ed25519KeyPair; +use arrayvec::ArrayVec; +use unwrap::unwrap; + +/// Write dewip v1 file content with user passphrase +pub fn write_dewif_v1_content(keypair: &Ed25519KeyPair, passphrase: &str) -> String { + let mut bytes = ArrayVec::<[u8; super::V1_BYTES_LEN]>::new(); + unwrap!(bytes.try_extend_from_slice(super::VERSION_V1)); + 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); + + base64::encode(bytes.as_ref()) +} + +/// Write dewip v2 file content with user passphrase +pub fn write_dewif_v2_content( + 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)); + 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..]); + + base64::encode(bytes.as_ref()) +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::keys::ed25519::KeyPairFromSeed32Generator; + use crate::seeds::Seed32; + + #[test] + fn write_dewif_v1() { + let keypair = KeyPairFromSeed32Generator::generate(Seed32::new([0u8; 32])); + + let dewif_content = write_dewif_v1_content(&keypair, "toto"); + println!("{}", dewif_content); + assert_eq!( + "AAAAAb30ng3kI9QGMbR7TYCqPhS99J4N5CPUBjG0e02Aqj4UElionaHOt0kv+eaWgGSGkrP1LQfuwivuvg7+9n0gd18=", + dewif_content + ) + } +} diff --git a/src/keys/ed25519.rs b/src/keys/ed25519.rs index f5018fbaa2d3499bea703de69466bc8f1487b5b9..c2697484d3b4e660e2959aa20ed3eef8d185ad92 100644 --- a/src/keys/ed25519.rs +++ b/src/keys/ed25519.rs @@ -286,6 +286,9 @@ impl Ed25519KeyPair { pub fn pubkey(&self) -> PublicKey { self.pubkey } + pub(crate) fn seed(&self) -> &Seed32 { + &self.seed + } } impl Display for Ed25519KeyPair { diff --git a/src/lib.rs b/src/lib.rs index 6bed05f234db5e19fbc16b4f78b37aaa0c5fed49..f1204d24c6f172e899d5aa57d2e31e2093955d8e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,7 +22,6 @@ missing_copy_implementations, trivial_casts, trivial_numeric_casts, - unsafe_code, unstable_features, unused_import_braces, unused_qualifications @@ -32,6 +31,8 @@ #[cfg(feature = "aes256")] pub mod aes256; pub mod bases; +#[cfg(feature = "dewip")] +pub mod dewip; pub mod hashs; pub mod keys; pub mod rand;