Skip to content

Use raw X25519 HPKE keys and sealing#3

Merged
dingwen07 merged 5 commits into
mainfrom
feature/android-hpke
Jun 26, 2026
Merged

Use raw X25519 HPKE keys and sealing#3
dingwen07 merged 5 commits into
mainfrom
feature/android-hpke

Conversation

@dingwen07

Copy link
Copy Markdown
Owner

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.

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.
Copilot AI review requested due to automatic review settings June 26, 2026 08:03
@dingwen07
dingwen07 merged commit eceffe1 into main Jun 26, 2026
1 check passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 hpkePublicKeyset to hpkePublicKey and updates encoding/decoding + tests accordingly.
  • Adds raw-key support in Hpke (rawPublicKey, sealToRaw) and dispatches seal() 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
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants