Skip to content

Commit

Permalink
Refactor pure Rust aes and xchacha20
Browse files Browse the repository at this point in the history
  • Loading branch information
kigawas committed Aug 18, 2023
1 parent 45851ac commit 27b058e
Show file tree
Hide file tree
Showing 17 changed files with 400 additions and 426 deletions.
3 changes: 3 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[target.wasm32-unknown-unknown]
runner = 'wasm-bindgen-test-runner'

[env]
RUST_TEST_THREADS = "1" # restirct concurrency
32 changes: 8 additions & 24 deletions .github/workflows/cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,35 +12,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3

- uses: actions/setup-node@v3
with:
node-version: 20
- uses: actions-rs/toolchain@v1

- uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
override: true
toolchain: ${{ matrix.toolchain }}
target: wasm32-unknown-unknown
- uses: actions-rs/cargo@v1
with:
command: generate-lockfile
- uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
~/.cargo/bin
target
key: ${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}

- name: Run openssl tests
run: cargo test
- name: Run pure rust tests
run: cargo test --no-default-features --features pure

- name: Install wasm dep
run: cargo install wasm-bindgen-cli || true
- name: Run wasm tests
run: cargo test --no-default-features --features pure --target=wasm32-unknown-unknown

- run: cargo generate-lockfile

- uses: Swatinem/rust-cache@v2

- name: Publish cargo package
run: cargo login $CARGO_LOGIN_TOKEN && cargo publish
Expand Down
70 changes: 38 additions & 32 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,25 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
toolchain: [stable, beta, nightly]
feature: [openssl, pure, xchacha20]
toolchain: ["1.69.0", stable, beta, nightly]
steps:
- uses: actions/checkout@v3

- uses: actions/setup-node@v3
with:
node-version: 20
- uses: actions-rs/toolchain@v1

- uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
override: true
target: wasm32-unknown-unknown
components: rustfmt, clippy
# cargo
- uses: actions-rs/cargo@v1
with:
command: generate-lockfile
- uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
~/.cargo/bin
target
key: ${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
components: rustfmt, clippy, llvm-tools-preview

- uses: taiki-e/install-action@cargo-llvm-cov

- run: cargo generate-lockfile

- uses: Swatinem/rust-cache@v2

# install openssl on Windows
- run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
Expand All @@ -57,25 +51,37 @@ jobs:

- name: Check and run lint
run: cargo check && cargo fmt && cargo clippy
- name: Check doc
run: cargo doc --no-deps

- name: Run tests
run: cargo test --no-default-features --features ${{ matrix.feature }}
- name: Run tests with std
run: cargo test --no-default-features --features ${{ matrix.feature }},std
# OpenSSL AES
- run: cargo test --no-default-features --features openssl
- run: cargo test --no-default-features --features openssl,std
- run: cargo test --no-default-features --features openssl,aes-12bytes-nonce
- run: cargo test --no-default-features --features openssl,std,aes-12bytes-nonce

- name: Run tests with 12 bytes nonce
run: cargo test --no-default-features --features ${{ matrix.feature }},aes-12bytes-nonce
if: matrix.feature != 'xchacha20'
# Pure Rust AES
- run: cargo test --no-default-features --features pure
- run: cargo test --no-default-features --features pure,std
- run: cargo test --no-default-features --features pure,aes-12bytes-nonce
- run: cargo test --no-default-features --features pure,std,aes-12bytes-nonce

- name: Run tests with std and 12 bytes nonce
run: cargo test --no-default-features --features ${{ matrix.feature }},std,aes-12bytes-nonce
if: matrix.feature != 'xchacha20'
# XChaCha20
- run: cargo test --no-default-features --features xchacha20
- run: cargo test --no-default-features --features xchacha20,std

- name: Install wasm dep
run: cargo install wasm-bindgen-cli || true
- name: Run tests on wasm target
run: cargo test --no-default-features --features ${{ matrix.feature }} --target=wasm32-unknown-unknown
if: matrix.feature != 'openssl'
# Pure Rust AES and XChaCha20 on WASM target
- run: cargo install wasm-bindgen-cli || true
- run: cargo test --no-default-features --features pure --target=wasm32-unknown-unknown
- run: cargo test --no-default-features --features pure,std --target=wasm32-unknown-unknown
- run: cargo test --no-default-features --features xchacha20 --target=wasm32-unknown-unknown
- run: cargo test --no-default-features --features xchacha20,std --target=wasm32-unknown-unknown

# Coverage
- run: cargo llvm-cov --no-default-features --features pure --lcov --output-path .lcov.info
- uses: codecov/codecov-action@v3
with:
files: .lcov.info

- name: Check cargo package
run: cargo publish --dry-run
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,6 @@ Cargo.lock

# These are backup files generated by rustfmt
**/*.rs.bk

# llvm coverage files
.lcov.info
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

- Support `no_std`
- Revamp documentation
- Revamp configuration and add xchacha20-poly1305 backend
- Revamp configuration and add XChaCha20-Poly1305 backend
- Add configuration for more compatibility
- Revamp error handling
- Migrate to edition 2021
Expand Down
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ libsecp256k1 = {version = "0.7.1", default-features = false, features = ["static
sha2 = {version = "0.10.7", default-features = false}

# openssl aes
openssl = {version = "0.10.55", default-features = false, optional = true}
openssl = {version = "0.10.56", default-features = false, optional = true}

# pure rust aes
aes-gcm = {version = "0.10.2", default-features = false, optional = true}
Expand Down Expand Up @@ -70,7 +70,7 @@ wasm-bindgen-test = "0.3.37"
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
futures-util = "0.3.28"
reqwest = "0.11.18"
tokio = {version = "1.29.1", features = ["rt-multi-thread"]}
tokio = {version = "1.32.0", features = ["rt-multi-thread"]}

[[bench]]
harness = false
Expand Down
20 changes: 11 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/1c6d6ed949dd4836ab97421039e8be75)](https://app.codacy.com/gh/ecies/rs/dashboard)
[![License](https://img.shields.io/github/license/ecies/rs.svg)](https://github.com/ecies/rs)
[![CI](https://img.shields.io/github/actions/workflow/status/ecies/rs/ci.yml)](https://github.com/ecies/rs/actions)
[![Codecov](https://img.shields.io/codecov/c/github/ecies/rs.svg)](https://codecov.io/gh/ecies/rs)
[![Crates](https://img.shields.io/crates/v/ecies)](https://crates.io/crates/ecies)
[![Doc](https://docs.rs/ecies/badge.svg)](https://docs.rs/ecies/latest/ecies/)

Elliptic Curve Integrated Encryption Scheme for secp256k1 in Rust, based on [pure Rust implementation](https://github.com/paritytech/libsecp256k1) of secp256k1.

ECIES functionalities are built upon AES-GCM-256 and HKDF-SHA256.
ECIES functionalities are built upon AES-256-GCM and HKDF-SHA256.

This is the Rust version of [eciespy](https://github.com/ecies/py).

Expand All @@ -25,7 +26,7 @@ ecies = {version = "0.2", features = ["std"]}
```rust
use ecies::{decrypt, encrypt, utils::generate_keypair};

const MSG: &str = "helloworld";
const MSG: &str = "helloworld🌍";
let (sk, pk) = generate_keypair();
let (sk, pk) = (&sk.serialize(), &pk.serialize());

Expand Down Expand Up @@ -66,10 +67,10 @@ It's also possible to build to the `wasm32-unknown-unknown` target with the pure

## Configuration

You can enable 12 bytes nonce by specify `aes-12bytes-nonce` feature.
You can enable 12 bytes nonce by `aes-12bytes-nonce` feature on OpenSSL or pure Rust AES backend.

```toml
ecies = {version = "0.2", features = ["aes-12bytes-nonce"]} # it also works for "pure"
ecies = {version = "0.2", features = ["aes-12bytes-nonce"]} # it also works with "pure"
```

You can also enable a pure Rust [XChaCha20-Poly1305](https://github.com/RustCrypto/AEADs/tree/master/chacha20poly1305) backend.
Expand Down Expand Up @@ -100,19 +101,19 @@ update_config(Config {
});
```

For compatibility, make sure different applications share the same configuration.
For compatibility, make sure different applications share the same configuration. Normally configuration is only updated once on initialization, if not, beware of race condition.

## Security

### Why AES-GCM-256 and HKDF-SHA256
### Why AES-256-GCM and HKDF-SHA256

AEAD scheme like AES-GCM-256 should be your first option for symmetric ciphers, with unique IVs in each encryption.
AEAD scheme like AES-256-GCM should be your first option for symmetric ciphers, with unique IVs in each encryption.

For key derivation functions on shared points between two asymmetric keys, HKDFs are [proven](https://github.com/ecies/py/issues/82) to be more secure than simple hash functions like SHA256.

### Why XChaCha20-Poly1305 instead of AES-GCM-256
### Why XChaCha20-Poly1305 instead of AES-256-GCM

XChaCha20-Poly1305 is a competitive alternative to AES-256-GCM because it's fast and constant-time without hardware acceleration (resistent to cache-timing attacks). It also has longer nonce length.
XChaCha20-Poly1305 is a competitive alternative to AES-256-GCM because it's fast and constant-time without hardware acceleration (resistent to cache-timing attacks). It also has longer nonce length to alleviate the risk of birthday attacks when nonces are generated randomly.

### Cross-language compatibility

Expand Down Expand Up @@ -170,6 +171,7 @@ decrypt 200M time: [363.14 ms 364.48 ms 365.74 ms]
```

### XChaCha20 backend

```bash
$ cargo bench --no-default-features --features xchacha20
encrypt 100M time: [149.52 ms 150.06 ms 150.59 ms]
Expand Down
66 changes: 66 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,69 @@ pub fn get_ephemeral_key_size() -> usize {
pub fn is_hkdf_key_compressed() -> bool {
ECIES_CONFIG.read().is_hkdf_key_compressed
}

#[cfg(test)]
mod tests {
use super::{reset_config, update_config, Config};
use libsecp256k1::PublicKey;

use crate::{
decrypt, encrypt,
utils::{
encapsulate, generate_keypair,
tests::{decode_hex, get_sk2_sk3},
},
};

const MSG: &str = "helloworld🌍";

#[test]
pub(super) fn test_known_hkdf_config() {
let (sk2, sk3) = get_sk2_sk3();
let pk3 = PublicKey::from_secret_key(&sk3);

update_config(Config {
is_hkdf_key_compressed: true,
..Config::default()
});

let encapsulated = encapsulate(&sk2, &pk3).unwrap();

assert_eq!(
encapsulated.to_vec(),
decode_hex("b192b226edb3f02da11ef9c6ce4afe1c7e40be304e05ae3b988f4834b1cb6c69")
);

reset_config();
}

#[test]
pub(super) fn test_ephemeral_key_config() {
let (sk, pk) = generate_keypair();
let (sk, pk) = (&sk.serialize(), &pk.serialize_compressed());
let encrypted_1 = encrypt(pk, MSG.as_bytes()).unwrap();
assert_eq!(MSG.as_bytes(), &decrypt(sk, &encrypted_1).unwrap());

update_config(Config {
is_ephemeral_key_compressed: true,
..Config::default()
});

let encrypted_2 = encrypt(pk, MSG.as_bytes()).unwrap();
assert_eq!(encrypted_1.len() - encrypted_2.len(), 32);
assert_eq!(MSG.as_bytes(), &decrypt(sk, &encrypted_2).unwrap());

reset_config();
}
}

#[cfg(all(test, target_arch = "wasm32"))]
mod wasm_tests {
use wasm_bindgen_test::*;

#[wasm_bindgen_test]
fn test_wasm() {
super::tests::test_ephemeral_key_config();
super::tests::test_known_hkdf_config();
}
}
17 changes: 9 additions & 8 deletions src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@ pub use libsecp256k1::util::COMPRESSED_PUBLIC_KEY_SIZE;
/// Uncompressed public key size
pub use libsecp256k1::util::FULL_PUBLIC_KEY_SIZE as UNCOMPRESSED_PUBLIC_KEY_SIZE;

/// AES nonce length
#[cfg(not(feature = "aes-12bytes-nonce"))]
pub const AES_NONCE_LENGTH: usize = 16;
#[cfg(feature = "aes-12bytes-nonce")]
pub const AES_NONCE_LENGTH: usize = 12;

/// XChaCha20 nonce length
/// Nonce length. AES (12/16 bytes) or XChaCha20 (24 bytes)
#[cfg(all(not(feature = "aes-12bytes-nonce"), not(feature = "xchacha20")))]
pub const NONCE_LENGTH: usize = 16;
#[cfg(all(feature = "aes-12bytes-nonce", not(feature = "xchacha20")))]
pub const NONCE_LENGTH: usize = 12;
#[cfg(feature = "xchacha20")]
pub const XCHACHA20_NONCE_LENGTH: usize = 24;
pub const NONCE_LENGTH: usize = 24;

/// AEAD tag length
pub const AEAD_TAG_LENGTH: usize = 16;

/// Nonce + tag length
pub const NONCE_TAG_LENGTH: usize = NONCE_LENGTH + AEAD_TAG_LENGTH;

/// Empty bytes array
pub const EMPTY_BYTES: [u8; 0] = [];
Loading

0 comments on commit 27b058e

Please sign in to comment.