Skip to content

feat: reuse Pubky Ring identities - #1109

Draft
Jasonvdb wants to merge 27 commits into
masterfrom
codex/shared-pubky-ring
Draft

Jasonvdb wants to merge 27 commits into
masterfrom
codex/shared-pubky-ring

Conversation

@Jasonvdb

@Jasonvdb Jasonvdb commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Builds on merged #1084 and #1097.

Description

  • Discover Ring-owned Pubky profiles and explicitly connect the selected identity from Profile setup.
  • Keep only a source reference and session. Borrowed keys are verified just in time and never stored privately or backed up.
  • Expose Bitkit-owned identities through a read-only provider with signature permission, exact package/UID checks, and certificate verification.
  • Revalidate borrowed access on initialization, foreground, session refresh, and authenticated operations.
  • Confirmed source loss hides the borrowed identity immediately, removes published Paykit endpoints, clears private state, then forgets local session access.
  • Failed cleanup stays durable and retryable across restart/sign-out. Unreadable quarantine state fails closed before key use or export.
  • Preserve existing callbacks, onboarding, restored sessions, and profile/contact behavior.

The Bitkit private key remains canonical. Ambiguous private state is quarantined rather than silently reclassified or deleted.

Release gate

Merge/release after Ring #342. Confirm Play-installed Ring and Bitkit signing certificates and pass final artifact interoperability.

Design

Bitkit handoff: Profile choice and contact import.

Validation

Current head f37c8cd6d contains master 905640577; every PR commit is signed.

  • Full unit suite: 2,798 tests passed.
  • Focused provider/repository suites: 131 tests passed.
  • Kotlin compilation, detekt, formatting lint, and git diff --check passed.
  • Hosted build, lint, detekt, and local-app build passed. Hosted E2E is currently blocked by a shared staging outage: all shards receive HTTP 502 and time out before the Lightning node starts; one failed-job retry reproduced it.
  • Android Lint was attempted locally but AndroidX Navigation's existing EmptyNavDeepLinkDetector crashes on unchanged ScreenDeepLinkRuntime.kt before producing findings. No rules, suppressions, or baselines were changed.

Local mainnet manifest generation remains blocked because the available google-services.json has no to.bitkit client.

Manual Tests

Tests used fresh disposable zero-balance emulator wallets and staging identities.

  • 1. Select the exact signed-up Ring key among two identities and take the empty-contact Pay Contacts route.
  • 2a. Restart both apps with an active Bitkit-owned profile; the same profile remains and Ring adopts that exact identity.
  • 2b. Recoverable profile survives current relaunch and pre-sharing-master update with exact key/name and restored session.
  • 2c. Pre-sharing Bitkit update preserves the exact full key/name; current Ring adopts it and both-app restart retains it.
  • 3. Current builds retain the borrowed key/profile/contact after restart; Ring keeps owned Backup/Delete controls without duplication.
  • 4a. Uninstall Ring; foregrounding Bitkit clears borrowed access/metadata and returns to Profile choice.
  • 4b. Delete the active Ring source; Bitkit clears reference, profile, and session without copying a private key or harming other Ring identities.
  • 5a. Export-disable failure aborts Reset and preserves encrypted identity/session/wallet data; retry completes normally.
  • 5b. Export-disable failures during Delete Profile/Disconnect preserve data; restored writes allow normal Delete. Disconnect retry is covered automatically.
  • 6. Pre-sharing Bitkit profile survives in-place update with exact key/name and is discoverable by current Ring.
  • 7. Borrowing Ring, approve /pub/pubky.app/:rw; requester verifies exact key/capabilities and no private key is copied.
  • 8a. Wrong-certificate Ring source is rejected by Bitkit.
  • 8b. Wrong-certificate Bitkit source is rejected by Ring on foreground and cold restart; owned keys remain.
  • 9. Stale success/cancel/error callbacks are consumed without profile or payment-state changes.
  • 10. Bitkit-owned identity adopted by Ring survives restart; delete/uninstall/reset makes Ring disconnect it.
  • 11. Published Ring profile and one selected followed contact import with exact key/name/bio.
  • 12. Borrowed-identity watch-only approval produces a verified xpub claim and exact two-path grant.

Automated coverage

SharedPubkyContractTest, SharedPubkyDiscoveryTest, SharedPubkyManifestTest, SharedPubkyProviderTest, PubkyStoreSerializerTest, PubkyRepoTest, PrivatePaykitRepoTest, PaykitSdkServiceTest, PubkyChoiceViewModelTest, PubkyRingAuthCallbackTest, and AppViewModelSendFlowTest.

Base automatically changed from codex/970-pubky-paykit-ui-parity to codex/paykit-watch-only-accounts July 27, 2026 15:46
@ben-kaufman
ben-kaufman force-pushed the codex/paykit-watch-only-accounts branch 4 times, most recently from ec8ee8c to 41ddb6b Compare August 3, 2026 11:10
Base automatically changed from codex/paykit-watch-only-accounts to master August 3, 2026 12:30
The first wipe step ran through the best-effort `step` helper, which logged
and swallowed both the thrown read-back check and the returned Result. The
shared mirror therefore stayed readable by Ring through node shutdown and
the network-bound remote cleanup whenever the guard failed. Call
disableSharedIdentityExport() directly and propagate its failure so the
wipe stops before anything else is torn down.
Session teardown attempted all four credential deletes independently, so a
failed PUBKY_SECRET_KEY delete could still be followed by a successful
quarantine-marker delete. The suspect secret would then be restored on the
next start and re-exported to Ring. Only clear the marker once the secret it
guards is actually gone.
@Jasonvdb
Jasonvdb force-pushed the codex/shared-pubky-ring branch from ad7ce1a to 68c9ee8 Compare September 17, 2026 11:17
@Jasonvdb

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 17, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-21T12:42:37.306279Z 26e67c3 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 68c9ee8a1f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/src/main/java/to/bitkit/repositories/PubkyRepo.kt Outdated
Comment thread app/src/main/java/to/bitkit/ui/screens/profile/PubkyChoiceScreen.kt
@Jasonvdb

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0700ee2367

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/src/main/java/to/bitkit/repositories/PubkyRepo.kt Outdated
Comment thread app/src/main/java/to/bitkit/repositories/PubkyRepo.kt Outdated
Disconnecting an unavailable Pubky Ring identity dropped the session and
cleared the sharing flags with cleanup marked done, so endpoints and the
receiver marker published under that identity stayed live and the
reconciliation loop had nothing left to retry. Remove them while the borrowed
session still works, mirroring sign out, and keep the cleanup pending when the
removal fails.
…te its secret

Local state teardown deleted the managed secret and its quarantine marker
independently, so a failed PUBKY_SECRET_KEY delete during sign out or a wallet
wipe could still be followed by a successful marker delete. The surviving
suspect secret would then be treated as a managed local identity on the next
start, signed in with and re-exported to Ring. Only release the marker once the
secret it guards is gone, matching the guarded credential teardown path.
@Jasonvdb

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 1eb57bfcc9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Jasonvdb
Jasonvdb marked this pull request as ready for review September 18, 2026 13:47
@greptile-apps

greptile-apps Bot commented Sep 18, 2026

Copy link
Copy Markdown

RetriggerConfidence Score: 3/5

The PR is not yet safe to merge because transient Ring query failures can erase a valid borrowed identity, and post-adoption import failures leave onboarding in a conflicting active state.

Findings

  1. P1 Transient Errors Disconnect Identities
  2. P1 Failed Import Leaves Identity Active

Summary

This PR introduces bidirectional Pubky identity reuse between Bitkit and Pubky Ring, including a signature-protected credential provider, Ring identity discovery and adoption, external-session lifecycle handling, quarantine safeguards, and a redesigned profile-choice flow.

  • Adds provider and discovery contracts with package, UID, permission, certificate, key-format, and cursor validation.
  • Persists only a Ring identity reference while retrieving and verifying credentials just in time.
  • Serializes identity lifecycle operations and coordinates export disabling, session cleanup, wallet wiping, and recovery.
  • Replaces the legacy Ring authorization flow with explicit identity selection and contact import.
  • Adds extensive unit coverage for sharing, persistence, cleanup, lifecycle, and UI state behavior.

Diagram

sequenceDiagram
    participant User
    participant Choice as Pubky Choice
    participant Repo as PubkyRepo
    participant Ring as Ring Provider
    participant SDK as Paykit SDK
    participant Store as Pubky Store

    Choice->>Ring: Discover signed Ring identities
    Ring-->>Choice: Public identity rows
    User->>Choice: Select identity
    Choice->>Repo: adoptRingIdentity(identity)
    Repo->>Ring: Read credential just in time
    Ring-->>Repo: Secret and identity metadata
    Repo->>Repo: Verify source, key format, and derived public key
    Repo->>Store: Persist source reference only
    Repo->>SDK: Create external session
    SDK-->>Repo: Active Pubky session
    Repo-->>Choice: Adoption succeeded
    Choice->>Repo: prepareImport()
    Repo-->>Choice: Profile and contacts or failure
Loading

Reviews (1) · Last reviewed commit: "chore: merge master into shared pubky ri..."

Comment thread app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
@github-actions

github-actions Bot commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Regtest APK

Built from f37c8cd (run).

Download bitkit-dev-debug universal APK (expires in 30 days).

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Reviewed at b890fefab as a key-material and auth change. No HIGH, no MEDIUM. One LOW inline. A second LOW is a follow-up on the codex cleanup thread, posted as a reply there. Paykit/Pubky is dev-flag gated, so both affect opted-in users on released builds and neither blocks.

Checked and clean:

  • Provider (Bitkit → Ring).
    • Read and write access require the signature-level READ_SHARED_PUBKY permission, and grantUriPermissions is off.
    • enforceCaller() requires callingPackage == app.pubkyring, requires that package to belong to the calling UID, and requires checkSignatures == SIGNATURE_MATCH.
    • insert, update and delete throw.
    • The credential row is returned only when the path pubky equals the locally derived pubky.
    • Each variant has its own authority, so a dev or tnet build, or a same-package clone, cannot read the release key.
  • Discovery (Ring → Bitkit).
    • verifyRingProvider checks package, authority, permission and signature before each query.
    • readCredential rejects count != 1 and a pubky mismatch.
    • signInWithExternalCredential re-derives the pubkey from the secret and compares it with the selected row, so the connected identity cannot differ from the one shown.
  • Key material.
    • The borrowed secret is never persisted: shouldStoreLocalSecret = false, managedSecretForSessionPersistence returns null, and the live access is stripped of localSecretKey.
    • snapshotSessionBackupState returns null while a ref exists, and the ref is not in the metadata backup.
    • No secret or credential URI reaches a Logger call.
  • Upgrade from rc31 and rc51.
    • Owned identities re-import with the same secret, and export is enabled after validation.
    • Legacy Ring-authorized sessions import as external, with export disabled.
    • externalIdentityRef defaults to null and ignoreUnknownKeys is set, so both upgrade and downgrade decode.
  • Wipe order. disableSharedIdentityExport().getOrThrow() runs before stopNode(), and clearLocalState disables export before deleting keys.
  • Concurrency. None of the identityLifecycleMutex callers acquires it while already holding it. Adoption rollback runs under NonCancellable. Choice effects use SharedFlow.
  • Gating note. SharedPubkyProvider is exported in every variant, whatever isPaykitEnabled is set to. It serves a credential only when the export flag is 1 and an owned secret exists. So a user who opted in and later turned Paykit off still has their key readable by a same-signature Ring. That looks intended, but it is worth one line in the PR body.

