Merge hardening: fuzz targets, cargo-deny policy, CI, two parser fixes
ci / test (push) Canceled after 0s
ci / deny (push) Canceled after 0s
ci / fuzz (push) Canceled after 0s

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-17 01:03:36 +09:00
co-authored by Claude Opus 5.5
27 changed files with 2670 additions and 12 deletions
+51 -6
View File
@@ -1,13 +1,58 @@
name: ci
on: [push, pull_request]
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
jobs:
test:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with: { components: "clippy,rustfmt" }
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: { persist-credentials: false }
- uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master, 2026-10
with: { toolchain: stable, components: "clippy, rustfmt" }
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
- run: cargo fmt --all --check
- run: cargo clippy --workspace --all-targets -- -D warnings
- run: cargo test --workspace
- uses: EmbarkStudios/cargo-deny-action@v2
deny:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: { persist-credentials: false }
- uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2.1.1
with: { command: check, arguments: --all-features }
# Builds every fuzz target and runs each for a minute from the checked-in seeds (fuzz/README.md).
fuzz:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with: { persist-credentials: false }
- uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master, 2026-10
with: { toolchain: nightly }
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with: { workspaces: fuzz }
- run: cargo install cargo-fuzz --locked
- name: Fuzz smoke run
working-directory: fuzz
run: |
for t in $(cargo +nightly fuzz list); do
mkdir -p "$RUNNER_TEMP/corpus/$t"
cargo +nightly fuzz run "$t" "$RUNNER_TEMP/corpus/$t" "seeds/$t" -- -max_total_time=60 -max_len=4096
done
- if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with: { name: fuzz-artifacts, path: fuzz/artifacts }
+9 -1
View File
@@ -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 {
+2 -2
View File
@@ -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))?;
+10 -1
View File
@@ -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"});
+9
View File
@@ -2,6 +2,13 @@
[advisories]
version = 2
yanked = "deny"
ignore = [
# Unmaintained (no known vulnerability), build-time only: pulled in by uniffi 0.28's proc-macros and
# bindgen, never linked into the shipped library's runtime paths. Goes away with a uniffi upgrade,
# which regenerates the Swift/Kotlin bindings (tracked in fuzz/README.md "Dependency policy").
{ id = "RUSTSEC-2025-0141", reason = "bincode 1.x via uniffi_macros 0.28 (build-time)" },
{ id = "RUSTSEC-2024-0436", reason = "paste via uniffi_core/uniffi_bindgen 0.28 (proc-macro)" },
]
[licenses]
version = 2
@@ -13,6 +20,8 @@ confidence-threshold = 0.9
[bans]
multiple-versions = "warn"
wildcards = "deny"
# The workspace's own crates are path dependencies without a version; they are never published.
allow-wildcard-paths = true
[sources]
unknown-registry = "deny"
+5
View File
@@ -0,0 +1,5 @@
target/
corpus/
artifacts/
coverage/
logs/
+2266
View File
File diff suppressed because it is too large Load Diff
+66
View File
@@ -0,0 +1,66 @@
[package]
name = "kestrove-core-fuzz"
version = "0.0.0"
publish = false
edition = "2021"
[package.metadata]
cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4"
serde_json = "1"
uuid = "1"
kv-proto = { path = "../crates/kv-proto" }
kv-crypto = { path = "../crates/kv-crypto" }
kv-model = { path = "../crates/kv-model" }
kv-sync = { path = "../crates/kv-sync" }
# Not part of the core workspace (crates/*); built only by `cargo +nightly fuzz`.
[workspace]
members = ["."]
[profile.release]
debug = 1
[[bin]]
name = "envelope_json"
path = "fuzz_targets/envelope_json.rs"
test = false
doc = false
bench = false
[[bin]]
name = "manifest_decode"
path = "fuzz_targets/manifest_decode.rs"
test = false
doc = false
bench = false
[[bin]]
name = "item_replay"
path = "fuzz_targets/item_replay.rs"
test = false
doc = false
bench = false
[[bin]]
name = "secret_key_parse"
path = "fuzz_targets/secret_key_parse.rs"
test = false
doc = false
bench = false
[[bin]]
name = "totp_parse"
path = "fuzz_targets/totp_parse.rs"
test = false
doc = false
bench = false
[[bin]]
name = "url_match"
path = "fuzz_targets/url_match.rs"
test = false
doc = false
bench = false
+133
View File
@@ -0,0 +1,133 @@
# Fuzzing and hardening report
libFuzzer targets (via `cargo-fuzz`) for every parser that sees untrusted input: data from the server,
the browser, the user, or the network. This file also records the October 2026 hardening pass across
all repos: fuzzing, dependency policy, CI and the deploy check.
## Running
```sh
rustup toolchain install nightly && cargo install cargo-fuzz --locked
cd fuzz
cargo +nightly fuzz list
mkdir -p corpus/<target>
cargo +nightly fuzz run <target> corpus/<target> seeds/<target> -- -max_total_time=600
```
`seeds/` holds a few valid inputs per target and is committed. `corpus/`, `artifacts/` and `logs/` are
gitignored. CI runs every target for 60 s from the seeds (the `fuzz` job in `.github/workflows/ci.yml`).
A crash input lands in `artifacts/<target>/`. Reproduce it with `cargo +nightly fuzz run <target> <file>`.
The server and desktop targets live in their own repos (`kestrove-server/fuzz`,
`kestrove-desktop/src-tauri/fuzz`) and run the same way.
## Targets and results (2026-10-16, Apple M-series, 1 job per target, ASan)
| Target | Code under test | Checks beyond "no panic" | Time | Execs | Coverage (edges) | Result |
|---|---|---|---|---|---|---|
| `envelope_json` | `kv_proto::Envelope` JSON decode | accepted ⇒ re-encode/decode round-trips | 8 min | 36.9 M | 2002 | clean |
| `manifest_decode` | `kv_sync::manifest::ManifestBody::decode` (strict canonical CBOR) | accepted ⇒ input is exactly `encode()` | 8 min | 2.1 M | 711 | clean |
| `item_replay` | item plaintext → `serde_json::Value` → `kv_model::Item`; `replay::check_pulled` / `check_successor` / `next_version`; `merge::merge3` | identical record never a replay; `next_version > seq` | 8 + 8 min | 1.5 M + 2.9 M | 2248 | **bug 2, fixed** |
| `secret_key_parse` | `kv_crypto::secret_key::SecretKey::parse` | accepted ⇒ display form parses to the same key | 8 + 8 min | 19.7 M | 423 | **bug 1, fixed** |
| `totp_parse` | `kv_model::totp::Totp::parse` (otpauth:// and bare base32), `code_at`, `remaining` | code has `digits` ASCII digits; 1 ≤ remaining ≤ period | 8 min | 3.0 M | 1688 | clean |
| `url_match` | `kv_model::match_url` | an https item never matches an http page | 8 min | 2.7 M | 15307 | clean |
| server `signature_headers` | `Signature-Input`, `Signature`, `Content-Digest` parsers (`kestrove-server/src/sigparse.rs`) | accepted ⇒ header == `kv=` + params; nonce valid; fixed component list | 10 min | 59.0 M | 696 | clean |
| desktop `nmh_session` | whole `kestrove-nmh` connection into `Broker::serve`: framing, outer JSON, `nmh_hello` / `ping` / `pair_init` / `pair_confirm` / `open` / `msg` | every reply is a valid frame with a `t` field | 8 min | 2.5 M | 2082 | clean |
| desktop `frame_read` | `framing::read_frame` (u32 length + payload, 1 MiB cap) | frame ≤ `MAX_FRAME`; re-framing round-trips | 8 min | 2.7 M | 63 | clean |
### Bug 1: `SecretKey::parse` panicked on non-ASCII input (fixed)
`parse` stripped separators, checked that the remainder was 27 **bytes**, then called `split_at(26)`. A
multibyte character straddling byte 26 panicked (`end byte index 26 is not a char boundary`). Release
builds use `panic = "abort"`, so a mistyped or pasted Secret Key containing, say, a Cyrillic `е` would
have crashed the app through FFI instead of showing "invalid Secret Key".
Fix: reject non-ASCII before the length check (one line in `crates/kv-crypto/src/secret_key.rs`).
Regression test: `secret_key::tests::non_ascii_is_rejected_without_panicking`, which uses the fuzzer's
crash input. It fails without the fix. Commit: "kv-crypto: SecretKey::parse rejects non-ASCII input
instead of panicking".
### Bug 2: `seq` + 1 overflowed at `u64::MAX` (fixed)
`replay::next_version` and the engine's two merge paths (`engine.rs`, "edit beats delete" and the
three-way merge) computed `max(seq) + 1` on `seq` values read from decrypted plaintext. A version carrying
`"seq": 18446744073709551615` panicked in debug and fuzz builds. In release, which has no overflow checks,
it wrapped to `seq` 0, so the device's next save looked like a replay to every other device (an
integrity alarm). The 8-minute random run didn't produce a 20-digit number. A targeted input, written while
checking the target's `next_version` assertion, triggered it at once. Reaching it needs an authentic
plaintext (one written by a device holding the vault key), so severity is low.
Fix: `saturating_add(1)` in the three places. Regression test:
`replay::tests::next_version_saturates_at_u64_max`. The input is now a seed (`seeds/item_replay/seq_max`),
so CI's smoke run covers it. Commit: "kv-sync: saturate seq increments instead of overflowing at
u64::MAX".
`kv-ffi`'s `save_item` has the same pattern on `modified_at` (`p.modified_at + 1`, i64). In release it
wraps to `i64::MIN` and `now.max(…)` absorbs it, so it's harmless there. It was left alone because the
passkeys branch is changing `kv-ffi`. Worth a `saturating_add` when that merges.
### Notes
- The desktop's `msg` path only reaches `handle_request` with a valid AES-GCM session, so the fuzzer covers
the session layer's rejection paths, not request handling. That is the expected boundary: an
unauthenticated peer can't get past it.
- To fuzz `parse_signature_input`, the pure header parsers moved from `kestrove-server/src/auth.rs` into
`src/sigparse.rs` unchanged. The server is a bin-only crate, and the fuzz crate includes that file by path.
## Dependency policy
| Repo | Check | Result |
|---|---|---|
| kestrove-core | `cargo deny check` | passes. `allow-wildcard-paths` for the workspace's own path crates. Ignored: RUSTSEC-2025-0141 (bincode 1.x) and RUSTSEC-2024-0436 (paste), both *unmaintained* notices for build-time crates pulled in by uniffi 0.28's proc-macros and bindgen. **Not fixable here:** the fix is a uniffi upgrade, which regenerates the Swift and Kotlin bindings for both apps. |
| kestrove-server | `cargo deny check` (new `deny.toml`) | passes, no ignores |
| kestrove-desktop/src-tauri | `cargo deny check` (new `deny.toml`) | passes. Also allows BSL-1.0 (clipboard-win/error-code via arboard on Windows) and CDLA-Permissive-2.0 (webpki-roots). Ignores the two uniffi notices plus RUSTSEC-2024-0370 (proc-macro-error via gtk-rs glib-macros 0.18, Tauri's Linux stack, build-time). **Not fixable here:** it goes away when Tauri moves to gtk-rs ≥ 0.19 / gtk4. |
| kestrove-browser | `npm audit --omit=dev` | 0 vulnerabilities. Dev-only: 1 moderate, esbuild ≤ 0.24.2 dev-server CORS issue (GHSA-67mh-4wv8-2f99). It only matters for `esbuild serve`, which the build script doesn't use, and none of it ships. |
| kestrove-desktop | `npm audit --omit=dev` and `npm audit` | 0 vulnerabilities |
| kestrove-android | Gradle dependency verification | new `gradle/verification-metadata.xml` pins sha256 for all 388 components used by `assembleDebug test lint`. The Linux and Windows `aapt2` jars were added by hand: the macOS hash Gradle recorded matched dl.google.com. The 106 release-runtime artifacts were checked against OSV.dev: 0 advisories. |
## CI (all repos)
All workflows pass `actionlint` (with shellcheck). Actions are pinned to commit SHAs with the version in a
comment, `permissions: contents: read`, `persist-credentials: false`. Sibling repos are checked out side by
side using `secrets.CORE_READ_TOKEN || github.token`. Add that secret (a fine-grained token with read
access) if the repos are private.
- **server, desktop:** the old workflows were invalid YAML (an unquoted `${{ }}` inside a `{ }` flow
mapping), so GitHub would have rejected them and never run anything.
- **core:** adds a fuzz smoke job. Also fixed `scripts/build-android.sh`, which ran uniffi-bindgen on
`libkv_ffi.dylib`, a file that doesn't exist on the Linux CI runner (`.so`).
- **server:** adds fmt, cargo-deny, the fuzz smoke job and a `docker build` of the deploy image.
- **desktop:** adds fmt and cargo-deny + `npm audit`. The code had never been through rustfmt, so this
commit adds a `rustfmt.toml` (the same one as core and server) and a formatting-only commit.
- **apple:** adds an `app` job: core → XCFramework, Safari JS, xcodegen, unsigned xcodebuild for iOS
Simulator and macOS. Ran it locally (Xcode 27): both builds succeed. `swift test` failed once under heavy
load and then passed 4 times in a row. The `SyncController` tests in `SyncTests.swift` rely on 100 ms
debounces and fixed sleeps, so expect flakes on slow CI runners (follow-up).
- **android:** gives the core checkout a token, adds Rust Android targets, `setup-gradle` (wrapper
validation) and lint report upload.
- **browser:** Node 24 (as used locally), plus `npm audit --omit=dev`.
## Deploy check (kestrove-server/deploy, Docker Desktop 29.4, DOMAIN=localhost)
Passed after these fixes:
1. **The image never built.** kv-proto inherits `edition`/deps from `kestrove-core/Cargo.toml`, which the
Dockerfile didn't copy (`error inheriting edition from workspace root`). Now copied. Also removed the
`--locked || cargo build` fallback that silently ignored the lockfile, and added
`Dockerfile.dockerignore`: the build context is the parent folder, and every `target/` and
`node_modules/` was being sent to the daemon.
2. **`docker stop` SIGKILLed the server** (exit 137). It only listened for Ctrl-C, and as PID 1 it has no
default SIGTERM action. It now shuts down on SIGTERM too and closes the pool, so the WAL is
checkpointed (exit 0, single `kestrove.db` left).
3. **backup.sh never handed restic its credentials.** It used `source` without `export`, so `RESTIC_*`
never reached restic. Fixed with `set -a`. It now also runs `sqlite3` as the volume owner (uid 65532),
so it can't leave root-owned `-wal`/`-shm` files. It runs `PRAGMA integrity_check` on the copy before
uploading, and `KV_VOLUME` overrides the volume name.
4. Smaller changes: `tmpfs: /tmp` for SQLite temp files under `read_only: true`, and Caddy drops `Via`.
Verified: `GET https://localhost/v1/server-info` → 200 over HTTP/2 via Caddy's internal CA, with HSTS,
nosniff and no-referrer. `/` → 404. `http://` → 308 to https. TLS 1.2 is refused. `SETUP_TOKEN=…` is
printed on first start. The container runs as uid/gid 65532 with `ReadonlyRootfs=true`, `CapDrop=[ALL]`
and `no-new-privileges`. Data files are owned by 65532. backup.sh ran in a Linux container standing in for
the VM host (real `sqlite3`, `docker volume inspect` and `restic` stubbed) while the server was live: the
`.backup` copy passed `integrity_check` with all migrations present.
+12
View File
@@ -0,0 +1,12 @@
//! kv-proto Envelope JSON decode: anything accepted must re-encode and decode to the same value.
#![no_main]
use kv_proto::Envelope;
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
if let Ok(e) = serde_json::from_slice::<Envelope>(data) {
let again = serde_json::to_vec(&e).expect("re-encode");
let back: Envelope = serde_json::from_slice(&again).expect("decode own encoding");
assert_eq!(back, e);
}
});
+35
View File
@@ -0,0 +1,35 @@
//! Decrypted item plaintext: JSON parse, Item model parse, and the per-item replay checks and merge that
//! run on every pulled record. Input: two plaintexts (held base, incoming) separated by a 0xFF byte.
#![no_main]
use kv_model::Item;
use kv_sync::{merge::merge3, replay};
use libfuzzer_sys::fuzz_target;
use serde_json::Value;
use uuid::Uuid;
fuzz_target!(|data: &[u8]| {
let (a, b) = match data.iter().position(|&c| c == 0xff) {
Some(i) => (&data[..i], &data[i + 1..]),
None => (data, &data[..0]),
};
let Ok(incoming) = serde_json::from_slice::<Value>(a) else { return };
let _ = serde_json::from_value::<Item>(incoming.clone());
let id = Uuid::from_u128(1);
let held: Option<Value> = serde_json::from_slice(b).ok();
let base = match &held {
Some(v) => replay::Base::Sealed(v),
None => replay::Base::LegacyTombstone,
};
for deleted in [false, true] {
let _ = replay::check_pulled(base, id, deleted, Some(&incoming));
}
// An identical record is never a replay.
assert!(replay::check_successor(replay::Base::Sealed(&incoming), &incoming).is_ok());
let next = replay::next_version(Some(&incoming), base);
assert!(next > replay::seq_of(&incoming) || replay::seq_of(&incoming) == u64::MAX);
if let Some(h) = &held {
let _ = merge3(None, h, &incoming);
let _ = merge3(Some(h), &incoming, h);
let _ = replay::same_content(h, &incoming);
}
});
+10
View File
@@ -0,0 +1,10 @@
//! kv-sync ManifestBody::decode (strict canonical CBOR): accepted input must be exactly its own encoding.
#![no_main]
use kv_sync::manifest::ManifestBody;
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
if let Ok(body) = ManifestBody::decode(data) {
assert_eq!(body.encode(), data, "non-canonical input accepted");
}
});
+12
View File
@@ -0,0 +1,12 @@
//! kv-crypto SecretKey::parse: never panics; an accepted key round-trips through its display form.
#![no_main]
use kv_crypto::secret_key::SecretKey;
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let Ok(s) = std::str::from_utf8(data) else { return };
if let Ok(k) = SecretKey::parse(s) {
let again = SecretKey::parse(&k.to_display()).expect("parse own display form");
assert_eq!(again.expose(), k.expose());
}
});
+19
View File
@@ -0,0 +1,19 @@
//! kv-model TOTP / otpauth:// parse, then code generation at fuzzed times.
#![no_main]
use kv_model::totp::Totp;
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
if data.len() < 8 {
return;
}
let t = u64::from_le_bytes(data[..8].try_into().unwrap());
let Ok(s) = std::str::from_utf8(&data[8..]) else { return };
if let Ok(totp) = Totp::parse(s) {
let code = totp.code_at(t);
assert_eq!(code.len(), totp.digits as usize);
assert!(code.bytes().all(|c| c.is_ascii_digit()));
let r = totp.remaining(t);
assert!(r >= 1 && r <= totp.period);
}
});
+17
View File
@@ -0,0 +1,17 @@
//! kv-model URL matching: never panics, a URL always matches itself at least at host level, and an
//! https item never matches an http page.
#![no_main]
use kv_model::{match_url, MatchLevel};
use libfuzzer_sys::fuzz_target;
fuzz_target!(|data: &[u8]| {
let Ok(s) = std::str::from_utf8(data) else { return };
let (a, b) = s.split_once('\n').unwrap_or((s, ""));
let m = match_url(a, b);
if b.trim_start().to_ascii_lowercase().starts_with("http://")
&& a.trim_start().to_ascii_lowercase().starts_with("https://")
{
assert_eq!(m, MatchLevel::None, "downgrade matched: {a:?} -> {b:?}");
}
let _ = match_url(a, a);
});
+1
View File
@@ -0,0 +1 @@
{"v":1,"alg":"hpke-x25519-chacha","kid":"00000000-0000-0000-0000-000000000001","ct":"AAEC","enc":"AAEC"}
+1
View File
@@ -0,0 +1 @@
{"v":1,"alg":"xc20p","kid":"00000000-0000-0000-0000-000000000001","n":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA","ct":"AAEC"}
+1
View File
@@ -0,0 +1 @@
{"id":"00000000-0000-0000-0000-000000000001","kind":"login","title":"t","username":"u","password":"p","urls":["https://example.com"],"created_at":1,"modified_at":2,"seq":3}ÿ{"id":"00000000-0000-0000-0000-000000000001","kind":"login","title":"t","username":"u","password":"p","urls":["https://example.com"],"created_at":1,"modified_at":2,"seq":2}
+1
View File
@@ -0,0 +1 @@
{"id":"00000000-0000-0000-0000-000000000001","seq":18446744073709551615}˙{"seq":18446744073709551614}
+1
View File
@@ -0,0 +1 @@
{"id":"00000000-0000-0000-0000-000000000001","seq":4,"deleted":true}ÿ{"id":"00000000-0000-0000-0000-000000000001","kind":"login","title":"t","username":"u","password":"p","urls":["https://example.com"],"created_at":1,"modified_at":2,"seq":3}
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
K1-000000-00000-00000-00000-00000-0
+1
View File
@@ -0,0 +1 @@
JBSW Y3DP EHPK 3PXP
Binary file not shown.
+2
View File
@@ -0,0 +1,2 @@
https://login.example.co.uk:8443/a
example.co.uk
+2
View File
@@ -0,0 +1,2 @@
http://[::1]:80/
https://xn--bcher-kva.example/
+3 -2
View File
@@ -5,6 +5,7 @@ cd "$(dirname "$0")/.."
OUT=../kestrove-android/core-ffi/src/main
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 -o "$OUT/jniLibs" build -p kv-ffi --release
cargo build -p kv-ffi
cargo run -p kv-ffi --bin uniffi-bindgen -- generate --library target/debug/libkv_ffi.dylib --language kotlin --out-dir "$OUT/java"
cargo build -p kv-ffi # host library for library-mode bindgen (.so on Linux CI, .dylib on macOS)
case "$(uname -s)" in Darwin) HOST_LIB=libkv_ffi.dylib ;; *) HOST_LIB=libkv_ffi.so ;; esac
cargo run -p kv-ffi --bin uniffi-bindgen -- generate --library "target/debug/$HOST_LIB" --language kotlin --out-dir "$OUT/java"
echo "✅ Android libs + bindings written to $OUT"