Skip to content

Commit 961d5bc

Browse files
committed
Add Argon2id key derivation for cross-platform encryption
Introduces a memory-hard Argon2id-based key derivation function to fula-crypto, fula-flutter, and fula-js for secure, brute-force resistant key generation from user credentials. Updates derive_key implementations in Flutter and JS bindings to use Argon2id for consistent encryption key derivation across platforms. Bumps workspace and Flutter client versions to 0.2.24.
1 parent c254b46 commit 961d5bc

8 files changed

Lines changed: 178 additions & 21 deletions

File tree

Cargo.lock

Lines changed: 41 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ name = "encrypted_upload_test"
7474
path = "examples/encrypted_upload_test.rs"
7575

7676
[workspace.package]
77-
version = "0.2.23"
77+
version = "0.2.24"
7878
edition = "2021"
7979
license = "MIT OR Apache-2.0"
8080
repository = "https://github.com/functionland/fula-api"

crates/fula-crypto/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ sha2 = { workspace = true }
2929
md-5 = { workspace = true }
3030
x25519-dalek = { workspace = true }
3131
ed25519-dalek = { workspace = true }
32+
argon2 = "0.5"
3233

3334
# Post-Quantum Cryptography (NIST FIPS 203 ML-KEM)
3435
# Using Cryspen's formally verified implementation for WASM + native support

crates/fula-crypto/src/hashing.rs

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,13 +197,66 @@ where
197197
hasher.finalize()
198198
}
199199

200-
/// Derive a key from the given input and context
200+
/// Derive a key from the given input and context using BLAKE3 KDF
201+
///
202+
/// Note: For password-based or credential-based key derivation where brute-force
203+
/// resistance is needed, use `derive_key_argon2id` instead.
201204
pub fn derive_key(context: &str, input: &[u8]) -> Blake3Hash {
202205
let mut hasher = IncrementalHasher::new_derive_key(context);
203206
hasher.update(input);
204207
hasher.finalize()
205208
}
206209

210+
/// Derive a key from the given input and context using Argon2id
211+
///
212+
/// This provides brute-force resistance for credential-based key derivation.
213+
/// Uses memory-hard Argon2id algorithm with secure parameters:
214+
/// - Memory: 64 MiB
215+
/// - Iterations: 3
216+
/// - Parallelism: 1 (for cross-platform consistency)
217+
/// - Output: 32 bytes
218+
///
219+
/// The context string is used as the salt to provide domain separation.
220+
///
221+
/// # Example
222+
/// ```
223+
/// use fula_crypto::hashing::derive_key_argon2id;
224+
///
225+
/// let key = derive_key_argon2id("fula-files-v1", b"google:123456:user@example.com");
226+
/// assert_eq!(key.len(), 32);
227+
/// ```
228+
pub fn derive_key_argon2id(context: &str, input: &[u8]) -> [u8; 32] {
229+
use argon2::{Argon2, Algorithm, Version, Params};
230+
231+
// Secure parameters for credential-based key derivation
232+
// Memory: 64 MiB (65536 KiB) - provides memory-hardness
233+
// Iterations: 3 - time cost
234+
// Parallelism: 1 - for consistent cross-platform results
235+
let params = Params::new(65536, 3, 1, Some(32))
236+
.expect("Invalid Argon2 parameters");
237+
238+
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params);
239+
240+
let mut output = [0u8; 32];
241+
242+
// Use context as salt for domain separation
243+
// Pad or hash the context to ensure minimum 8-byte salt requirement
244+
let salt = if context.len() >= 8 {
245+
context.as_bytes().to_vec()
246+
} else {
247+
// Pad short contexts with zeros
248+
let mut padded = vec![0u8; 8];
249+
padded[..context.len()].copy_from_slice(context.as_bytes());
250+
padded
251+
};
252+
253+
argon2
254+
.hash_password_into(input, &salt, &mut output)
255+
.expect("Argon2 hashing failed");
256+
257+
output
258+
}
259+
207260
/// Calculate an MD5 hash for S3 ETag compatibility
208261
pub fn md5_hash(data: &[u8]) -> String {
209262
use md5::{Md5, Digest};
@@ -308,4 +361,30 @@ mod tests {
308361
let key2 = derive_key("context2", b"input");
309362
assert_ne!(key1, key2);
310363
}
364+
365+
#[test]
366+
fn test_derive_key_argon2id() {
367+
// Test basic functionality
368+
let key1 = derive_key_argon2id("fula-files-v1", b"google:123456:user@example.com");
369+
assert_eq!(key1.len(), 32);
370+
371+
// Test consistency - same input produces same output
372+
let key2 = derive_key_argon2id("fula-files-v1", b"google:123456:user@example.com");
373+
assert_eq!(key1, key2);
374+
375+
// Test different context produces different key
376+
let key3 = derive_key_argon2id("fula-files-v2", b"google:123456:user@example.com");
377+
assert_ne!(key1, key3);
378+
379+
// Test different input produces different key
380+
let key4 = derive_key_argon2id("fula-files-v1", b"google:789012:other@example.com");
381+
assert_ne!(key1, key4);
382+
}
383+
384+
#[test]
385+
fn test_derive_key_argon2id_short_context() {
386+
// Test with short context (< 8 bytes) - should still work
387+
let key = derive_key_argon2id("short", b"input");
388+
assert_eq!(key.len(), 32);
389+
}
311390
}

