Skip to content

Commit 1fb6aab

Browse files
CopilotSteake
andcommitted
Address code review feedback: improve error handling, security, and validation
Co-authored-by: Steake <530040+Steake@users.noreply.github.com>
1 parent b3ecad4 commit 1fb6aab

6 files changed

Lines changed: 118 additions & 20 deletions

File tree

crates/bitcell-admin/src/hsm.rs

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ pub struct HsmConfig {
7070
}
7171

7272
/// HSM authentication credentials
73-
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
73+
///
74+
/// # Security
75+
/// Credentials are automatically zeroed when dropped to prevent
76+
/// sensitive data from remaining in memory.
77+
#[derive(Debug, Clone, Serialize, Deserialize)]
7478
pub struct HsmCredentials {
7579
/// API token (for Vault)
7680
#[serde(skip_serializing)]
@@ -87,6 +91,46 @@ pub struct HsmCredentials {
8791
pub client_key: Option<String>,
8892
}
8993

94+
impl Default for HsmCredentials {
95+
fn default() -> Self {
96+
Self {
97+
token: None,
98+
access_key: None,
99+
secret_key: None,
100+
client_cert: None,
101+
client_key: None,
102+
}
103+
}
104+
}
105+
106+
impl Drop for HsmCredentials {
107+
fn drop(&mut self) {
108+
// Securely zero out sensitive credential data
109+
// Note: This provides basic protection but for production use
110+
// consider using the `secrecy` crate for guaranteed secure zeroing
111+
if let Some(ref mut token) = self.token {
112+
// Safety: We're writing zeros to memory that will be dropped
113+
// This helps prevent secrets from lingering in memory
114+
let bytes = unsafe { token.as_bytes_mut() };
115+
for byte in bytes {
116+
unsafe { std::ptr::write_volatile(byte, 0) };
117+
}
118+
}
119+
if let Some(ref mut key) = self.access_key {
120+
let bytes = unsafe { key.as_bytes_mut() };
121+
for byte in bytes {
122+
unsafe { std::ptr::write_volatile(byte, 0) };
123+
}
124+
}
125+
if let Some(ref mut key) = self.secret_key {
126+
let bytes = unsafe { key.as_bytes_mut() };
127+
for byte in bytes {
128+
unsafe { std::ptr::write_volatile(byte, 0) };
129+
}
130+
}
131+
}
132+
}
133+
90134
impl HsmConfig {
91135
/// Create configuration for HashiCorp Vault
92136
pub fn vault(endpoint: &str, token: &str, key_name: &str) -> Self {
@@ -95,7 +139,10 @@ impl HsmConfig {
95139
endpoint: endpoint.to_string(),
96140
credentials: HsmCredentials {
97141
token: Some(token.to_string()),
98-
..Default::default()
142+
access_key: None,
143+
secret_key: None,
144+
client_cert: None,
145+
client_key: None,
99146
},
100147
default_key: key_name.to_string(),
101148
timeout_secs: 30,
@@ -109,9 +156,11 @@ impl HsmConfig {
109156
provider: HsmProvider::AwsCloudHsm,
110157
endpoint: endpoint.to_string(),
111158
credentials: HsmCredentials {
159+
token: None,
112160
access_key: Some(access_key.to_string()),
113161
secret_key: Some(secret_key.to_string()),
114-
..Default::default()
162+
client_cert: None,
163+
client_key: None,
115164
},
116165
default_key: key_name.to_string(),
117166
timeout_secs: 30,
@@ -124,7 +173,7 @@ impl HsmConfig {
124173
Self {
125174
provider: HsmProvider::Mock,
126175
endpoint: "mock://localhost".to_string(),
127-
credentials: Default::default(),
176+
credentials: HsmCredentials::default(),
128177
default_key: key_name.to_string(),
129178
timeout_secs: 5,
130179
audit_logging: false,
@@ -185,6 +234,10 @@ pub trait HsmBackend: Send + Sync {
185234
async fn list_keys(&self) -> HsmResult<Vec<String>>;
186235
}
187236

237+
/// Maximum number of audit log entries to keep in memory
238+
/// Older entries are automatically rotated out
239+
const MAX_AUDIT_LOG_ENTRIES: usize = 10_000;
240+
188241
/// HSM client for secure key management
189242
pub struct HsmClient {
190243
config: HsmConfig,
@@ -309,6 +362,9 @@ impl HsmClient {
309362
}
310363

311364
/// Log an operation
365+
///
366+
/// The audit log is bounded to MAX_AUDIT_LOG_ENTRIES entries.
367+
/// When the limit is reached, the oldest entries are removed.
312368
async fn log_operation(&self, operation: &str, key_name: &str, success: bool, error: Option<&HsmError>) {
313369
if !self.config.audit_logging {
314370
return;
@@ -325,7 +381,14 @@ impl HsmClient {
325381
error: error.map(|e| e.to_string()),
326382
};
327383

328-
self.audit_log.write().await.push(entry);
384+
let mut log = self.audit_log.write().await;
385+
log.push(entry);
386+
387+
// Enforce maximum size by removing oldest entries
388+
if log.len() > MAX_AUDIT_LOG_ENTRIES {
389+
let excess = log.len() - MAX_AUDIT_LOG_ENTRIES;
390+
log.drain(0..excess);
391+
}
329392
}
330393
}
331394

@@ -438,7 +501,9 @@ mod tests {
438501

439502
#[tokio::test]
440503
async fn test_mock_hsm_audit_log() {
441-
let config = HsmConfig::mock("audit-test");
504+
// Create mock config with audit logging enabled
505+
let mut config = HsmConfig::mock("audit-test");
506+
config.audit_logging = true;
442507
let hsm = HsmClient::connect(config).await.unwrap();
443508

444509
// Perform some operations

crates/bitcell-crypto/src/poseidon.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,20 @@ impl PoseidonParams<Fr> {
8181
let total_constants = t * (full_rounds + partial_rounds);
8282
let mut constants = Vec::with_capacity(total_constants);
8383

84+
// Maximum iterations to prevent theoretical infinite loop
85+
const MAX_ITERATIONS: u64 = 1_000_000;
86+
8487
// Use domain-separated SHA-256 as PRF
8588
let mut counter = 0u64;
8689
while constants.len() < total_constants {
90+
if counter >= MAX_ITERATIONS {
91+
panic!(
92+
"Round constant generation exceeded {} iterations. \
93+
This should never happen with SHA-256 PRF - please report this bug.",
94+
MAX_ITERATIONS
95+
);
96+
}
97+
8798
let mut hasher = Sha256::new();
8899
hasher.update(b"BitCell_Poseidon_RC");
89100
hasher.update(&counter.to_le_bytes());
@@ -120,7 +131,10 @@ impl PoseidonParams<Fr> {
120131
for j in 0..t {
121132
// M[i][j] = 1 / (x[i] + y[j])
122133
let sum = x[i] + y[j];
123-
matrix[i][j] = sum.inverse().expect("x[i] + y[j] should be non-zero");
134+
matrix[i][j] = sum.inverse().expect(
135+
"Cauchy MDS construction: x[i] and y[j] are distinct elements, \
136+
so x[i] + y[j] != 0 and is always invertible in a prime field"
137+
);
124138
}
125139
}
126140

crates/bitcell-wallet-gui/src/main.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ fn setup_callbacks(window: &MainWindow, state: Rc<RefCell<AppState>>) {
391391
let amount: f64 = match amount_str.parse() {
392392
Ok(a) if a > 0.0 => a,
393393
_ => {
394-
wallet_state.set_status_message("Invalid amount: must be a positive number".into());
394+
wallet_state.set_status_message("Invalid amount format: expected a positive number (e.g., 1.23)".into());
395395
return;
396396
}
397397
};
@@ -403,6 +403,16 @@ fn setup_callbacks(window: &MainWindow, state: Rc<RefCell<AppState>>) {
403403

404404
let chain = parse_chain(&chain_str);
405405

406+
// Validate amount before conversion to prevent overflow
407+
// Max safe value: u64::MAX / 100_000_000 ≈ 184 billion
408+
const MAX_AMOUNT: f64 = 184_467_440_737.0; // u64::MAX / 100_000_000
409+
if amount > MAX_AMOUNT {
410+
wallet_state.set_status_message(format!(
411+
"Amount too large. Maximum: {} CELL", MAX_AMOUNT
412+
).into());
413+
return;
414+
}
415+
406416
// Convert to smallest units (1 CELL = 100_000_000 units)
407417
let amount_units = (amount * 100_000_000.0) as u64;
408418

crates/bitcell-wallet/src/hardware.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -84,40 +84,41 @@ pub struct HardwareWallet {
8484

8585
impl HardwareWallet {
8686
/// Connect to a hardware wallet
87-
#[allow(unreachable_code)]
8887
pub fn connect(wallet_type: HardwareWalletType) -> Result<Self> {
8988
let device: Arc<dyn HardwareWalletDevice> = match wallet_type {
9089
HardwareWalletType::Ledger => {
9190
#[cfg(feature = "ledger")]
9291
{
93-
Arc::new(LedgerDevice::connect()?)
92+
return Ok(Self {
93+
device: Arc::new(LedgerDevice::connect()?),
94+
derivation_path: "m/44'/60'/0'/0/0".to_string(),
95+
});
9496
}
9597
#[cfg(not(feature = "ledger"))]
9698
{
97-
return Err(Error::UnsupportedChain("Ledger support not compiled in".into()));
99+
return Err(Error::HardwareWallet("Ledger support not compiled in. Enable the 'ledger' feature.".into()));
98100
}
99101
}
100102
HardwareWalletType::Trezor => {
101103
#[cfg(feature = "trezor")]
102104
{
103-
Arc::new(TrezorDevice::connect()?)
105+
return Ok(Self {
106+
device: Arc::new(TrezorDevice::connect()?),
107+
derivation_path: "m/44'/60'/0'/0/0".to_string(),
108+
});
104109
}
105110
#[cfg(not(feature = "trezor"))]
106111
{
107-
return Err(Error::UnsupportedChain("Trezor support not compiled in".into()));
112+
return Err(Error::HardwareWallet("Trezor support not compiled in. Enable the 'trezor' feature.".into()));
108113
}
109114
}
110115
HardwareWalletType::Generic => {
111-
return Err(Error::UnsupportedChain("Generic hardware wallet not implemented".into()));
116+
return Err(Error::HardwareWallet("Generic hardware wallet is not yet implemented".into()));
112117
}
113118
#[cfg(test)]
114119
HardwareWalletType::Mock => {
115120
Arc::new(MockHardwareWallet::new())
116121
}
117-
#[cfg(not(test))]
118-
_ => {
119-
return Err(Error::UnsupportedChain("Unknown hardware wallet type".into()));
120-
}
121122
};
122123

123124
Ok(Self {

crates/bitcell-wallet/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ pub enum Error {
6161
#[error("Chain not supported: {0}")]
6262
UnsupportedChain(String),
6363

64+
#[error("Hardware wallet error: {0}")]
65+
HardwareWallet(String),
66+
6467
#[error("Wallet locked")]
6568
WalletLocked,
6669

crates/bitcell-zkp/src/poseidon_merkle.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,10 @@ impl<F: PrimeField> PoseidonMerkleGadget<F> {
134134
for i in 0..t {
135135
for j in 0..t {
136136
let sum = x[i] + y[j];
137-
matrix[i][j] = sum.inverse().expect("x[i] + y[j] should be non-zero");
137+
matrix[i][j] = sum.inverse().expect(
138+
"MDS matrix Cauchy construction guarantees non-zero inverse: \
139+
x[i] and y[j] are chosen as distinct elements so x[i] + y[j] != 0"
140+
);
138141
}
139142
}
140143

@@ -340,7 +343,9 @@ fn poseidon_hash_native<F: PrimeField>(left: F, right: F) -> F {
340343
for i in 0..t {
341344
for j in 0..t {
342345
let sum = x[i] + y[j];
343-
mds_matrix[i][j] = sum.inverse().unwrap();
346+
mds_matrix[i][j] = sum.inverse().expect(
347+
"MDS matrix Cauchy construction guarantees non-zero inverse for distinct x_i, y_j"
348+
);
344349
}
345350
}
346351

0 commit comments

Comments
 (0)