fix password issue
ober
07e3948cf8bd4b0bfd81d23be7bea19227b47628
--- a/.gitignore +++ b/.gitignore @@ -17,6 +17,8 @@ signal_tui_shim.dylib signal_tui_shim.so signal_log_shim.dylib signal_log_shim.so +signal_security_shim.dylib +signal_security_shim.so *.db *.trace /trace --- a/signal/logdb.ss +++ b/signal/logdb.ss @@ -338,6 +338,34 @@ [sealed (log-aead-seal key nonce plain aad)]) (bv-append *jlog-magic* salt generation-bytes sealed))) + ;; Pre-hardening (before P1 #38 / commit ef4e7c3) container format: magic || + ;; salt || nonce(12, random) || AEAD(plain, aad = magic only). Same magic + ;; prefix as the current format, so jlog-container-bytes? cannot distinguish + ;; them by inspection alone. decrypt-container tries the current + ;; (generation-counter) format first and only falls back to this one when + ;; that AEAD open fails, so a genuinely wrong passphrase still fails closed + ;; against both formats. Returns #f (not this format / wrong passphrase) or + ;; (list key salt plain). + (def (try-decrypt-legacy-nonce-container bytes passphrase) + (let* ([magic-len (bytevector-length *jlog-magic*)] + [need (+ magic-len *jlog-salt-len* *jlog-nonce-len*)]) + (and (>= (bytevector-length bytes) need) + (let* ([salt (bv-sub bytes magic-len *jlog-salt-len*)] + [nonce (bv-sub bytes (+ magic-len *jlog-salt-len*) + *jlog-nonce-len*)] + [sealed-start (+ magic-len *jlog-salt-len* *jlog-nonce-len*)] + [sealed (bv-sub bytes sealed-start + (- (bytevector-length bytes) sealed-start))] + [key (derive-key passphrase salt)]) + (guard (e [(condition? e) #f]) + (list key salt (log-aead-open key nonce sealed *jlog-magic*))))))) + + ;; A 5th legacy-nonce-format? value flags a container decrypted via the + ;; pre-hardening fallback above; callers that persist a real generation + ;; counter (open-jlog-container) must migrate on this, and callers that + ;; should never see it (incremental checkpoints, always written in the + ;; current format) must fail closed instead of trusting the placeholder 0 + ;; generation. (def (decrypt-container bytes passphrase) (let* ([magic-len (bytevector-length *jlog-magic*)] [need (+ magic-len *jlog-salt-len* *jlog-generation-len*)]) @@ -352,10 +380,16 @@ [sealed (bv-sub bytes sealed-start (- (bytevector-length bytes) sealed-start))] [key (derive-key passphrase salt)] - [aad (container-aad salt nonce generation-bytes)] - [plain (guard (e [(condition? e) (raise-passphrase-auth-error)]) - (log-aead-open key nonce sealed aad))]) - (values key salt generation plain)))) + [aad (container-aad salt nonce generation-bytes)]) + (guard (e [(condition? e) + (let ([legacy (try-decrypt-legacy-nonce-container + bytes passphrase)]) + (if legacy + (values (list-ref legacy 0) (list-ref legacy 1) 0 + (list-ref legacy 2) #t) + (raise-passphrase-auth-error)))]) + (values key salt generation (log-aead-open key nonce sealed aad) + #f))))) ;; Anti-rollback high-water mark stored OUTSIDE the mutable container file. (def (generation-marker-path path) @@ -649,9 +683,10 @@ (make-logdb-handle 'jsqlite log))) (def (open-jlog-container path passphrase bytes) - (let-values ([(key salt generation plain) + (let-values ([(key salt generation plain legacy-nonce?) (decrypt-container bytes passphrase)]) - (let ([marker (read-generation-marker path)]) + (let* ([marker (read-generation-marker path)] + [generation (if legacy-nonce? (+ marker 1) generation)]) (when (< generation marker) (error 'logdb-open "encrypted log generation regression detected (rollback)" @@ -668,6 +703,16 @@ (make-bytevector *jlog-chain-hash-len* 0) 0 0 0 #f)]) (start-jlog-flusher! log) + ;; Re-seal immediately under the current format so the + ;; pre-hardening random-nonce container never persists past its + ;; first successful open. + (when legacy-nonce? + (display + (string-append + "jerboa-signal: message log is in a pre-hardening" + " container format; migrating to the current format...\n") + (current-error-port)) + (persist-jlog-legacy! log)) (make-logdb-handle 'jsqlite log))))))) ;; Open an incremental log: decrypt the head container (fails closed on a wrong @@ -691,8 +736,13 @@ (let ([ckpt-bytes (read-file-bytevector* (checkpoint-path path))]) (unless (jlog-container-bytes? ckpt-bytes) (error 'logdb-open "incremental log checkpoint missing" path)) - (let-values ([(ckpt-key ckpt-salt ckpt-gen ckpt-plain) + (let-values ([(ckpt-key ckpt-salt ckpt-gen ckpt-plain + ckpt-legacy-nonce?) (decrypt-container ckpt-bytes passphrase)]) + (when ckpt-legacy-nonce? + (error 'logdb-open + "incremental log checkpoint has an unexpected container format" + path)) (unless (= ckpt-gen ckpt-generation) (error 'logdb-open "incremental log checkpoint generation mismatch" --- a/signal/tui/main.ss +++ b/signal/tui/main.ss @@ -2940,17 +2940,32 @@ ;; --- @mention support --- - ;; All direct conversation titles (contact names) as mention candidates. + ;; Mention candidates scoped to the currently open conversation only: for a + ;; direct chat, the other party; for a group, the distinct senders seen so + ;; far in that conversation. There is no group roster API wired up (see + ;; docs/full-port.md #15, "Groups V2" member/role tracking is still + ;; unimplemented), so message history is the best available proxy for + ;; "who's in this room." (def (mention-candidates state) - (let loop ([convs (tui-state-conversations state)] - [acc '()]) + (let ([conv (selected-conversation state)]) + (if (not conv) + '() + (case (conversation-kind conv) + [(direct) (list (conversation-title conv))] + [(group) (conversation-participant-senders conv)] + [else '()])))) + + (def (conversation-participant-senders conv) + (let loop ([msgs (conversation-messages conv)] [acc '()]) (cond - [(null? convs) (reverse acc)] + [(null? msgs) (reverse acc)] [else - (let ([conv (car convs)]) - (loop (cdr convs) - (if (eq? (conversation-kind conv) 'direct) - (cons (conversation-title conv) acc) + (let ([sender (chat-message-sender (car msgs))]) + (loop (cdr msgs) + (if (and (non-empty-string? sender) + (not (string=? sender "You")) + (not (member sender acc))) + (cons sender acc) acc)))]))) ;; Filter candidates by the partial query after the @. --- a/tests/test-logdb-crypto.ss +++ b/tests/test-logdb-crypto.ss @@ -14,7 +14,8 @@ make-date make-time) (except (jerboa prelude) meta atom?) (only (std security taint) safe-delete-file) - (signal logdb)) + (signal logdb) + (signal log_crypto)) (def (check label pred) (unless pred @@ -130,6 +131,71 @@ #f))) (clean! p)) +;; Pre-hardening compat (regression for the format break found 2026-07-23): +;; a container in the format that predates commit ef4e7c3 (magic || salt || +;; nonce(12, random) || AEAD(plain, aad=magic), no generation field) must +;; still open with the correct passphrase, self-migrating to the current +;; generation-counter format, and must still fail closed with a wrong one. +(def *legacy-salt-len* 16) +(def *legacy-nonce-len* 12) +(def *legacy-key-len* 32) +(def *legacy-scrypt-n* 16384) +(def *legacy-scrypt-r* 8) +(def *legacy-scrypt-p* 1) +(def *legacy-magic* (string->utf8 "JSQLITELOGv1\n")) + +(def (write-legacy-nonce-container! p passphrase) + (let* ([salt (log-random-bytes *legacy-salt-len*)] + [nonce (log-random-bytes *legacy-nonce-len*)] + [key (log-scrypt-key passphrase salt *legacy-key-len* + *legacy-scrypt-n* *legacy-scrypt-r* + *legacy-scrypt-p*)] + [sealed (log-aead-seal key nonce (make-bytevector 0 0) *legacy-magic*)] + [container (let* ([total (+ (bytevector-length *legacy-magic*) + *legacy-salt-len* *legacy-nonce-len* + (bytevector-length sealed))] + [out (make-bytevector total 0)]) + (bytevector-copy! *legacy-magic* 0 out 0 + (bytevector-length *legacy-magic*)) + (bytevector-copy! salt 0 out + (bytevector-length *legacy-magic*) + *legacy-salt-len*) + (bytevector-copy! nonce 0 out + (+ (bytevector-length *legacy-magic*) + *legacy-salt-len*) + *legacy-nonce-len*) + (bytevector-copy! sealed 0 out + (+ (bytevector-length *legacy-magic*) + *legacy-salt-len* *legacy-nonce-len*) + (bytevector-length sealed)) + out)]) + (write-file-bytevector! p container))) + +(let ([p "/tmp/jerboa-signal-logdb-legacy-nonce-test.db"]) + (clean! p) + (write-legacy-nonce-container! p "legacy horse") + (check "legacy-nonce container opens and migrates with correct passphrase" + (let ([h (logdb-open p "legacy horse")]) + (and h (begin (logdb-close h) #t)))) + (check "migrated container now opens via the current format (no fallback needed)" + (let ([h (logdb-open p "legacy horse")]) + (and h (begin (logdb-close h) #t)))) + (check "migrated container has a real (small) generation counter" + (< (container-generation (read-file-bytevector p)) 1000)) + (clean! p)) + +(let ([p "/tmp/jerboa-signal-logdb-legacy-nonce-authfail-test.db"]) + (clean! p) + (write-legacy-nonce-container! p "legacy horse") + (check "legacy-nonce container still fails closed on wrong passphrase" + (guard (e [(condition? e) + (and (who-condition? e) + (eq? (condition-who e) 'logdb-passphrase-mismatch))]) + (let ([h (logdb-open p "wrong horse")]) + (when h (logdb-close h)) + #f))) + (clean! p)) + ;; Key zeroing: jlog-close! zeroes the derived AEAD key bytevector. (let ([p "/tmp/jerboa-signal-logdb-keyzero-test.db"]) (clean! p)