What changed, and why it matters
This commit adds a new password-stretching algorithm (V1) for BitBox02 devices that use the Optiga secure chip. It changes how the device turns a user's password into an encryption key, adding extra secure-chip-backed hashing steps and enforcing that newly created passwords use the new algorithm. Old V0 passwords can still be unlocked for backward compatibility. There is no direct evidence in the commit of a security vulnerability being fixed; it reads as a planned feature/upgrade.
Treat as a routine firmware feature/algorithm upgrade. Review the V1 KDF flow for correct counter and authorization handling during normal QA, and verify that V0 unlock paths remain functional for existing users. No urgent security response is indicated by the supplied materials.
Security signals we found
New password-stretching algorithm (V1) implemented for Optiga secure chip
New algorithm uses additional write-protected HMAC key slot and extra monotonic counters
New-password creation now restricted to V1 only; V0 kept only for unlock compatibility
Unit-test event counters changed to reflect new secure-chip operations
No explicit security bug, CVE, or vulnerability description in commit message or diff
Evidence from the diff
The patch implements MEMORY_PASSWORD_STRETCH_ALGO_V1 in src/optiga/optiga.c. It introduces _v1_get_auth_password() (salt-hash + internal KDF + HMAC-writeprotected KDF), _set_hmac_writeprotected() (updates a write-protected HMAC key slot), _optiga_verify_password_v1() (authorizes against OID_PASSWORD and reads OID_PASSWORD_SECRET), and a completed _stretch_password_v1(). optiga_init_new_password() now rejects non-V1 algorithms. The Rust testing HAL defaults to Optiga and V1, and unit-test event-counter expectations are updated accordingly. V0 remains supported only in optiga_stretch_password() for unlocking legacy seeds.
Changed components
src/optiga/optiga.csrc/rust/bitbox02-rust/src/hal.rssrc/rust/bitbox02-rust/src/keystore.rssrc/rust/bitbox02-rust/src/hww/api/backup.rssrc/rust/bitbox02-rust/src/hww/api/change_password.rssrc/rust/bitbox02-rust/src/hww/api/restore.rssrc/rust/bitbox02-rust/src/hww/api/set_password.rssrc/rust/bitbox02-rust/src/hww/api/show_mnemonic.rssrc/rust/bitbox02-rust/src/reset.rssrc/rust/bitbox02-rust/src/workflow/unlock.rstest/hardware-fakes/src/fake_securechip.cInspect captured patch +230 / −47
diff --git a/src/optiga/optiga.c b/src/optiga/optiga.c
index 8f3999b..52dcfc4 100644
--- a/src/optiga/optiga.c
+++ b/src/optiga/optiga.c
@@ -1144,21 +1144,35 @@ static int _maybe_update_config_v1(void)
static int _set_password(
const uint8_t* password_secret,
size_t password_secret_len,
- const uint8_t* data,
- size_t data_len)
+ const uint8_t* auth_password,
+ size_t auth_password_len)
{
optiga_lib_status_t res = _authorize(OID_PASSWORD_SECRET, password_secret, password_secret_len);
if (res != OPTIGA_UTIL_SUCCESS) {
goto cleanup;
}
+ uint8_t auth_password_salted_hashed[32] = {0};
+ if (!salt_hash_data(
+ auth_password, auth_password_len, "optiga_password", auth_password_salted_hashed)) {
+ res = SC_ERR_SALT;
+ goto cleanup;
+ }
+
res = optiga_ops_util_write_data_sync(
- _util, OID_PASSWORD, OPTIGA_UTIL_ERASE_AND_WRITE, 0x00, data, data_len);
+ _util,
+ OID_PASSWORD,
+ OPTIGA_UTIL_ERASE_AND_WRITE,
+ 0x00,
+ auth_password_salted_hashed,
+ sizeof(auth_password_salted_hashed));
if (res != OPTIGA_UTIL_SUCCESS) {
goto cleanup;
}
- res = _reset_counter(OID_COUNTER_PASSWORD, SMALL_MONOTONIC_COUNTER_MAX_USE);
+ // We add one extra to the counter threshold, as afterwards, we will
+ // write to the write-protected hmac slot, which increments the counter.
+ res = _reset_counter(OID_COUNTER_PASSWORD, SMALL_MONOTONIC_COUNTER_MAX_USE + 1);
if (res != OPTIGA_LIB_SUCCESS) {
goto cleanup;
}
@@ -1229,11 +1243,91 @@ static int _kdf_internal(const uint8_t* msg, size_t len, uint8_t* kdf_out)
return 0;
}
+static int _set_hmac_writeprotected(
+ const uint8_t* hmac_key,
+ const uint8_t* auth_password,
+ size_t auth_password_len)
+{
+ uint8_t auth_password_salted_hashed[32] = {0};
+ if (!salt_hash_data(
+ auth_password, auth_password_len, "optiga_password", auth_password_salted_hashed)) {
+ return SC_ERR_SALT;
+ }
+
+ optiga_lib_status_t res =
+ _authorize(OID_PASSWORD, auth_password_salted_hashed, sizeof(auth_password_salted_hashed));
+ if (res) {
+ goto cleanup;
+ }
+
+ res = optiga_ops_util_write_data_sync(
+ _util, OID_HMAC_WRITEPROTECTED, OPTIGA_UTIL_ERASE_AND_WRITE, 0x00, hmac_key, 32);
+ if (res) {
+ util_log("failed updating the hmac-writeprotected key: %x", res);
+ goto cleanup;
+ }
+
+ res = _reset_counter(OID_COUNTER_HMAC_WRITEPROTECTED, SMALL_MONOTONIC_COUNTER_MAX_USE);
+ if (res) {
+ goto cleanup;
+ }
+
+cleanup: {
+ optiga_lib_status_t res_clear = optiga_ops_crypt_clear_auto_state_sync(_crypt, OID_PASSWORD);
+ return res ? res : res_clear;
+}
+}
+
+static int _v1_get_auth_password(
+ const char* password,
+ const uint8_t* hmac_key,
+ uint8_t* stretched_password_out)
+{
+ uint8_t password_salted_hashed[32] = {0};
+ UTIL_CLEANUP_32(password_salted_hashed);
+ if (!salt_hash_data(
+ (const uint8_t*)password,
+ strlen(password),
+ "optiga_password_stretch_in",
+ password_salted_hashed)) {
+ return SC_ERR_SALT;
+ }
+
+ uint8_t kdf_in[32] = {0};
+ UTIL_CLEANUP_32(kdf_in);
+ memcpy(kdf_in, password_salted_hashed, 32);
+
+ // First KDF on internal key increments the large monotonic counter. Call only once!
+ int securechip_result = _kdf_internal(kdf_in, 32, stretched_password_out);
+ if (securechip_result) {
+ return securechip_result;
+ }
+ // Second KDF increments the small monotonic counter in `OID_HMAC_WRITEPROTECTED`. Call only
+ // once!
+ memcpy(kdf_in, stretched_password_out, 32);
+ if (hmac_key != NULL) {
+ rust_hmac_sha256(hmac_key, 32, kdf_in, 32, stretched_password_out);
+ } else {
+ securechip_result = _kdf_hmac(OID_HMAC_WRITEPROTECTED, kdf_in, 32, stretched_password_out);
+ if (securechip_result) {
+ if (securechip_result == 0x802F) {
+ return SC_ERR_INCORRECT_PASSWORD;
+ }
+ return securechip_result;
+ }
+ }
+
+ return 0;
+}
+
int optiga_init_new_password(
const char* password,
memory_password_stretch_algo_t password_stretch_algo)
{
- (void)password_stretch_algo;
+ if (password_stretch_algo != MEMORY_PASSWORD_STRETCH_ALGO_V1) {
+ // New passwords must use the latest algo.
+ return SC_ERR_INVALID_PASSWORD_STRETCH_ALGO;
+ }
// Set new hmac key.
uint8_t new_hmac_key[32] = {0};
@@ -1268,33 +1362,36 @@ int optiga_init_new_password(
return res;
}
- uint8_t password_salted_hashed[32] = {0};
- UTIL_CLEANUP_32(password_salted_hashed);
- if (!salt_hash_data(
- (const uint8_t*)password,
- strlen(password),
- "optiga_password",
- password_salted_hashed)) {
- return SC_ERR_SALT;
+ uint8_t new_hmac_writeprotected_key[32] = {0};
+ _ifs->random_32_bytes(new_hmac_writeprotected_key);
+
+ uint8_t auth_password[32] = {0};
+ res = _v1_get_auth_password(password, new_hmac_writeprotected_key, auth_password);
+ if (res) {
+ return res;
}
res = _set_password(
- password_secret,
- sizeof(password_secret),
- password_salted_hashed,
- sizeof(password_salted_hashed));
+ password_secret, sizeof(password_secret), auth_password, sizeof(auth_password));
+ if (res) {
+ return res;
+ }
+
+ res =
+ _set_hmac_writeprotected(new_hmac_writeprotected_key, auth_password, sizeof(auth_password));
if (res) {
return res;
}
+
return 0;
}
bool optiga_reset_keys(void)
{
- // This resets the OID_AES_SYMKEY and OID_HMAC keys, as well as the OID_PASSWORD_SECRET and
- // OID_PASSWORD keys. A password is needed because updating the OID_PASSWORD key requires
- // auth using the OID_PASSWORD_SECRET key, but any password is fine for the purpose of resetting
- // the keys.
+ // This resets the OID_AES_SYMKEY and OID_HMAC/OID_HMAC_WRITEPROTECTED keys, as well as the
+ // OID_PASSWORD_SECRET and OID_PASSWORD keys. A password is needed because updating the
+ // OID_PASSWORD key requires auth using the OID_PASSWORD_SECRET key, but any password is fine
+ // for the purpose of resetting the keys.
// We reset using V1, the latest algorithm. It covers resetting everything from V0 as well.
return optiga_init_new_password("", MEMORY_PASSWORD_STRETCH_ALGO_V1) == 0;
@@ -1353,6 +1450,59 @@ cleanup: {
}
}
+static int _optiga_verify_password_v1(const uint8_t* auth_password, uint8_t* password_secret_out)
+{
+ uint8_t auth_password_salted_hashed[32] = {0};
+ if (!salt_hash_data(auth_password, 32, "optiga_password", auth_password_salted_hashed)) {
+ return SC_ERR_SALT;
+ }
+
+ optiga_lib_status_t res =
+ _authorize(OID_PASSWORD, auth_password_salted_hashed, sizeof(auth_password_salted_hashed));
+ if (res) {
+ goto cleanup;
+ }
+
+ uint16_t password_secret_size = 32;
+ res = optiga_ops_util_read_data_sync(
+ _util, OID_PASSWORD_SECRET, 0, password_secret_out, &password_secret_size);
+ if (res) {
+ goto cleanup;
+ }
+ if (password_secret_size != 32) {
+ res = SC_OPTIGA_ERR_UNEXPECTED_LEN;
+ goto cleanup;
+ }
+
+ res = _authorize(OID_PASSWORD_SECRET, password_secret_out, password_secret_size);
+ if (res) {
+ goto cleanup;
+ }
+
+ res = _reset_counter(OID_COUNTER_PASSWORD, SMALL_MONOTONIC_COUNTER_MAX_USE);
+ if (res) {
+ goto cleanup;
+ }
+
+ res = _reset_counter(OID_COUNTER_HMAC_WRITEPROTECTED, SMALL_MONOTONIC_COUNTER_MAX_USE);
+ if (res) {
+ goto cleanup;
+ }
+
+cleanup: {
+ optiga_lib_status_t res_clear1 = optiga_ops_crypt_clear_auto_state_sync(_crypt, OID_PASSWORD);
+ optiga_lib_status_t res_clear2 =
+ optiga_ops_crypt_clear_auto_state_sync(_crypt, OID_PASSWORD_SECRET);
+ if (res) {
+ return res;
+ }
+ if (res_clear1) {
+ return res_clear1;
+ }
+ return res_clear2;
+}
+}
+
#if VERIFY_METADATA == 1
static int _verify_metadata_config(void)
{
@@ -1561,12 +1711,12 @@ static int _stretch_password_v0(const char* password, uint8_t* stretched_out)
UTIL_CLEANUP_32(kdf_in);
memcpy(kdf_in, password_salted_hashed, 32);
- // First KDF on internal key increments the monotonic counter. Call only once!
+ // First KDF on internal key increments the large monotonic counter. Call only once!
int securechip_result = _kdf_internal(kdf_in, 32, stretched_out);
if (securechip_result) {
return securechip_result;
}
- // Second KDF does not use the counter and we call it multiple times.
+ // Second KDF does not use any counters and we call it multiple times.
for (int i = 0; i < KDF_NUM_ITERATIONS_V0; i++) {
memcpy(kdf_in, stretched_out, 32);
securechip_result = optiga_kdf_external(kdf_in, 32, stretched_out);
@@ -1602,10 +1752,36 @@ static int _stretch_password_v0(const char* password, uint8_t* stretched_out)
static int _stretch_password_v1(const char* password, uint8_t* stretched_out)
{
- // TODO implement
- (void)password;
- (void)stretched_out;
- return SC_ERR_INVALID_PASSWORD_STRETCH_ALGO;
+ uint8_t auth_password[32] = {0};
+ // Get auth password. This increments the small monotonic counter in
+ // `OID_COUNTER_HMAC_WRITEPROTECTED` and the large monotonic counter.
+ int res = _v1_get_auth_password(password, NULL, auth_password);
+ if (res) {
+ return res;
+ }
+ // Verify password incrementing the small monotonic counter in `OID_COUNTER_PASSWORD`.
+ uint8_t password_secret[32] = {0};
+ res = _optiga_verify_password_v1(auth_password, password_secret);
+ if (res) {
+ if (res == 0x802F) {
+ return SC_ERR_INCORRECT_PASSWORD;
+ }
+ return res;
+ }
+
+ rust_hmac_sha256(password_secret, sizeof(password_secret), stretched_out, 32, stretched_out);
+
+ uint8_t password_salted_hashed[32] = {0};
+ if (!salt_hash_data(
+ (const uint8_t*)password,
+ strlen(password),
+ "optiga_password_stretch_out",
+ password_salted_hashed)) {
+ return SC_ERR_SALT;
+ }
+ rust_hmac_sha256(
+ password_salted_hashed, sizeof(password_salted_hashed), stretched_out, 32, stretched_out);
+ return 0;
}
int optiga_stretch_password(
diff --git a/src/rust/bitbox02-rust/src/hal.rs b/src/rust/bitbox02-rust/src/hal.rs
index 1694506..f3ee968 100644
--- a/src/rust/bitbox02-rust/src/hal.rs
+++ b/src/rust/bitbox02-rust/src/hal.rs
@@ -512,16 +512,19 @@ pub mod testing {
_password: &str,
_password_stretch_algo: bitbox02::memory::PasswordStretchAlgo,
) -> Result<(), bitbox02::securechip::Error> {
- self.event_counter += 1;
+ self.event_counter += 3;
Ok(())
}
fn stretch_password(
&mut self,
password: &str,
- _password_stretch_algo: bitbox02::memory::PasswordStretchAlgo,
+ password_stretch_algo: bitbox02::memory::PasswordStretchAlgo,
) -> Result<zeroize::Zeroizing<Vec<u8>>, bitbox02::securechip::Error> {
- self.event_counter += 5;
+ self.event_counter += match password_stretch_algo {
+ bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V0 => 5,
+ bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V1 => 4,
+ };
use bitcoin::hashes::{HashEngine, Hmac, HmacEngine, sha256};
let mut engine = HmacEngine::<sha256::Hash>::new(b"unit-test");
@@ -573,7 +576,7 @@ pub mod testing {
self.reset_keys_fail_once = false;
Err(())
} else {
- self.event_counter += 1;
+ self.event_counter += 3;
Ok(())
}
}
@@ -588,7 +591,7 @@ pub mod testing {
impl TestingMemory {
pub fn new() -> Self {
Self {
- securechip_type: SecurechipType::Atecc,
+ securechip_type: SecurechipType::Optiga,
platform: bitbox02::memory::Platform::BitBox02,
initialized: false,
is_seeded: false,
diff --git a/src/rust/bitbox02-rust/src/hww/api/backup.rs b/src/rust/bitbox02-rust/src/hww/api/backup.rs
index 0cf6878..6c3cbf7 100644
--- a/src/rust/bitbox02-rust/src/hww/api/backup.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/backup.rs
@@ -237,7 +237,7 @@ mod tests {
)),
Ok(Response::Success(pb::Success {}))
);
- assert_eq!(mock_hal.securechip.get_event_counter(), 5);
+ assert_eq!(mock_hal.securechip.get_event_counter(), 4);
assert_eq!(
mock_hal.ui.screens,
vec![
diff --git a/src/rust/bitbox02-rust/src/hww/api/change_password.rs b/src/rust/bitbox02-rust/src/hww/api/change_password.rs
index bb96998..4e3b584 100644
--- a/src/rust/bitbox02-rust/src/hww/api/change_password.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/change_password.rs
@@ -164,7 +164,7 @@ mod tests {
]
);
// We expect 5 secure chip events (sensitive to code changes)
- assert_eq!(hal.securechip.get_event_counter(), 5);
+ assert_eq!(hal.securechip.get_event_counter(), 4);
// check that the old password is still valid
assert_eq!(
diff --git a/src/rust/bitbox02-rust/src/hww/api/restore.rs b/src/rust/bitbox02-rust/src/hww/api/restore.rs
index 7d9fb9d..43c5f99 100644
--- a/src/rust/bitbox02-rust/src/hww/api/restore.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/restore.rs
@@ -190,7 +190,7 @@ mod tests {
)),
Ok(Response::Success(pb::Success {}))
);
- assert_eq!(mock_hal.securechip.get_event_counter(), 8);
+ assert_eq!(mock_hal.securechip.get_event_counter(), 9);
assert!(!crate::keystore::is_locked());
assert!(mock_hal.memory.is_initialized());
diff --git a/src/rust/bitbox02-rust/src/hww/api/set_password.rs b/src/rust/bitbox02-rust/src/hww/api/set_password.rs
index 383d38a..09ace53 100644
--- a/src/rust/bitbox02-rust/src/hww/api/set_password.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/set_password.rs
@@ -69,7 +69,7 @@ mod tests {
)),
Ok(Response::Success(pb::Success {}))
);
- assert_eq!(mock_hal.securechip.get_event_counter(), 9);
+ assert_eq!(mock_hal.securechip.get_event_counter(), 10);
assert!(!keystore::is_locked());
assert!(keystore::copy_seed(&mut mock_hal).unwrap().len() == 32);
diff --git a/src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs b/src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs
index 6781a75..da423a5 100644
--- a/src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs
+++ b/src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs
@@ -143,7 +143,7 @@ mod tests {
block_on(process(&mut mock_hal)),
Ok(Response::Success(pb::Success {}))
);
- assert_eq!(mock_hal.securechip.get_event_counter(), 5);
+ assert_eq!(mock_hal.securechip.get_event_counter(), 4);
assert_eq!(
mock_hal.ui.screens,
@@ -196,7 +196,7 @@ mod tests {
mock_hal.securechip.event_counter_reset();
assert_eq!(block_on(process(&mut mock_hal)), Err(Error::Generic));
- assert_eq!(mock_hal.securechip.get_event_counter(), 5);
+ assert_eq!(mock_hal.securechip.get_event_counter(), 4);
assert_eq!(
mock_hal.ui.screens,
diff --git a/src/rust/bitbox02-rust/src/keystore.rs b/src/rust/bitbox02-rust/src/keystore.rs
index e52816e..6083005 100644
--- a/src/rust/bitbox02-rust/src/keystore.rs
+++ b/src/rust/bitbox02-rust/src/keystore.rs
@@ -219,8 +219,7 @@ fn default_password_stretch_algo(
Ok(bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V0)
}
bitbox02::memory::SecurechipType::Optiga => {
- // TODO: flip to V1 once implemented
- Ok(bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V0)
+ Ok(bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V1)
}
}
}
@@ -880,7 +879,7 @@ mod tests {
assert_eq!(
password_stretch_algo,
- bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V0
+ bitbox02::memory::PasswordStretchAlgo::MEMORY_PASSWORD_STRETCH_ALGO_V1
);
// Same as Python:
// import hmac, hashlib; hmac.digest(b"unit-test", b"password", hashlib.sha256).hex()
@@ -1112,7 +1111,7 @@ mod tests {
.as_slice(),
seed
);
- assert_eq!(mock_hal.securechip.get_event_counter(), 6);
+ assert_eq!(mock_hal.securechip.get_event_counter(), 5);
// Loop to check that unlocking works while unlocked.
for _ in 0..2 {
@@ -1125,7 +1124,7 @@ mod tests {
.as_slice(),
seed
);
- assert_eq!(mock_hal.securechip.get_event_counter(), 5);
+ assert_eq!(mock_hal.securechip.get_event_counter(), 4);
}
// Also check that the retained seed was encrypted with the expected encryption key.
@@ -1786,7 +1785,7 @@ mod tests {
mock_hal.securechip.event_counter_reset();
assert!(encrypt_and_store_seed(&mut mock_hal, seed, "foo").is_ok());
- assert_eq!(mock_hal.securechip.get_event_counter(), 7);
+ assert_eq!(mock_hal.securechip.get_event_counter(), 8);
assert!(is_locked());
diff --git a/src/rust/bitbox02-rust/src/reset.rs b/src/rust/bitbox02-rust/src/reset.rs
index 987cadb..f0cdf7f 100644
--- a/src/rust/bitbox02-rust/src/reset.rs
+++ b/src/rust/bitbox02-rust/src/reset.rs
@@ -101,7 +101,7 @@ mod tests {
block_on(reset(&mut hal, true));
// Secure chip operations happened as expected: reset_keys() was retried once, but only the
// successful call increments the event counter.
- assert_eq!(hal.securechip.get_event_counter(), 1);
+ assert_eq!(hal.securechip.get_event_counter(), 3);
// Keystore is locked again.
assert!(keystore::is_locked());
diff --git a/src/rust/bitbox02-rust/src/workflow/unlock.rs b/src/rust/bitbox02-rust/src/workflow/unlock.rs
index a67f23f..32a807b 100644
--- a/src/rust/bitbox02-rust/src/workflow/unlock.rs
+++ b/src/rust/bitbox02-rust/src/workflow/unlock.rs
@@ -241,7 +241,7 @@ mod tests {
mock_hal.securechip.event_counter_reset();
assert_eq!(block_on(unlock(&mut mock_hal)), Ok(()));
// 6 for keystore unlock, 1 for keystore bip39 unlock.
- assert_eq!(mock_hal.securechip.get_event_counter(), 7);
+ assert_eq!(mock_hal.securechip.get_event_counter(), 6);
assert!(!crate::keystore::is_locked());
@@ -291,7 +291,7 @@ mod tests {
)),
Err(UnlockError::IncorrectPassword),
));
- assert_eq!(mock_hal.securechip.get_event_counter(), 5);
+ assert_eq!(mock_hal.securechip.get_event_counter(), 4);
// Checks that the device is locked.
assert!(crate::keystore::copy_seed(&mut mock_hal).is_err());
diff --git a/test/hardware-fakes/src/fake_securechip.c b/test/hardware-fakes/src/fake_securechip.c
index edda6e1..7f8f0dc 100644
--- a/test/hardware-fakes/src/fake_securechip.c
+++ b/test/hardware-fakes/src/fake_securechip.c
@@ -23,6 +23,11 @@ int securechip_init_new_password(
const char* password,
memory_password_stretch_algo_t password_stretch_algo)
{
+ if (password_stretch_algo != MEMORY_PASSWORD_STRETCH_ALGO_V1) {
+ // New passwords must use the latest algo.
+ return SC_ERR_INVALID_PASSWORD_STRETCH_ALGO;
+ }
+
(void)password;
(void)password_stretch_algo;
return 0;
Why this scored 26/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.