Comment thread app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
@Jasonvdb
Jasonvdb force-pushed the codex/shared-pubky-ring branch from b890fef to 41ebbd7 Compare September 21, 2026 10:47

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 613bc78c24

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/src/main/java/to/bitkit/repositories/PubkyRepo.kt
Comment thread app/src/main/java/to/bitkit/repositories/PubkyRepo.kt Outdated

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Delta since b890fefab (ab5362507, 25cbd4018, 3adc559ae, merge 613bc78c2): no HIGH, no MEDIUM.

  • createIdentity guard: fixed. A restored legacy Ring session is now rejected with PubkyAlreadySignedInError (:511), and the pending-setup branch resumes only when the stored secret derives the active key (:484-494). The Choice → Profile redirect is still gone, but that now costs a toast instead of an identity.
  • Private endpoints on Ring loss: partially fixed. It works when the network is up. The offline case still leaves endpoints published; follow-up on the existing thread.
  • Lock ordering. PrivatePaykitRepo never takes identityLifecycleMutex, so calling it under the lock can't invert. The Provider breaks the DI cycle.
  • Cross-identity. Every session-creating path runs closeAndClear() before setting _publicKey. Backup restore applies METADATA before WALLET, so a carried-over marker cannot erase freshly restored private state.
  • Transient vs definitive (ab5362507). A null cursor, SecurityException or a dead provider keeps the ref and session. Only an uninstalled, untrusted or unsupported source, or a missing identity, disconnects.
  • Upgrade. privatePaykitStateCleanupPending defaults to false, and unknown keys are ignored.
  • Merge. The only conflict was Button.kt, which isn't PR-owned and was resolved to master.

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 26e67c319d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread app/src/main/java/to/bitkit/data/sharing/SharedPubkyProvider.kt Outdated
Comment thread app/src/main/java/to/bitkit/repositories/PubkyRepo.kt Outdated

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Delta since 613bc78c2 (d1e3ed253, 26e67c319): no findings.

  • 26e67c319. Once the session is deleted, teardown finishes under NonCancellable, and the ref reset now happens together with the session deletion. Adoption rollback is still unconditional. Lock order is identityLifecycleMutexPrivatePaykitRepoPaykitSdkService, with no reverse edge.
  • d1e3ed253. Only clearUnavailableExternalIdentityLocked raises the quarantine marker, and restore now clears it only after PUBKY_SECRET_KEY has been deleted. No managed secret is left readable next to a borrowed ref. On LocalSeed restore the secret is re-derived from the seed, so nothing unrecoverable is deleted.
  • Still open: the offline Ring-loss case on the cleanup thread. clearExternalSessionAccess() still runs even when the private endpoint removal failed, and the pending retry only clears local state. That is the same gap codex raised as "Retry endpoint removal before discarding cleanup state".

@jvsena42 jvsena42 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Delta since 26e67c319 (f19a2c785, f37c8cd6d): two MEDIUMs, both gated behind Paykit and affecting only opted-in users on released builds, so this is not blocking. Posted as a reply on the retention thread (PubkyRepo.kt:1729), because both are side effects of the retention that thread asked for: the Ring session gets into the metadata backup during the retained window, and a rejected retained session blocks every identity entry point.

Checked and clean:

  • f19a2c785. A missing quarantine marker returns null without touching the keystore, and only a decrypt or datastore failure throws. A first launch after upgrade from a shipped tag therefore takes the null path. The provider and resolveStoredSessionInitialization fail closed on a read error.
  • Teardown order. closeAndClear runs before clearExternalSessionAccess, and the marker is cleared only after local cleanup succeeds. Everything after session deletion is NonCancellable. A cancel during removal leaves the ref null, the marker set and the session retained, and the next retry converges.
  • Lock order is unchanged: identityLifecycleMutexPrivatePaykitRepoPaykitSdkService, with no reverse edge.
  • Parity. iOS clears the session before deleting the reference, so it never has the ref-gone/session-kept state. Neither hole exists in synonymdev/bitkit-ios#643.

@Jasonvdb
Jasonvdb marked this pull request as draft September 22, 2026 09:56

This branch has not been deployed

No deployments
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.

3 participants