crates/fula-flutter/src/api/encrypted.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,38 @@ pub fn derive_public_key_from_secret(secret_key_bytes: Vec<u8>) -> anyhow::Resul
205205
Ok(public.as_bytes().to_vec())
206206
}
207207

208+
/// Derive a 32-byte key using Argon2id (memory-hard KDF)
209+
///
210+
/// **IMPORTANT**: Use this function to derive encryption keys from user credentials
211+
/// instead of platform-specific PBKDF2 implementations.
212+
///
213+
/// This ensures both FxFiles (Flutter) and WebUI (WASM) derive the exact same key
214+
/// from the same inputs, with brute-force resistance from Argon2id's memory-hardness.
215+
///
216+
/// Parameters:
217+
/// - Memory: 64 MiB
218+
/// - Iterations: 3
219+
/// - Parallelism: 1 (for cross-platform consistency)
220+
///
221+
/// # Arguments
222+
/// * `context` - A context string used as salt (e.g., "fula-files-v1")
223+
/// * `input` - The input bytes (e.g., UTF-8 encoded "google:{userId}:{email}")
224+
///
225+
/// # Returns
226+
/// * 32-byte derived key
227+
///
228+
/// # Example
229+
/// ```dart
230+
/// // Derive encryption key from Google credentials (same as WebUI):
231+
/// final input = utf8.encode('google:${userId}:${email}');
232+
/// final secretKey = await deriveKey(context: 'fula-files-v1', input: input);
233+
///
234+
/// // Use secretKey for createEncryptedClient
235+
/// ```
236+
pub fn derive_key(context: String, input: Vec<u8>) -> Vec<u8> {
237+
fula_crypto::hashing::derive_key_argon2id(&context, &input).to_vec()
238+
}
239+
208240
/// Check if client uses FlatNamespace mode
209241
pub async fn is_flat_namespace(client: &EncryptedClientHandle) -> bool {
210242
let guard = client.inner.read().await;

crates/fula-flutter/src/api/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ pub use encrypted::{
5252
list_directory,
5353
export_secret_key,
5454
get_public_key,
55+
derive_key,
56+
derive_public_key_from_secret,
5557
is_flat_namespace,
5658
enc_list_buckets,
5759
enc_create_bucket,

crates/fula-js/src/lib.rs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -456,15 +456,24 @@ pub async fn get_public_key(client: &EncryptedClient) -> Vec<u8> {
456456
guard.encryption_config().public_key().as_bytes().to_vec()
457457
}
458458

459-
/// Derive a 32-byte key from context and input
459+
/// Derive a 32-byte key from context and input using Argon2id (memory-hard KDF)
460460
///
461-
/// Use this to derive encryption keys from Google credentials:
461+
/// Use this to derive encryption keys from Google credentials with brute-force resistance:
462462
/// ```javascript
463-
/// const key = deriveKey('my-app-v1', new TextEncoder().encode(userId + email));
463+
/// const key = deriveKey('fula-files-v1', new TextEncoder().encode(`google:${userId}:${email}`));
464464
/// ```
465+
///
466+
/// Parameters:
467+
/// - Memory: 64 MiB
468+
/// - Iterations: 3
469+
/// - Parallelism: 1 (for cross-platform consistency)
470+
///
471+
/// @param context - Context string used as salt (e.g., "fula-files-v1")
472+
/// @param input - Input bytes (e.g., UTF-8 encoded credentials)
473+
/// @returns 32-byte derived key
465474
#[wasm_bindgen(js_name = deriveKey)]
466475
pub fn derive_key(context: &str, input: &[u8]) -> Vec<u8> {
467-
fula_crypto::hashing::derive_key(context, input).as_bytes().to_vec()
476+
fula_crypto::hashing::derive_key_argon2id(context, input).to_vec()
468477
}
469478

470479
/// Derive X25519 public key from private key bytes
@@ -611,14 +620,15 @@ mod tests {
611620
wasm_bindgen_test_configure!(run_in_browser);
612621

613622
#[wasm_bindgen_test]
614-
fn test_derive_key() {
615-
let key1 = derive_key("test-context", b"input1");
616-
let key2 = derive_key("test-context", b"input2");
617-
let key3 = derive_key("test-context", b"input1");
623+
fn test_derive_key_argon2id() {
624+
// Test Argon2id key derivation
625+
let key1 = derive_key("fula-files-v1", b"google:123:user@test.com");
626+
let key2 = derive_key("fula-files-v1", b"google:456:other@test.com");
627+
let key3 = derive_key("fula-files-v1", b"google:123:user@test.com");
618628

619629
assert_eq!(key1.len(), 32);
620-
assert_ne!(key1, key2);
621-
assert_eq!(key1, key3); // Deterministic
630+
assert_ne!(key1, key2); // Different input -> different key
631+
assert_eq!(key1, key3); // Same input -> same key (deterministic)
622632
}
623633

624634
#[wasm_bindgen_test]

packages/fula_client/pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: fula_client
22
description: Flutter SDK for Fula decentralized storage with client-side encryption, metadata privacy, and secure sharing.
3-
version: 0.2.23
3+
version: 0.2.24
44
homepage: https://fx.land
55
repository: https://github.com/functionland/fula-api
66
issue_tracker: https://github.com/functionland/fula-api/issues

0 commit comments

Comments
 (0)