Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: CI

on:
push:
branches: [main, ci/test-pipeline]
pull_request:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
rust:
name: Rust (lint + test)
runs-on: ubuntu-latest
defaults:
run:
working-directory: rust
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
workspaces: rust
- run: cargo clippy --all-targets -- -D warnings
- run: cargo test

dart:
name: Dart (codegen + analyze)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
- uses: dtolnay/rust-toolchain@stable
- run: cargo install flutter_rust_bridge_codegen
- run: flutter pub get
- run: flutter_rust_bridge_codegen generate
- run: dart run build_runner build --delete-conflicting-outputs
- run: dart analyze lib/ integration_test/

build-android:
name: Android build
needs: [rust, dart]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-linux-android,armv7-linux-androideabi
- uses: nttld/setup-ndk@v1
with:
ndk-version: r27c
- run: cargo install flutter_rust_bridge_codegen
- run: flutter pub get
- run: flutter_rust_bridge_codegen generate
- run: dart run build_runner build --delete-conflicting-outputs
- run: cd example && flutter build apk --release

build-ios:
name: iOS build
needs: [rust, dart]
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-ios,aarch64-apple-ios-sim
- run: cargo install flutter_rust_bridge_codegen
- run: flutter pub get
- run: flutter_rust_bridge_codegen generate
- run: dart run build_runner build --delete-conflicting-outputs
- run: cd example && flutter build ios --simulator --debug

