- Add automatic re-hashing of legacy SHA-256 PINs to Argon2id on
successful verification, returning new hash to frontend for persistence
- Use constant-time comparison (subtle::ConstantTimeEq) for both
Argon2id and legacy SHA-256 hash verification
- Add unit tests for hash_pin, verify_pin (Argon2id and legacy paths),
re-hashing flow, error cases, and hex encoding roundtrip
- Update frontend to handle VerifyPinResult struct and save rehashed
PIN hash via profile update
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace SHA-256 with Argon2id (m=64MiB, t=3, p=1) for PIN hashing.
Existing SHA-256 hashes are verified transparently via format detection
(argon2id: prefix). New PINs are always hashed with Argon2id.
Addresses CWE-916: Use of Password Hash With Insufficient Computational Effort.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previous test refactor wrapped both keys in their respective DER
envelopes. CI surfaced the asymmetry: jsonwebtoken's two from_ed_der
constructors expect different inputs.
- EncodingKey::from_ed_der → PKCS#8 v1 wrapped (ring's
Ed25519KeyPair::from_pkcs8 path). The 16-byte prefix + 32-byte seed
blob is correct.
- DecodingKey::from_ed_der → raw 32-byte public key. Internally it
becomes ring's UnparsedPublicKey::new(&ED25519, key_bytes), which
takes the bare bytes, NOT a SubjectPublicKeyInfo wrapper.
The test was building an SPKI DER for the public key, so verification
saw a malformed key and failed every signature with InvalidSignature
(`accepts_well_formed_base_license` and `activation_token_matches_machine`).
Drop the SPKI helper, pass `signing_key.verifying_key().to_bytes()`
straight into DecodingKey::from_ed_der. Inline doc-comment captures
the asymmetry so the next person doesn't fall in the same hole.
cargo CI flagged: `unresolved import ed25519_dalek::pkcs8::LineEnding`. The
`LineEnding` re-export path varies between pkcs8/spki/der versions, so the
test code that called `to_pkcs8_pem(LineEnding::LF)` won't compile against
the dependency tree we get with ed25519-dalek 2.2 + pkcs8 0.10.
Fix:
- Drop the `pem` feature from the ed25519-dalek dev-dependency.
- In tests, build the PKCS#8 v1 PrivateKeyInfo and SubjectPublicKeyInfo
DER blobs manually from the raw 32-byte Ed25519 seed/public key. The
Ed25519 layout is fixed (16-byte prefix + 32-byte key) so this is short
and stable.
- Pass the resulting DER bytes to `EncodingKey::from_ed_der` /
`DecodingKey::from_ed_der`.
Refactor:
- Extract `strict_validation()` and `embedded_decoding_key()` helpers so
the validation config (mandatory exp/iat for CWE-613) lives in one
place and production callers all share the same DecodingKey constructor.
- `validate_with_key` and `validate_activation_with_key` now take a
`&DecodingKey` instead of raw PEM bytes; production builds the key
once via `embedded_decoding_key()`.
- New canary test `embedded_public_key_pem_parses` fails fast if the
embedded PEM constant ever becomes malformed.
Introduces the offline license infrastructure for the Base/Premium editions.
- jsonwebtoken (EdDSA) verifies license JWTs against an embedded Ed25519
public key. The exp claim is mandatory (CWE-613) and is enforced via
Validation::set_required_spec_claims.
- Activation tokens (server-issued, machine-bound) prevent license.key
copying between machines. Storage is wired up; the actual issuance flow
ships with Issue #49.
- get_edition() fails closed to "free" when the license is missing,
invalid, expired, or activated for a different machine.
- New commands/entitlements module centralizes feature → tier mapping so
Issue #48 (and any future gate) reads from a single source of truth.
- machine-uid provides the cross-platform machine identifier; OS reinstall
invalidates the activation token by design.
- Tests cover happy path, expiry, wrong-key signature, malformed JWT,
unknown edition, and machine_id matching for activation tokens.
The embedded PUBLIC_KEY_PEM is the RFC 8410 §10.3 test vector, clearly
labelled as a development placeholder; replacing it with the production
public key is a release-time task.
The previous approach deleted migration records to force re-application,
but this is dangerous for migration 2 which DELETEs all categories and
keywords before re-inserting them, wiping user customizations.
Now computes the expected SHA-384 checksum (matching sqlx) and updates
the stored checksum in _sqlx_migrations, so the migration is recognized
as already applied without being re-run.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add repair_migrations Tauri command that deletes stale migration 1
checksum from _sqlx_migrations before Database.load(). Migration 1
is idempotent (CREATE IF NOT EXISTS) so re-applying is safe.
Fixes "migration 1 was previously applied but has been modified".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each profile gets its own SQLite database file for complete data isolation.
Profile selection screen at launch, sidebar switcher for quick switching,
and optional 4-6 digit PIN for privacy. Existing database becomes the
default profile with seamless upgrade.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add export (JSON/CSV) and import (full replace) to the Settings page.
Export supports 3 modes (transactions+categories, transactions only,
categories only) with optional password encryption using Argon2id key
derivation. Import detects encrypted .sref files, prompts for password,
and shows a destructive confirmation modal before replacing data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>