Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Commit

Permalink
Added ECIES encryption
Browse files Browse the repository at this point in the history
  • Loading branch information
arkpar committed Apr 5, 2023
1 parent 9c92e49 commit 8f6c78c
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 8 deletions.
28 changes: 22 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 16 additions & 2 deletions primitives/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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 }
Expand All @@ -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" }
Expand Down Expand Up @@ -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
Expand Down
176 changes: 176 additions & 0 deletions primitives/core/src/ecies.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<u8>, 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<Vec<u8>, 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::<sha2::Sha256>::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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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<Vec<u8>, 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)
);
}
}
2 changes: 2 additions & 0 deletions primitives/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down

0 comments on commit 8f6c78c

Please sign in to comment.