Use raw X25519 HPKE keys and sealing#3
Merged
Merged
Conversation
Add support for publishing and sealing to raw 32-byte X25519 HPKE public keys while retaining Tink private keysets. Hpke now exposes rawPublicKey and sealToRaw, and seal dispatches by length (32 bytes → raw path, otherwise Tink keyset). AppGraph and RotationManager publish the raw public key bytes for cross-platform (Tink-free) peers; EnvelopeCrypto tests updated and a new HpkeTest verifies round-trips, length dispatch, context binding, and cross-key failures. This preserves existing Tink private key storage and produces compatible NO_PREFIX HPKE framing.
Compute the encryptionKey fingerprint from the raw HPKE public key bytes instead of the local Tink keyset. This ensures the self-diagnostic matches the HPKE fingerprint peers compute over the same bytes carried in ClientKeyEpoch. Keeps the null-safe runCatching behavior and falls back to "—" when no key is available.
Make envelope sealing robust to individual-recipient failures by dropping unsealable recipients instead of aborting the whole send. EnvelopeCrypto.seal now attempts per-recipient HPKE sealing with runCatching and collects only successful PerRecipientKey entries, requiring failure only if every recipient failed. SecureChannel.send wraps the non-suspending seal call in runCatching, logs and continues (skips the body) if sealing completely fails, and logs a warning when some recipients were dropped. Added tests in EnvelopeCryptoTest and SecureChannelTest to cover skipping unsealable recipients and the case where all recipients fail. Files changed: SecureChannel.kt, SecureChannelTest.kt, EnvelopeCrypto.kt, EnvelopeCryptoTest.kt.
Rename the identifier for the 32-byte raw X25519 public key from hpkePublicKeyset to hpkePublicKey across the codebase to clarify that this is the raw public key (not a Tink keyset). Updated protocol model (ClientKeyEpoch, Cards), app models and logic (Peer, TrustStore, AppGraph, RotationManager, TrustPeerDirectory, Pairing), crypto layers (EnvelopeCrypto, Hpke), and all affected tests and protocol codec/server tests. This is a mechanical rename and comment/update alignment; no functional change to sealing/opening behavior.
Introduce PeerDirectory.unsealableRecipients (default empty) to surface trusted peers targeted by a scope that currently lack a usable key-epoch. SecureChannel now calls this before sending and invokes onUnresolvedSender for each such peer so send-initiated delivery attempts drive the same broker key-epoch refetch used on receive. TrustPeerDirectory implements the new API using TrustState.peersNeedingKeyEpoch. Tests updated/added to verify send triggers repairs, TestSupport updated to pass the onUnresolvedSender handler, and nginx config tweaked to route /v1 to the legacy broker and the rest to the v2 broker.
There was a problem hiding this comment.
Pull request overview
This PR updates NotiSync’s HPKE key publishing and sealing to support a cross-platform, Tink-free wire form by publishing raw 32-byte X25519 public keys while retaining Tink private keysets for opening.
Changes:
- Renames the protocol/client model field from
hpkePublicKeysettohpkePublicKeyand updates encoding/decoding + tests accordingly. - Adds raw-key support in
Hpke(rawPublicKey,sealToRaw) and dispatchesseal()based on recipient key length (32 bytes → raw, else legacy Tink keyset). - Makes outbound sealing more defensive (skip unsealable recipients, trigger key-epoch repair on attempted send) and updates nginx routing to favor NS2 by default.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| server/src/test/kotlin/net/extrawdw/notisync/server/BrokerFlowTest.kt | Updates test construction to use hpkePublicKey. |
| protocol/src/test/kotlin/net/extrawdw/notisync/protocol/ProtocolCodecTest.kt | Updates codec test to the renamed HPKE field. |
| protocol/src/main/kotlin/net/extrawdw/notisync/protocol/Cards.kt | Renames ClientKeyEpoch HPKE field and updates documentation to describe raw-key publishing. |
| protocol-crypto/src/test/kotlin/net/extrawdw/notisync/protocol/crypto/KeyEpochTest.kt | Updates key-epoch tests to use hpkePublicKey. |
| protocol-crypto/src/test/kotlin/net/extrawdw/notisync/protocol/crypto/HpkeTest.kt | Adds new tests covering raw public key extraction and raw sealing/opening behavior. |
| protocol-crypto/src/test/kotlin/net/extrawdw/notisync/protocol/crypto/EnvelopeCryptoTest.kt | Adds tests for raw-recipient sealing and “skip unsealable recipient” fan-out behavior. |
| protocol-crypto/src/main/kotlin/net/extrawdw/notisync/protocol/crypto/Hpke.kt | Implements raw X25519 extraction + sealing and length-based dispatch in seal(). |
| protocol-crypto/src/main/kotlin/net/extrawdw/notisync/protocol/crypto/EnvelopeCrypto.kt | Changes per-recipient sealing to drop recipients that can’t be sealed to; throws only if none are sealable. |
| deploy/nginx-notisync.conf | Routes /v1 explicitly to legacy NS1 and sends all other paths to NS2. |
| app/src/test/java/net/extrawdw/apps/notisync/testsupport/TestSupport.kt | Renames peer/test helpers to the new HPKE field and wires onUnresolvedSender into testChannel. |
| app/src/test/java/net/extrawdw/apps/notisync/data/TrustStoreTest.kt | Updates trust store tests to hpkePublicKey. |
| app/src/test/java/net/extrawdw/apps/notisync/channel/SecureChannelTest.kt | Adds regression tests ensuring send skips unsealable recipients and triggers key-epoch repair. |
| app/src/main/java/net/extrawdw/apps/notisync/pairing/Pairing.kt | Updates HPKE fingerprinting to operate on hpkePublicKey. |
| app/src/main/java/net/extrawdw/apps/notisync/foundation/TrustPeerDirectory.kt | Reads hpkePublicKeyB64 and adds unsealableRecipients() for send-initiated repair. |
| app/src/main/java/net/extrawdw/apps/notisync/foundation/RotationManager.kt | Publishes the raw X25519 key bytes into the key-epoch blob. |
| app/src/main/java/net/extrawdw/apps/notisync/data/TrustStore.kt | Persists hpkePublicKeyB64 from the current key-epoch. |
| app/src/main/java/net/extrawdw/apps/notisync/data/Models.kt | Renames stored peer HPKE key field to hpkePublicKeyB64 and updates docs. |
| app/src/main/java/net/extrawdw/apps/notisync/channel/SecureChannel.kt | Adds send-initiated key-epoch repair and makes sealing failures non-fatal for send. |
| app/src/main/java/net/extrawdw/apps/notisync/channel/PeerDirectory.kt | Extends interface with unsealableRecipients() defaulting to empty. |
| app/src/main/java/net/extrawdw/apps/notisync/AppGraph.kt | Publishes raw public key bytes in ClientKeyEpoch and fingerprints the raw published key. |
| app/build.gradle.kts | Bumps app version to 1.3.0-rc.1 (code 15). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+80
to
+82
| fun sealToRaw(dek: ByteArray, raw32: ByteArray, contextInfo: ByteArray): ByteArray { | ||
| CryptoInit.ensure() | ||
| val key = HpkePublicKey.create(params, Bytes.copyFrom(raw32), null) |
Comment on lines
+68
to
+73
| fun rawPublicKey(publicKeyset: ByteArray): ByteArray { | ||
| CryptoInit.ensure() | ||
| val handle = TinkProtoKeysetFormat.parseKeysetWithoutSecret(publicKeyset) | ||
| val key = handle.getAt(0).key as HpkePublicKey | ||
| return key.publicKeyBytes.toByteArray() | ||
| } |
Comment on lines
+109
to
+117
| val perRecipient = recipients.mapNotNull { r -> | ||
| runCatching { | ||
| PerRecipientKey( | ||
| recipientId = r.clientId, | ||
| sealedDek = Hpke.seal(dek, r.hpkePublicKey, dekContext(suite, r.clientId, r.recipientEpoch)), | ||
| recipientEpoch = r.recipientEpoch, | ||
| ) | ||
| }.getOrNull() | ||
| } |
Comment on lines
+158
to
+167
| val envelope = runCatching { | ||
| if (op != null) { | ||
| EnvelopeCrypto.seal(op, typ, body, recipients, messageId, seqN, createdAt) | ||
| } else { | ||
| EnvelopeCrypto.seal(signer, typ, body, recipients, messageId, seqN, createdAt) | ||
| } | ||
| }.getOrElse { | ||
| log.warn("seal failed for $typ ($messageId); skipping send: ${it.message}") | ||
| continue | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Add support for publishing and sealing to raw 32-byte X25519 HPKE public keys while retaining Tink private keysets. Hpke now exposes
rawPublicKeyandsealToRaw, and seal dispatches by length (32 bytes → raw path, otherwise Tink keyset).AppGraphandRotationManagerpublish the raw public key bytes for cross-platform (Tink-free) peers.