Merge hardening: fuzz targets, cargo-deny policy, CI, two parser fixes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -47,7 +47,8 @@ impl SecretKey {
|
||||
s.chars().filter(|c| !c.is_whitespace() && *c != '-').map(|c| c.to_ascii_uppercase()).collect(),
|
||||
);
|
||||
let rest = cleaned.strip_prefix(PREFIX).ok_or(CryptoError::BadSecretKey)?;
|
||||
if rest.len() != 27 {
|
||||
// ASCII only: `split_at` below works on bytes and must not land inside a character.
|
||||
if !rest.is_ascii() || rest.len() != 27 {
|
||||
return Err(CryptoError::BadSecretKey);
|
||||
}
|
||||
let (body, check) = rest.split_at(26);
|
||||
@@ -96,6 +97,13 @@ fn decode(s: &str) -> Result<[u8; 16]> {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn non_ascii_is_rejected_without_panicking() {
|
||||
// Found by fuzz/secret_key_parse: 27 bytes whose 26th..27th byte is a Cyrillic 'е'.
|
||||
assert!(SecretKey::parse("K1-10Y5-00Y0-000000p00-000000p0\u{435}-").is_err());
|
||||
assert!(SecretKey::parse("K1-000000-00000-00000-00000-0000\u{e9}-0").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn roundtrip() {
|
||||
for _ in 0..200 {
|
||||
|
||||
@@ -379,7 +379,7 @@ impl SyncContext<'_> {
|
||||
open_record(acct, vault, vk, &local.record)?.ok_or(SyncError::Protocol("empty item".into()))?;
|
||||
let tomb =
|
||||
open_sealed(acct, vault, vk, remote)?.ok_or(SyncError::Protocol("empty tombstone".into()))?;
|
||||
let seq = replay::seq_of(&mine).max(replay::seq_of(&tomb)) + 1;
|
||||
let seq = replay::seq_of(&mine).max(replay::seq_of(&tomb)).saturating_add(1);
|
||||
replay::stamp(&mut mine, seq);
|
||||
let sealed = seal_record(acct, vault, vk, id, &mine, self.device_id)?;
|
||||
self.store.put(vault, &rebased(sealed))?;
|
||||
@@ -408,7 +408,7 @@ impl SyncContext<'_> {
|
||||
if replay::same_content(&out.merged, &theirs) {
|
||||
self.store.put_remote(vault, remote)?;
|
||||
} else {
|
||||
let seq = replay::seq_of(&mine).max(replay::seq_of(&theirs)) + 1;
|
||||
let seq = replay::seq_of(&mine).max(replay::seq_of(&theirs)).saturating_add(1);
|
||||
replay::stamp(&mut out.merged, seq);
|
||||
let sealed = seal_record(acct, vault, vk, id, &out.merged, self.device_id)?;
|
||||
self.store.put(vault, &rebased(sealed))?;
|
||||
|
||||
@@ -113,7 +113,8 @@ pub fn next_version(local: Option<&Value>, base: Base<'_>) -> u64 {
|
||||
Base::Sealed(v) => seq_of(v),
|
||||
_ => 0,
|
||||
};
|
||||
local.map_or(0, seq_of).max(held) + 1
|
||||
// Saturates: `seq` comes from decrypted plaintext, and a version at u64::MAX must not panic or wrap to 0.
|
||||
local.map_or(0, seq_of).max(held).saturating_add(1)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -122,6 +123,14 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn next_version_saturates_at_u64_max() {
|
||||
// A plaintext carrying seq = u64::MAX used to overflow (panic in debug, wrap to 0 in release).
|
||||
let max = json!({"title": "a", "seq": u64::MAX});
|
||||
assert_eq!(next_version(Some(&max), Base::None), u64::MAX);
|
||||
assert_eq!(next_version(None, Base::Sealed(&max)), u64::MAX);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stamp_and_same_content() {
|
||||
let mut v = json!({"title": "a"});
|
||||
|
||||
Reference in New Issue
Block a user