diff --git a/Cargo.lock b/Cargo.lock index a49f8fc4208a2..b389d45c481a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2281,7 +2281,7 @@ dependencies = [ "log", "parity-scale-codec", "rand 0.8.5", - "rand_pcg", + "rand_pcg 0.3.1", "sc-block-builder", "sc-cli", "sc-client-api", @@ -5851,7 +5851,7 @@ dependencies = [ "parity-scale-codec", "pretty_assertions", "rand 0.8.5", - "rand_pcg", + "rand_pcg 0.3.1", "scale-info", "serde", "smallvec", @@ -7703,6 +7703,7 @@ dependencies = [ "rand_chacha 0.2.2", "rand_core 0.5.1", "rand_hc", + "rand_pcg 0.2.1", ] [[package]] @@ -7773,6 +7774,15 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rand_pcg" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16abd0c1b639e9eb4d7c50c0b8100b0d0f849be2349829c740fe8e6eb4816429" +dependencies = [ + "rand_core 0.5.1", +] + [[package]] name = "rand_pcg" version = "0.3.1" @@ -9488,7 +9498,7 @@ dependencies = [ "libc", "log", "rand 0.8.5", - "rand_pcg", + "rand_pcg 0.3.1", "regex", "sc-telemetry", "serde", @@ -10350,17 +10360,21 @@ dependencies = [ name = "sp-core" version = "7.0.0" dependencies = [ + "aes-gcm 0.10.1", "array-bytes", "bitflags", "blake2", "bounded-collections", "bs58", "criterion", + "curve25519-dalek 3.2.0", "dyn-clonable", + "ed25519-dalek", "ed25519-zebra", "futures", "hash-db", "hash256-std-hasher", + "hkdf", "impl-serde", "lazy_static", "libsecp256k1", @@ -10369,7 +10383,7 @@ dependencies = [ "parity-scale-codec", "parking_lot 0.12.1", "primitive-types", - "rand 0.8.5", + "rand 0.7.3", "regex", "scale-info", "schnorrkel", @@ -10377,6 +10391,7 @@ dependencies = [ "secrecy", "serde", "serde_json", + "sha2 0.10.6", "sp-core-hashing", "sp-core-hashing-proc-macro", "sp-debug-derive", @@ -10389,6 +10404,7 @@ dependencies = [ "substrate-bip39", "thiserror", "tiny-bip39", + "x25519-dalek 1.1.1", "zeroize", ] @@ -13176,9 +13192,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.5.7" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" dependencies = [ "zeroize_derive", ] diff --git a/primitives/core/Cargo.toml b/primitives/core/Cargo.toml index 9b253fd154675..1360658616544 100644 --- a/primitives/core/Cargo.toml +++ b/primitives/core/Cargo.toml @@ -23,7 +23,7 @@ impl-serde = { version = "0.4.0", optional = true } hash-db = { version = "0.16.0", default-features = false } hash256-std-hasher = { version = "0.15.2", default-features = false } bs58 = { version = "0.4.0", default-features = false, optional = true } -rand = { version = "0.8.5", features = ["small_rng"], optional = true } +rand = { version = "0.7.2", features = ["small_rng"], optional = true } substrate-bip39 = { version = "0.4.4", optional = true } tiny-bip39 = { version = "1.0.0", optional = true } regex = { version = "1.6.0", optional = true } @@ -40,6 +40,14 @@ dyn-clonable = { version = "0.9.0", optional = true } thiserror = { version = "1.0.30", optional = true } bitflags = "1.3" +# ECIES dependencies +ed25519-dalek = { version = "1.0", optional = true } +x25519-dalek = { version = "1.1.0", optional = true } +curve25519-dalek = { version = "3.2", optional = true } +aes-gcm = { version = "0.10", optional = true } +hkdf = { version = "0.12.0", optional = true } +sha2 = { version = "0.10.0", optional = true } + # full crypto array-bytes = { version = "4.1", optional = true } ed25519-zebra = { version = "3.1.0", default-features = false, optional = true } @@ -54,7 +62,6 @@ sp-runtime-interface = { version = "7.0.0", default-features = false, path = ".. [dev-dependencies] sp-serializer = { version = "4.0.0-dev", path = "../serializer" } -rand = "0.8.5" criterion = "0.4.0" serde_json = "1.0" sp-core-hashing-proc-macro = { version = "5.0.0", path = "./hashing/proc-macro" } @@ -110,6 +117,13 @@ std = [ "futures/thread-pool", "libsecp256k1/std", "dyn-clonable", + + "ed25519-dalek", + "x25519-dalek", + "curve25519-dalek", + "aes-gcm", + "hkdf", + "sha2", ] # This feature enables all crypto primitives for `no_std` builds like microcontrollers diff --git a/primitives/core/src/ecies.rs b/primitives/core/src/ecies.rs new file mode 100644 index 0000000000000..2d9cb74b64f0b --- /dev/null +++ b/primitives/core/src/ecies.rs @@ -0,0 +1,176 @@ +// This file is part of Substrate. + +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// tag::description[] +//! Encryption scheme that uses x25519 key exchange. +// end::description[] + +use crate::crypto::Pair; +use aes_gcm::{aead::Aead, KeyInit}; +use rand::{rngs::OsRng, RngCore}; +use sha2::Digest; + +type SecretKey = x25519_dalek::StaticSecret; +type PublicKey = x25519_dalek::PublicKey; + +/// Encryption or decryption error. +#[derive(Debug, PartialEq, Eq, thiserror::Error)] +pub enum Error { + /// Generic AES encryption error. + #[error("Encryption error")] + Encryption, + /// Generic AES decryption error. + #[error("Decryption error")] + Decryption, + /// Error reading key data. Not enough data in the buffer. + #[error("Bad cypher text")] + BadData, +} + +const NONCE_LEN: usize = 12; +const PK_LEN: usize = 32; +const AES_KEY_LEN: usize = 32; + +fn aes_encrypt(key: &[u8; AES_KEY_LEN], nonce: &[u8], plaintext: &[u8]) -> Result, Error> { + let enc = aes_gcm::Aes256Gcm::new(key.into()); + + enc.encrypt(nonce.into(), aes_gcm::aead::Payload { msg: plaintext, aad: b"" }) + .map_err(|_| Error::Encryption) +} + +fn aes_decrypt(key: &[u8; AES_KEY_LEN], nonce: &[u8], ciphertext: &[u8]) -> Result, Error> { + let dec = aes_gcm::Aes256Gcm::new(key.into()); + dec.decrypt(nonce.into(), aes_gcm::aead::Payload { msg: ciphertext, aad: b"" }) + .map_err(|_| Error::Decryption) +} + +fn kdf(shared_secret: &[u8]) -> [u8; AES_KEY_LEN] { + let hkdf = hkdf::Hkdf::::new(None, shared_secret); + let mut aes_key = [0u8; AES_KEY_LEN]; + hkdf.expand(b"", &mut aes_key) + .expect("There's always enough data for derivation."); + aes_key +} + +/// Encrypt `plaintext` with the given public key. Decryption can be performed with the matching +/// secret key. +pub fn encrypt(pk: &PublicKey, plaintext: &[u8]) -> Result, Error> { + let ephemeral_sk = x25519_dalek::StaticSecret::new(OsRng); + let ephemeral_pk = x25519_dalek::PublicKey::from(&ephemeral_sk); + + let mut shared_secret = ephemeral_sk.diffie_hellman(pk).to_bytes().to_vec(); + shared_secret.extend_from_slice(ephemeral_pk.as_bytes()); + + let aes_key = kdf(&shared_secret); + + let mut nonce = [0u8; NONCE_LEN]; + OsRng.fill_bytes(&mut nonce); + let ciphertext = aes_encrypt(&aes_key, &nonce, plaintext)?; + + let mut out = Vec::with_capacity(ciphertext.len() + PK_LEN + NONCE_LEN); + out.extend_from_slice(ephemeral_pk.as_bytes()); + out.extend_from_slice(nonce.as_slice()); + out.extend_from_slice(ciphertext.as_slice()); + + Ok(out) +} + +/// Encrypt `plaintext` with the given public key. Decryption can be performed with the matching +/// secret key. +pub fn encrypt_ed25519(pk: &crate::ed25519::Public, plaintext: &[u8]) -> Result, Error> { + let ed25519 = curve25519_dalek::edwards::CompressedEdwardsY(pk.0); + let x25519 = ed25519 + .decompress() + .ok_or(Error::BadData) + .expect("The compressed point is invalid") + .to_montgomery(); + let montgomery = x25519_dalek::PublicKey::from(x25519.to_bytes()); + encrypt(&montgomery, plaintext) +} + +/// Decrypt with the secret key. +pub fn decrypt(sk: &SecretKey, encrypted: &[u8]) -> Result, Error> { + if encrypted.len() < PK_LEN + NONCE_LEN { + return Err(Error::BadData) + } + let mut ephemeral_pk: [u8; PK_LEN] = Default::default(); + ephemeral_pk.copy_from_slice(&encrypted[0..PK_LEN]); + let ephemeral_pk = PublicKey::from(ephemeral_pk); + + let mut shared_secret = sk.diffie_hellman(&ephemeral_pk).to_bytes().to_vec(); + shared_secret.extend_from_slice(ephemeral_pk.as_bytes()); + + let aes_key = kdf(&shared_secret); + + let nonce = &encrypted[PK_LEN..PK_LEN + NONCE_LEN]; + aes_decrypt(&aes_key, &nonce, &encrypted[PK_LEN + NONCE_LEN..]) +} + +/// Decrypt with the secret key. +pub fn decrypt_ed25519(pair: &crate::ed25519::Pair, encrypted: &[u8]) -> Result, Error> { + let raw = pair.to_raw_vec(); + let hash: [u8; 32] = + sha2::Sha512::digest(&raw).as_slice()[..32].try_into().expect("Hashing error"); + let secret = x25519_dalek::StaticSecret::from(hash); + decrypt(&secret, encrypted) +} + +#[cfg(test)] +mod test { + use super::*; + use crate::crypto::Pair; + use rand::rngs::OsRng; + + #[test] + fn basic_x25519_encryption() { + let sk = SecretKey::new(OsRng); + let pk = PublicKey::from(&sk); + + let plain_message = b"An important secret message"; + let encrypted = encrypt(&pk, plain_message).unwrap(); + + let decrypted = decrypt(&sk, &encrypted).unwrap(); + assert_eq!(plain_message, decrypted.as_slice()); + } + + #[test] + fn basic_ed25519_encryption() { + let (pair, _) = crate::ed25519::Pair::generate(); + let pk = pair.into(); + + let plain_message = b"An important secret message"; + let encrypted = encrypt_ed25519(&pk, plain_message).unwrap(); + + let decrypted = decrypt_ed25519(&pair, &encrypted).unwrap(); + assert_eq!(plain_message, decrypted.as_slice()); + } + + #[test] + fn fails_on_bad_data() { + let sk = SecretKey::new(OsRng); + let pk = PublicKey::from(&sk); + + let plain_message = b"An important secret message"; + let encrypted = encrypt(&pk, plain_message).unwrap(); + + assert_eq!(decrypt(&sk, &[]), Err(Error::BadData)); + assert_eq!( + decrypt(&sk, &encrypted[0..super::PK_LEN + super::NONCE_LEN - 1]), + Err(Error::BadData) + ); + } +} diff --git a/primitives/core/src/lib.rs b/primitives/core/src/lib.rs index efccd0378e95a..96e35be987180 100644 --- a/primitives/core/src/lib.rs +++ b/primitives/core/src/lib.rs @@ -56,6 +56,8 @@ pub mod hexdisplay; pub mod defer; pub mod ecdsa; +#[cfg(feature = "std")] +pub mod ecies; pub mod ed25519; pub mod hash; #[cfg(feature = "std")]