Skip to content

Commit b050434

Browse files
atlas-formclaude
andcommitted
refactor: reorganize crypto module structure and enhance key operations
- Restructure crypto module: remove algorithm.rs and p256.rs, add AES and ChaCha20 support - Enhance X25519 implementation with improved error handling and operations - Refactor ED25519 module with better key generation and signing capabilities - Add dedicated error module for better error management - Create new key module in capsula-key for centralized key operations - Update dependencies in Cargo.toml files for both crates - Add multiple example files demonstrating key operations (key_demo, key_file_demo, key_pem_demo, x25519_demo) - Include test utilities (test_loaded_keys.rs, analyze_encrypted_key.sh) - Update storage_demo example to reflect new API changes BREAKING CHANGE: Algorithm and P256 modules removed, API restructured 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
1 parent ed08a42 commit b050434

File tree

25 files changed

+1669
-753
lines changed

25 files changed

+1669
-753
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
# will have compiled files and executables
33
debug/
44
target/
5+
keys/
6+
demo_keys/
7+
58

69
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
710
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html

Cargo.toml

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,35 @@ path = "examples/full_demo.rs"
2020
name = "storage_demo"
2121
path = "examples/storage_demo.rs"
2222

23+
[[example]]
24+
name = "key_demo"
25+
path = "examples/key_demo.rs"
26+
27+
[[example]]
28+
name = "key_file_demo"
29+
path = "examples/key_file_demo.rs"
30+
31+
[[example]]
32+
name = "key_pem_demo"
33+
path = "examples/key_pem_demo.rs"
34+
35+
[[example]]
36+
name = "x25519_demo"
37+
path = "examples/x25519_demo.rs"
38+
2339
[dependencies]
2440
capsula-pki = { path = "crates/capsula-pki" }
2541
capsula-core = { path = "crates/capsula-core" }
2642
capsula-api = { path = "crates/capsula-api" }
2743
capsula-key = { path = "crates/capsula-key" }
44+
capsula-crypto = { path = "crates/capsula-crypto" }
2845

