Skip to content

Commit ee62825

Browse files
committed
v2.0.0: Enhanced with encryption, redaction, and metrics
- Added AES-256-GCM log encryption module - Added PII redaction patterns (SSN, Credit Card, Email, Phone, API Keys) - Added logging metrics and rate limiting - Updated lib.rs with LoggerConfig and HashAlgorithm - New modules: encryption.rs, redaction.rs, metrics.rs
1 parent d6d5a32 commit ee62825

File tree

5 files changed

+1153
-6
lines changed

5 files changed

+1153
-6
lines changed

Cargo.toml

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,40 @@
11
[package]
22
name = "rust-secure-logger"
3-
version = "0.1.0"
3+
version = "2.0.0"
44
edition = "2021"
55
authors = ["Tony Chuks Awunor <[email protected]>"]
6-
description = "Memory-safe security logging for financial systems and critical infrastructure"
6+
description = "Memory-safe security logging for financial systems and critical infrastructure with encryption, log forwarding, and compliance automation"
77
license = "MIT"
88
repository = "https://github.com/guardsarm/rust-secure-logger"
99
keywords = ["security", "logging", "audit", "financial", "memory-safe"]
1010
categories = ["development-tools::debugging", "cryptography"]
11+
readme = "README.md"
12+
13+
[features]
14+
default = ["encryption", "compression"]
15+
encryption = []
16+
compression = []
17+
async-logging = ["tokio"]
18+
metrics = []
1119

1220
[dependencies]
1321
chrono = { version = "0.4", features = ["serde"] }
1422
serde = { version = "1.0", features = ["derive"] }
1523
serde_json = "1.0"
1624
sha2 = "0.10"
25+
sha3 = "0.10"
26+
aes-gcm = "0.10"
27+
flate2 = "1.0"
28+
base64 = "0.21"
29+
uuid = { version = "1.6", features = ["v4"] }
30+
tokio = { version = "1.35", features = ["full"], optional = true }
31+
thiserror = "1.0"
32+
regex = "1.10"
33+
rand = "0.8"
1734

1835
[dev-dependencies]
1936
tempfile = "3.8"
37+
criterion = "0.5"
2038

2139
[[example]]
2240
name = "basic_usage"
@@ -25,3 +43,11 @@ path = "examples/basic_usage.rs"
2543
[[example]]
2644
name = "audit_trail"
2745
path = "examples/audit_trail.rs"
46+
47+
[[example]]
48+
name = "encrypted_logging"
49+
path = "examples/encrypted_logging.rs"
50+
51+
[[bench]]
52+
name = "logging_benchmark"
53+
harness = false