build-linux:
name: Linux build
needs: [rust, dart]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: subosito/flutter-action@v2
with:
channel: stable
- uses: dtolnay/rust-toolchain@stable
- run: sudo apt-get update && sudo apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev
- run: cargo install flutter_rust_bridge_codegen
- run: flutter pub get
- run: flutter_rust_bridge_codegen generate
- run: dart run build_runner build --delete-conflicting-outputs
- run: cd example && flutter build linux --release
12 changes: 4 additions & 8 deletions rust/src/api/encryption/aes_gcm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,19 +220,15 @@ mod tests {
fn nist_test_vector_decrypt() {
let key = hex("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308");
let nonce = hex("cafebabefacedbaddecaf888");
let expected_pt = hex(
"d9313225f88406e5a55909c5aff5269a\
let expected_pt = hex("d9313225f88406e5a55909c5aff5269a\
86a7a9531534f7da2e4c303d8a318a72\
1c3c0c95956809532fcf0e2449a6b525\
b16aedf5aa0de657ba637b39",
);
b16aedf5aa0de657ba637b39");
let aad = hex("feedfacedeadbeeffeedfacedeadbeefabaddad2");
let ct = hex(
"522dc1f099567d07f47f37a32a84427d\
let ct = hex("522dc1f099567d07f47f37a32a84427d\
643a8cdcbfe5c0c97598a2bd2555d1aa\
8cb08e48590dbb3da7b08b1056828838\
c5f61e6393ba7a0abcc9f662",
);
c5f61e6393ba7a0abcc9f662");
let tag = hex("76fc6ece0f4e1768cddf8853bb2d551b");

// Build wire format: nonce || ciphertext || tag
Expand Down
28 changes: 19 additions & 9 deletions rust/src/api/hashing/argon2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ mod tests {

#[test]
fn test_hash_contains_preset_params() {
let hash = argon2id_hash("test".into(), Argon2Preset::Mobile)
.expect("hashing should succeed");
let hash =
argon2id_hash("test".into(), Argon2Preset::Mobile).expect("hashing should succeed");

// Mobile: m=65536 (64*1024), t=3, p=4
assert!(hash.contains("m=65536"), "Missing memory param: {}", hash);
Expand All @@ -106,8 +106,8 @@ mod tests {

#[test]
fn test_hash_desktop_preset_params() {
let hash = argon2id_hash("test".into(), Argon2Preset::Desktop)
.expect("hashing should succeed");
let hash =
argon2id_hash("test".into(), Argon2Preset::Desktop).expect("hashing should succeed");

// Desktop: m=262144 (256*1024), t=4, p=8
assert!(hash.contains("m=262144"), "Missing memory param: {}", hash);
Expand Down Expand Up @@ -147,7 +147,10 @@ mod tests {
let hash2 = argon2id_hash_with_salt("password".into(), salt.into(), Argon2Preset::Mobile)
.expect("hashing should succeed");

assert_eq!(hash1, hash2, "Same password + salt should produce same hash");
assert_eq!(
hash1, hash2,
"Same password + salt should produce same hash"
);
}

#[test]
Expand All @@ -159,7 +162,10 @@ mod tests {
let hash2 = argon2id_hash_with_salt("password2".into(), salt.into(), Argon2Preset::Mobile)
.expect("hashing should succeed");

assert_ne!(hash1, hash2, "Different passwords should produce different hashes");
assert_ne!(
hash1, hash2,
"Different passwords should produce different hashes"
);
}

#[test]
Expand Down Expand Up @@ -196,7 +202,11 @@ mod tests {

#[test]
fn test_invalid_salt() {
let result = argon2id_hash_with_salt("password".into(), "!!!invalid!!!".into(), Argon2Preset::Mobile);
let result = argon2id_hash_with_salt(
"password".into(),
"!!!invalid!!!".into(),
Argon2Preset::Mobile,
);
assert!(result.is_err());
match result {
Err(CryptoError::InvalidParameter(_)) => {} // expected
Expand All @@ -207,8 +217,8 @@ mod tests {
#[test]
fn test_empty_password_hashes() {
// Empty password is valid — Argon2id should handle it
let hash = argon2id_hash(String::new(), Argon2Preset::Mobile)
.expect("empty password should hash");
let hash =
argon2id_hash(String::new(), Argon2Preset::Mobile).expect("empty password should hash");
assert!(hash.starts_with("$argon2id$"));
assert!(argon2id_verify(hash, String::new()).is_ok());
}
Expand Down
20 changes: 12 additions & 8 deletions rust/src/api/hashing/blake3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,23 +56,25 @@ mod tests {
let digest = hasher.finalize().expect("finalize should succeed");

// BLAKE3 empty string hash (official test vector)
let expected = hex::decode(
"af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"
).expect("valid hex");
let expected =
hex::decode("af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262")
.expect("valid hex");

assert_eq!(digest, expected);
}

#[test]
fn test_hello_world() {
let mut hasher = Blake3Hasher::new();
hasher.update(b"hello world").expect("update should succeed");
hasher
.update(b"hello world")
.expect("update should succeed");
let digest = hasher.finalize().expect("finalize should succeed");

// BLAKE3("hello world") - verified against reference implementation
let expected = hex::decode(
"d74981efa70a0c880b8d8c1985d075dbcbf679b99a5f9914e5aaf96b831a9e24"
).expect("valid hex");
let expected =
hex::decode("d74981efa70a0c880b8d8c1985d075dbcbf679b99a5f9914e5aaf96b831a9e24")
.expect("valid hex");

assert_eq!(digest, expected);
}
Expand Down Expand Up @@ -101,7 +103,9 @@ mod tests {

// After reset, should produce empty hash
let digest = hasher.finalize().expect("finalize should succeed");
let empty_hash = Blake3Hasher::new().finalize().expect("finalize should succeed");
let empty_hash = Blake3Hasher::new()
.finalize()
.expect("finalize should succeed");

assert_eq!(digest, empty_hash);
}
Expand Down
30 changes: 17 additions & 13 deletions rust/src/api/hashing/mod.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! Hashing API module.

pub mod argon2;
mod blake3;
mod sha3;
pub mod argon2;

use crate::core::error::CryptoError;
use crate::core::traits::Hasher;
Expand Down Expand Up @@ -37,33 +37,37 @@ pub fn create_sha3() -> HasherHandle {

/// Feed data into the hasher.
pub fn hasher_update(handle: &HasherHandle, data: Vec<u8>) -> Result<(), CryptoError> {
let mut guard = handle.inner.lock().map_err(|_| {
CryptoError::HashingFailed("Hasher lock poisoned".into())
})?;
let mut guard = handle
.inner
.lock()
.map_err(|_| CryptoError::HashingFailed("Hasher lock poisoned".into()))?;
guard.update(&data)
}

/// Reset the hasher to its initial state.
pub fn hasher_reset(handle: &HasherHandle) -> Result<(), CryptoError> {
let mut guard = handle.inner.lock().map_err(|_| {
CryptoError::HashingFailed("Hasher lock poisoned".into())
})?;
let mut guard = handle
.inner
.lock()
.map_err(|_| CryptoError::HashingFailed("Hasher lock poisoned".into()))?;
guard.reset()
}

/// Finalize and return the digest.
pub fn hasher_finalize(handle: &HasherHandle) -> Result<Vec<u8>, CryptoError> {
let guard = handle.inner.lock().map_err(|_| {
CryptoError::HashingFailed("Hasher lock poisoned".into())
})?;
let guard = handle
.inner
.lock()
.map_err(|_| CryptoError::HashingFailed("Hasher lock poisoned".into()))?;
guard.finalize()
}

/// Get the algorithm identifier for the hasher.
pub fn hasher_algorithm_id(handle: &HasherHandle) -> Result<String, CryptoError> {
let guard = handle.inner.lock().map_err(|_| {
CryptoError::HashingFailed("Hasher lock poisoned".into())
})?;
let guard = handle
.inner
.lock()
.map_err(|_| CryptoError::HashingFailed("Hasher lock poisoned".into()))?;
Ok(guard.algorithm_id().to_string())
}

Expand Down
18 changes: 9 additions & 9 deletions rust/src/api/hashing/sha3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,9 @@ mod tests {
let digest = hasher.finalize().expect("finalize should succeed");

// NIST SHA-3 empty string test vector
let expected = hex::decode(
"a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a",
)
.expect("valid hex");
let expected =
hex::decode("a7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a")
.expect("valid hex");

assert_eq!(digest, expected);
}
Expand All @@ -72,10 +71,9 @@ mod tests {
let digest = hasher.finalize().expect("finalize should succeed");

// NIST SHA-3("abc") test vector
let expected = hex::decode(
"3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532",
)
.expect("valid hex");
let expected =
hex::decode("3a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532")
.expect("valid hex");

assert_eq!(digest, expected);
}
Expand Down Expand Up @@ -104,7 +102,9 @@ mod tests {

// After reset, should produce empty hash
let digest = hasher.finalize().expect("finalize should succeed");
let empty_hash = Sha3Hasher::new().finalize().expect("finalize should succeed");
let empty_hash = Sha3Hasher::new()
.finalize()
.expect("finalize should succeed");

assert_eq!(digest, empty_hash);
}
Expand Down
Loading