2946
# 通用依赖
30-
serde = { version = "1.0", features = ["derive"] }
31-
serde_json = "1.0"
32-
thiserror = "2.0"
33-
time = { version = "0.3", features = [
47+
serde = { workspace = true }
48+
serde_json = { workspace = true }
49+
hex = { workspace = true }
50+
thiserror = { workspace = true }
51+
time = { workspace = true, features = [
3452
"serde",
3553
"macros",
3654
"formatting",
@@ -57,11 +75,13 @@ repository = "https://github.com/ancient/capsula"
5775

5876
[workspace.dependencies]
5977
# 内部 crates
78+
capsula-crypto = { path = "crates/capsula-crypto" }
6079
capsula-pki = { path = "crates/capsula-pki" }
6180
capsula-core = { path = "crates/capsula-core" }
6281
capsula-api = { path = "crates/capsula-api" }
6382
capsula-key = { path = "crates/capsula-key" }
6483

84+
6585
# 外部依赖
6686
serde = { version = "1.0", features = ["derive"] }
6787
serde_json = "1.0"
@@ -75,12 +95,14 @@ time = { version = "0.3", features = [
7595
hex = "0.4"
7696

7797
# 加密相关
78-
ed25519-dalek = { version = "2.2", features = ["pkcs8", "pem", "rand_core"] }
79-
x25519-dalek = { version = "2.0.1", features = [] }
98+
ed25519-dalek = { version = "2.2" }
99+
x25519-dalek = { version = "2.0.1" }
80100
p256 = { version = "0.13", features = [] }
101+
pkcs8 = { version = "0.10", features = ["pem"] }
81102
getrandom = "0.3"
82103
sha2 = "0.10"
83104
signature = "2.2"
105+
base64 = "0.22.1"
84106

85107
# 证书相关
86108
rcgen = { version = "0.14", features = ["pem"] }
@@ -90,6 +112,7 @@ pem = "3.0"
90112

91113
# 密钥存储相关
92114
chacha20poly1305 = { version = "0.10", features = ["std"] }
115+
aes-gcm = { version = "0.10", features = ["std", "aes"] }
93116

94117
# 开发依赖
95118
tempfile = "3.12"

analyze_encrypted_key.sh

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/bin/bash
2+
3+
echo "=== Analyzing Encrypted Private Key Structure ==="
4+
echo
5+
6+
# Save the encrypted key to a file
7+
cat > encrypted_key.pem << 'EOF'
8+
-----BEGIN ENCRYPTED PRIVATE KEY-----
9+
MIGkMGAGCSqGSIb3DQEFDTBTMDIGCSqGSIb3DQEFDDAlBBBbLaF5L8ETOFeOm5kj
10+
sNYSAgMJJ8AwDAYIKoZIhvcNAgkFADAdBglghkgBZQMEASoEEONA7IKGbBuFmEyH
11+
9jDC/iIEQOEHJgW/5fQSzIOcnbiMhlcNnmVrIqzk1Nw1O7w35uiX0lftt/AIOZj4
12+
4wuETg79ymhr9xQ4DnMUhjFid3VWRAU=
13+
-----END ENCRYPTED PRIVATE KEY-----
14+
EOF
15+
16+
# Extract just the base64 content
17+
cat encrypted_key.pem | grep -v "BEGIN\|END" | tr -d '\n' > encrypted_key.b64
18+
19+
echo "Base64 content length: $(wc -c < encrypted_key.b64) characters"
20+
echo
21+
22+
# Decode to binary and check size
23+
base64 -d encrypted_key.b64 > encrypted_key.der
24+
echo "Binary size: $(wc -c < encrypted_key.der) bytes"
25+
echo
26+
27+
# Show hex dump of first part (ASN.1 structure)
28+
echo "First 32 bytes (hex) showing ASN.1 structure:"
29+
xxd -l 32 encrypted_key.der
30+
echo
31+
32+
echo "=== Comparison ==="
33+
echo
34+
echo "Plain Ed25519 key: 32 bytes → ~44 base64 chars"
35+
echo "Encrypted Ed25519 key: ~164 bytes → ~220 base64 chars"
36+
echo
37+
echo "The encrypted version contains:"
38+
echo " 1. PKCS#8 wrapper structure"
39+
echo " 2. Encryption algorithm identifiers (PBES2, PBKDF2, AES)"
40+
echo " 3. Salt for key derivation"
41+
echo " 4. Iteration count for PBKDF2"
42+
echo " 5. IV for AES encryption"
43+
echo " 6. The actual encrypted key data"
44+
45+
# Clean up
46+
rm -f encrypted_key.pem encrypted_key.b64 encrypted_key.der

crates/capsula-crypto/Cargo.toml

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,19 @@ license.workspace = true
77
repository.workspace = true
88

99
[dependencies]
10-
# Ed25519 signatures
11-
ed25519-dalek = { workspace = true }
12-
13-
# X25519 key exchange
14-
x25519-dalek = { workspace = true, features = ["pkcs8"] }
15-
16-
# P256 (NIST P-256/secp256r1)
10+
thiserror = { workspace = true }
11+
serde_json = { workspace = true }
12+
base64 = { workspace = true, features = [] }
13+
14+
ed25519-dalek = { workspace = true, features = [
15+
"pkcs8",
16+
"pem",
17+
"rand_core",
18+
"zeroize",
19+
] }
20+
x25519-dalek = { workspace = true, features = ["static_secrets", "zeroize"] }
1721
p256 = { workspace = true, features = ["ecdsa"] }
22+
pkcs8 = { workspace = true }
1823

1924
# PEM encoding
2025
pem = { workspace = true }
@@ -28,5 +33,9 @@ getrandom = { workspace = true }
2833
# Encoding
2934
hex = { workspace = true }
3035

36+
# Symmetric encryption
37+
chacha20poly1305 = { workspace = true }
38+
aes-gcm = { workspace = true }
39+
3140
[dev-dependencies]
3241
tempfile = { workspace = true }

crates/capsula-crypto/src/aes.rs

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//! AES-GCM authenticated encryption
2+
//!
3+
//! Provides AEAD encryption using AES-GCM algorithm with 256-bit keys.
4+
5+
use aes_gcm::{
6+
aead::{Aead, AeadCore, KeyInit, OsRng},
7+
Aes256Gcm, Key, Nonce,
8+
};
9+
10+
use crate::error::{Error, Result};
11+
12+
/// AES-256-GCM cipher wrapper
13+
pub struct Aes {
14+
cipher: Aes256Gcm,
15+
}
16+
17+
impl Aes {
18+
/// Create a new AES-256-GCM cipher from a 32-byte key
19+
pub fn new(key: &[u8; 32]) -> Result<Self> {
20+
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));
21+
Ok(Self { cipher })
22+
}
23+
24+
/// Encrypt data with AES-256-GCM
25+
///
26+
/// Returns encrypted data with 12-byte nonce prepended
27+
pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
28+
// Generate random nonce (12 bytes for AES-GCM)
29+
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
30+
31+
// Encrypt the plaintext
32+
let ciphertext = self.cipher
33+
.encrypt(&nonce, plaintext)
34+
.map_err(|e| Error::Other(format!("AES-GCM encryption failed: {}", e)))?;
35+
36+
// Prepend nonce to ciphertext
37+
let mut result = Vec::with_capacity(nonce.len() + ciphertext.len());
38+
result.extend_from_slice(&nonce);
39+
result.extend_from_slice(&ciphertext);
40+
41+
Ok(result)
42+
}
43+
44+
/// Decrypt data with AES-256-GCM
45+
///
46+
/// Expects encrypted data with 12-byte nonce prepended
47+
pub fn decrypt(&self, encrypted_data: &[u8]) -> Result<Vec<u8>> {
48+
// Check minimum length (12 bytes nonce + at least 16 bytes auth tag)
49+
if encrypted_data.len() < 28 {
50+
return Err(Error::Other("Encrypted data too short for AES-GCM".to_string()));
51+
}
52+
53+
// Extract nonce from the first 12 bytes
54+
let (nonce_bytes, ciphertext) = encrypted_data.split_at(12);
55+
let nonce = Nonce::from_slice(nonce_bytes);
56+
57+
// Decrypt the ciphertext
58+
let plaintext = self.cipher
59+
.decrypt(nonce, ciphertext)
60+
.map_err(|e| Error::Other(format!("AES-GCM decryption failed: {}", e)))?;
61+
62+
Ok(plaintext)
63+
}
64+
}
65+
66+
/// Encrypt data using AES-256-GCM with a one-time key
67+
///
68+
/// # Arguments
69+
/// * `key` - 32-byte encryption key
70+
/// * `plaintext` - Data to encrypt
71+
///
72+
/// # Returns
73+
/// Encrypted data with 12-byte nonce prepended
74+
pub fn encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>> {
75+
let aes = Aes::new(key)?;
76+
aes.encrypt(plaintext)
77+
}
78+
79+
/// Decrypt data using AES-256-GCM with a one-time key
80+
///
81+
/// # Arguments
82+
/// * `key` - 32-byte encryption key
83+
/// * `encrypted_data` - Encrypted data with 12-byte nonce prepended
84+
///
85+
/// # Returns
86+
/// Decrypted plaintext
87+
pub fn decrypt(key: &[u8; 32], encrypted_data: &[u8]) -> Result<Vec<u8>> {
88+
let aes = Aes::new(key)?;
89+
aes.decrypt(encrypted_data)
90+
}
91+
92+
#[cfg(test)]
93+
mod tests {
94+
use super::*;
95+
96+
#[test]
97+
fn test_encrypt_decrypt() {
98+
let key = [0u8; 32];
99+
let plaintext = b"Hello, AES-256-GCM!";
100+
101+
// Test standalone functions
102+
let encrypted = encrypt(&key, plaintext).unwrap();
103+
let decrypted = decrypt(&key, &encrypted).unwrap();
104+
assert_eq!(decrypted, plaintext);
105+
106+
// Test struct methods
107+
let aes = Aes::new(&key).unwrap();
108+
let encrypted = aes.encrypt(plaintext).unwrap();
109+
let decrypted = aes.decrypt(&encrypted).unwrap();
110+
assert_eq!(decrypted, plaintext);
111+
}
112+
}

0 commit comments

Comments
 (0)