src/encryption.rs

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
//! Log encryption module for secure logging v2.0
2+
//!
3+
//! Provides AES-256-GCM encryption for sensitive log entries.
4+
5+
use aes_gcm::{
6+
aead::{Aead, KeyInit, OsRng},
7+
Aes256Gcm, Nonce,
8+
};
9+
use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
10+
use rand::RngCore;
11+
use serde::{Deserialize, Serialize};
12+
use thiserror::Error;
13+
14+
/// Encryption errors
15+
#[derive(Error, Debug)]
16+
pub enum EncryptionError {
17+
#[error("Encryption failed: {0}")]
18+
EncryptionFailed(String),
19+
20+
#[error("Decryption failed: {0}")]
21+
DecryptionFailed(String),
22+
23+
#[error("Invalid key length")]
24+
InvalidKeyLength,
25+
26+
#[error("Base64 decode error: {0}")]
27+
Base64Error(String),
28+
}
29+
30+
/// Encrypted log entry structure
31+
#[derive(Debug, Clone, Serialize, Deserialize)]
32+
pub struct EncryptedLogEntry {
33+
pub ciphertext: String, // Base64 encoded
34+
pub nonce: String, // Base64 encoded
35+
pub timestamp: chrono::DateTime<chrono::Utc>,
36+
pub entry_id: String,
37+
}
38+
39+
/// Log encryption key with secure handling
40+
pub struct EncryptionKey {
41+
key: Vec<u8>,
42+
}
43+
44+
impl EncryptionKey {
45+
/// Generate a new random encryption key
46+
pub fn generate() -> Self {
47+
let mut key = vec![0u8; 32];
48+
OsRng.fill_bytes(&mut key);
49+
Self { key }
50+
}
51+
52+
/// Create from existing bytes
53+
pub fn from_bytes(bytes: &[u8]) -> Result<Self, EncryptionError> {
54+
if bytes.len() != 32 {
55+
return Err(EncryptionError::InvalidKeyLength);
56+
}
57+
Ok(Self {
58+
key: bytes.to_vec(),
59+
})
60+
}
61+
62+
/// Get key bytes
63+
pub fn as_bytes(&self) -> &[u8] {
64+
&self.key
65+
}
66+
}
67+
68+
impl Drop for EncryptionKey {
69+
fn drop(&mut self) {
70+
// Zero out key on drop for security
71+
for byte in &mut self.key {
72+
*byte = 0;
73+
}
74+
}
75+
}
76+
77+
/// Log encryptor for secure log entries
78+
pub struct LogEncryptor {
79+
cipher: Aes256Gcm,
80+
}
81+
82+
impl LogEncryptor {
83+
/// Create a new log encryptor with the given key
84+
pub fn new(key: &EncryptionKey) -> Result<Self, EncryptionError> {
85+
let cipher = Aes256Gcm::new_from_slice(key.as_bytes())
86+
.map_err(|e| EncryptionError::EncryptionFailed(e.to_string()))?;
87+
Ok(Self { cipher })
88+
}
89+
90+
/// Encrypt a log entry
91+
pub fn encrypt(&self, plaintext: &str, entry_id: &str) -> Result<EncryptedLogEntry, EncryptionError> {
92+
let mut nonce_bytes = [0u8; 12];
93+
OsRng.fill_bytes(&mut nonce_bytes);
94+
let nonce = Nonce::from_slice(&nonce_bytes);
95+
96+
let ciphertext = self
97+
.cipher
98+
.encrypt(nonce, plaintext.as_bytes())
99+
.map_err(|e| EncryptionError::EncryptionFailed(e.to_string()))?;
100+
101+
Ok(EncryptedLogEntry {
102+
ciphertext: BASE64.encode(&ciphertext),
103+
nonce: BASE64.encode(nonce_bytes),
104+
timestamp: chrono::Utc::now(),
105+
entry_id: entry_id.to_string(),
106+
})
107+
}
108+
109+
/// Decrypt a log entry
110+
pub fn decrypt(&self, encrypted: &EncryptedLogEntry) -> Result<String, EncryptionError> {
111+
let ciphertext = BASE64
112+
.decode(&encrypted.ciphertext)
113+
.map_err(|e| EncryptionError::Base64Error(e.to_string()))?;
114+
115+
let nonce_bytes = BASE64
116+
.decode(&encrypted.nonce)
117+
.map_err(|e| EncryptionError::Base64Error(e.to_string()))?;
118+
119+
if nonce_bytes.len() != 12 {
120+
return Err(EncryptionError::DecryptionFailed("Invalid nonce length".to_string()));
121+
}
122+
123+
let nonce = Nonce::from_slice(&nonce_bytes);
124+
125+
let plaintext = self
126+
.cipher
127+
.decrypt(nonce, ciphertext.as_ref())
128+
.map_err(|e| EncryptionError::DecryptionFailed(e.to_string()))?;
129+
130+
String::from_utf8(plaintext)
131+
.map_err(|e| EncryptionError::DecryptionFailed(e.to_string()))
132+
}
133+
134+
/// Encrypt multiple log entries in batch
135+
pub fn encrypt_batch(&self, entries: &[(&str, &str)]) -> Vec<Result<EncryptedLogEntry, EncryptionError>> {
136+
entries
137+
.iter()
138+
.map(|(plaintext, entry_id)| self.encrypt(plaintext, entry_id))
139+
.collect()
140+
}
141+
}
142+
143+
#[cfg(test)]
144+
mod tests {
145+
use super::*;
146+
147+
#[test]
148+
fn test_encryption_decryption() {
149+
let key = EncryptionKey::generate();
150+
let encryptor = LogEncryptor::new(&key).unwrap();
151+
152+
let plaintext = "Sensitive log entry: User 12345 accessed financial records";
153+
let entry_id = "LOG-001";
154+
155+
let encrypted = encryptor.encrypt(plaintext, entry_id).unwrap();
156+
let decrypted = encryptor.decrypt(&encrypted).unwrap();
157+
158+
assert_eq!(plaintext, decrypted);
159+
assert_eq!(encrypted.entry_id, entry_id);
160+
}
161+
162+
#[test]
163+
fn test_different_nonces() {
164+
let key = EncryptionKey::generate();
165+
let encryptor = LogEncryptor::new(&key).unwrap();
166+
167+
let plaintext = "Same message";
168+
let enc1 = encryptor.encrypt(plaintext, "LOG-001").unwrap();
169+
let enc2 = encryptor.encrypt(plaintext, "LOG-002").unwrap();
170+
171+
// Same plaintext should produce different ciphertext due to random nonce
172+
assert_ne!(enc1.ciphertext, enc2.ciphertext);
173+
assert_ne!(enc1.nonce, enc2.nonce);
174+
}
175+
176+
#[test]
177+
fn test_invalid_key_length() {
178+
let short_key = vec![0u8; 16];
179+
let result = EncryptionKey::from_bytes(&short_key);
180+
assert!(result.is_err());
181+
}
182+
183+
#[test]
184+
fn test_batch_encryption() {
185+
let key = EncryptionKey::generate();
186+
let encryptor = LogEncryptor::new(&key).unwrap();
187+
188+
let entries = vec![
189+
("Log entry 1", "LOG-001"),
190+
("Log entry 2", "LOG-002"),
191+
("Log entry 3", "LOG-003"),
192+
];
193+
194+
let results = encryptor.encrypt_batch(&entries);
195+
assert_eq!(results.len(), 3);
196+
assert!(results.iter().all(|r| r.is_ok()));
197+
}
198+
199+
#[test]
200+
fn test_encrypted_entry_serialization() {
201+
let key = EncryptionKey::generate();
202+
let encryptor = LogEncryptor::new(&key).unwrap();
203+
204+
let encrypted = encryptor.encrypt("Test message", "LOG-001").unwrap();
205+
let json = serde_json::to_string(&encrypted).unwrap();
206+
let deserialized: EncryptedLogEntry = serde_json::from_str(&json).unwrap();
207+
208+
let decrypted = encryptor.decrypt(&deserialized).unwrap();
209+
assert_eq!(decrypted, "Test message");
210+
}
211+
}

0 commit comments

Comments
 (0)