mirror of
https://github.com/PR0M3TH3AN/bitlogin.git
synced 2026-11-02 11:31:21 +00:00
Document experimental portable application state
This commit is contained in:
1 parent
6f0bc2a056
commit
66c97820db
1 file changed
+467
@@ -0,0 +1,467 @@
|
||||
# BitLogin Portable Application State
|
||||
|
||||
**Status:** Experimental design
|
||||
**Document version:** 0.1
|
||||
**Date:** July 22, 2026
|
||||
**Implementation status:** Not implemented
|
||||
|
||||
## 1. Summary
|
||||
|
||||
Portable Application State allows compatible applications to store encrypted,
|
||||
user-owned settings and lightweight state through BitLogin so the application
|
||||
experience follows the user across devices.
|
||||
|
||||
Examples include:
|
||||
|
||||
- Interface preferences
|
||||
- Subscription and follow selections
|
||||
- Block and mute lists
|
||||
- Draft-editor preferences
|
||||
- Notification settings
|
||||
- Upload defaults
|
||||
- Saved views and filters
|
||||
- Application-specific bookmarks
|
||||
|
||||
The application defines the meaning of its records. BitLogin provides:
|
||||
|
||||
- Namespacing
|
||||
- Local encryption and decryption
|
||||
- Relay publication and retrieval
|
||||
- Independent record replacement
|
||||
- Recovery on a clean device
|
||||
- Conflict and rollback metadata
|
||||
- Export and deletion
|
||||
|
||||
Portable Application State must not be stored directly inside the BitLogin
|
||||
credential or recovery capsule.
|
||||
|
||||
## 2. Motivation
|
||||
|
||||
A portable Nostr identity does not automatically produce a portable application
|
||||
experience. Users may recover the same `npub` on another device while losing:
|
||||
|
||||
- Application preferences
|
||||
- Locally maintained lists
|
||||
- Draft settings
|
||||
- Notification choices
|
||||
- Saved filters
|
||||
- Other application state that was never published as standard Nostr events
|
||||
|
||||
This causes decentralized applications to feel less coherent than conventional
|
||||
accounts even when identity recovery works correctly.
|
||||
|
||||
The product goal is:
|
||||
|
||||
> Compatible applications should feel like the same account on every device
|
||||
> without placing their private database under a centralized provider.
|
||||
|
||||
## 3. Scope
|
||||
|
||||
Portable Application State is intended for small, encrypted, user-specific
|
||||
records that are useful across devices.
|
||||
|
||||
Appropriate examples:
|
||||
|
||||
```text
|
||||
BitRoad
|
||||
├── storefront editor preferences
|
||||
├── notification settings
|
||||
└── saved management filters
|
||||
|
||||
BitVid
|
||||
├── playback preferences
|
||||
├── subscription presentation settings
|
||||
└── upload defaults
|
||||
|
||||
Satisfied
|
||||
├── analysis display preferences
|
||||
├── dietary preference settings
|
||||
└── conversation presentation settings
|
||||
```
|
||||
|
||||
Inappropriate uses include:
|
||||
|
||||
- Large binary files
|
||||
- Video or image storage
|
||||
- High-frequency telemetry
|
||||
- Server-authoritative balances
|
||||
- Payment credentials
|
||||
- S3 credentials
|
||||
- NWC credentials
|
||||
- Cryptocurrency seed phrases
|
||||
- Data requiring transactional database semantics
|
||||
|
||||
Large files belong in user-selected storage such as S3-compatible services.
|
||||
High-value credentials belong in the Connection Vault.
|
||||
|
||||
## 4. Record model
|
||||
|
||||
Application state must be divided into independently replaceable logical
|
||||
records rather than one global application document.
|
||||
|
||||
Good:
|
||||
|
||||
```text
|
||||
network.bitroad/preferences/editor
|
||||
network.bitroad/preferences/notifications
|
||||
network.bitroad/filters/product-list
|
||||
```
|
||||
|
||||
Avoid:
|
||||
|
||||
```text
|
||||
network.bitroad/everything
|
||||
```
|
||||
|
||||
This reduces multi-device clobbering and allows selective deletion, export, and
|
||||
permission management.
|
||||
|
||||
## 5. Application identifiers
|
||||
|
||||
Each application must use a stable identifier. Recommended forms:
|
||||
|
||||
```text
|
||||
network.bitroad
|
||||
network.bitunlock
|
||||
network.bitvid
|
||||
app.satisfied
|
||||
```
|
||||
|
||||
An application identifier must:
|
||||
|
||||
- Be lowercase ASCII.
|
||||
- Contain only letters, numbers, dots, and hyphens.
|
||||
- Be between 3 and 128 characters.
|
||||
- Remain stable across deployments and domains.
|
||||
- Not impersonate another application's identifier.
|
||||
|
||||
A future registry or signed application manifest may bind an identifier to one
|
||||
or more origins and application pubkeys. Until then, users must be shown the
|
||||
requesting origin whenever an app asks to read or write state.
|
||||
|
||||
## 6. State record identifier
|
||||
|
||||
Each record has a logical key chosen by the application:
|
||||
|
||||
```text
|
||||
preferences/editor
|
||||
preferences/notifications
|
||||
lists/blocked-pubkeys
|
||||
views/default-product-filter
|
||||
```
|
||||
|
||||
The combination of application ID and record key identifies the replaceable
|
||||
record.
|
||||
|
||||
Requirements:
|
||||
|
||||
- UTF-8 text normalized according to the final protocol specification.
|
||||
- No secrets or personal data embedded in the public identifier.
|
||||
- Stable for the logical record.
|
||||
- Limited to a reasonable maximum length.
|
||||
|
||||
## 7. Nostr event
|
||||
|
||||
Each state record should be stored as its own encrypted NIP-78-style addressable
|
||||
event under the user's everyday Nostr identity or a future application-state
|
||||
identity.
|
||||
|
||||
Proposed event:
|
||||
|
||||
```json
|
||||
{
|
||||
"pubkey": "<everyday-or-app-state-pubkey>",
|
||||
"created_at": 1784750000,
|
||||
"kind": 30078,
|
||||
"tags": [
|
||||
["d", "bitlogin:app-state:<opaque-record-id>"]
|
||||
],
|
||||
"content": "<encoded-encrypted-envelope>",
|
||||
"sig": "<signature>"
|
||||
}
|
||||
```
|
||||
|
||||
The public `d` tag should use an opaque deterministic or random identifier so
|
||||
relays do not learn the application ID or record key.
|
||||
|
||||
A possible opaque identifier is:
|
||||
|
||||
```text
|
||||
base64url(
|
||||
SHA256(
|
||||
"bitlogin/app-state-id/v1"
|
||||
|| 0x00
|
||||
|| application_id
|
||||
|| 0x00
|
||||
|| record_key
|
||||
)
|
||||
)
|
||||
```
|
||||
|
||||
If deterministic IDs are used, clients must acknowledge that observers who
|
||||
already know the exact application ID and record key may test guesses. A keyed
|
||||
identifier derived from an app-state root provides stronger privacy and is
|
||||
preferred for the final protocol.
|
||||
|
||||
## 8. Encryption key hierarchy
|
||||
|
||||
The final design should use a stable application-state root that survives
|
||||
password changes and ordinary identity use.
|
||||
|
||||
Two options require evaluation:
|
||||
|
||||
1. Derive app-state keys from a dedicated random root stored in both capsules.
|
||||
2. Derive app-state keys from the Connection Vault root with strict domain
|
||||
separation.
|
||||
|
||||
The dedicated-root option provides stronger compartmentalization. Reusing the
|
||||
Connection Vault root reduces capsule expansion but couples settings recovery to
|
||||
the higher-value credential vault.
|
||||
|
||||
A proposed dedicated construction is:
|
||||
|
||||
```text
|
||||
app_state_prk = HKDF-Extract(
|
||||
salt = SHA256("bitlogin/app-state-root/v1"),
|
||||
IKM = application_state_root
|
||||
)
|
||||
```
|
||||
|
||||
Then derive one key per application and record:
|
||||
|
||||
```text
|
||||
record_key = HKDF-Expand(
|
||||
app_state_prk,
|
||||
"bitlogin/app-state-record/v1"
|
||||
|| 0x00
|
||||
|| application_id
|
||||
|| 0x00
|
||||
|| record_key_name,
|
||||
32
|
||||
)
|
||||
```
|
||||
|
||||
Exact encoding, normalization, and test vectors must be fixed before
|
||||
implementation.
|
||||
|
||||
## 9. Plaintext schema
|
||||
|
||||
```json
|
||||
{
|
||||
"schema": "bitlogin.app-state.v1",
|
||||
"application_id": "network.bitroad",
|
||||
"record_key": "preferences/editor",
|
||||
"generation": 7,
|
||||
"created_at": 1784700000,
|
||||
"updated_at": 1784750000,
|
||||
"content_type": "application/json",
|
||||
"data": {
|
||||
"compactMode": true,
|
||||
"defaultCurrency": "sat"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Required fields:
|
||||
|
||||
- `schema`
|
||||
- `application_id`
|
||||
- `record_key`
|
||||
- `generation`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `content_type`
|
||||
- `data`
|
||||
|
||||
Initial supported content type:
|
||||
|
||||
```text
|
||||
application/json
|
||||
```
|
||||
|
||||
Binary content and arbitrary executable code are prohibited.
|
||||
|
||||
## 10. Encryption and padding
|
||||
|
||||
Records should reuse BitLogin's audited primitives where possible:
|
||||
|
||||
- RFC 8785 canonical JSON serialization
|
||||
- AES-256-GCM
|
||||
- Fresh random 96-bit nonce per publication
|
||||
- Context-bound associated data
|
||||
- Fixed-size plaintext buckets
|
||||
|
||||
Proposed associated data:
|
||||
|
||||
```text
|
||||
bitlogin|app-state|v1|<author-pubkey>|30078|<d-tag>
|
||||
```
|
||||
|
||||
The app-state specification may require larger buckets than the identity
|
||||
capsules. Relay limits must be checked before publication.
|
||||
|
||||
## 11. Reading and writing
|
||||
|
||||
Conceptual application API:
|
||||
|
||||
```javascript
|
||||
const state = await bitlogin.appState.get({
|
||||
applicationId: "network.bitroad",
|
||||
recordKey: "preferences/editor"
|
||||
});
|
||||
|
||||
await bitlogin.appState.put({
|
||||
applicationId: "network.bitroad",
|
||||
recordKey: "preferences/editor",
|
||||
data: {
|
||||
compactMode: true,
|
||||
defaultCurrency: "sat"
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The host application must not receive keys for other applications' namespaces.
|
||||
|
||||
## 12. Authorization
|
||||
|
||||
An application should receive permission to access only its own namespace by
|
||||
default.
|
||||
|
||||
Suggested permission screen:
|
||||
|
||||
```text
|
||||
BitRoad wants to:
|
||||
|
||||
✓ Read BitRoad settings stored through BitLogin
|
||||
✓ Update BitRoad settings
|
||||
✗ Read settings belonging to other applications
|
||||
|
||||
[Not now] [Allow]
|
||||
```
|
||||
|
||||
Optional scopes:
|
||||
|
||||
```text
|
||||
app-state:read
|
||||
app-state:write
|
||||
app-state:delete
|
||||
app-state:export
|
||||
```
|
||||
|
||||
Permissions should bind to:
|
||||
|
||||
- Application ID
|
||||
- Requesting origin
|
||||
- Optional application pubkey
|
||||
- Allowed record-key prefixes
|
||||
- Expiration or persistence choice
|
||||
|
||||
## 13. Conflict handling
|
||||
|
||||
Nostr addressable events use last-write-wins replacement. BitLogin should reduce
|
||||
surprise by including:
|
||||
|
||||
- Monotonic `generation`
|
||||
- Monotonic event timestamps
|
||||
- Local high-water marks
|
||||
- Previous-event ID where useful
|
||||
- Optional application-level merge metadata
|
||||
|
||||
When two devices update the same record concurrently, BitLogin may:
|
||||
|
||||
1. Accept the newest valid event.
|
||||
2. Surface a conflict to the application.
|
||||
3. Preserve the losing plaintext locally long enough for user recovery.
|
||||
4. Allow the application to merge records and publish a new generation.
|
||||
|
||||
BitLogin must not attempt semantic merging of arbitrary application data.
|
||||
|
||||
## 14. Local cache
|
||||
|
||||
Decrypted app state may be cached locally for performance, namespaced by:
|
||||
|
||||
```text
|
||||
account -> application -> record
|
||||
```
|
||||
|
||||
Cache entries must be cleared when:
|
||||
|
||||
- The account logs out.
|
||||
- The account is removed from the device.
|
||||
- The user deletes the record.
|
||||
- The application's permission is revoked, when configured to clear local data.
|
||||
|
||||
Applications should receive immutable copies rather than references to shared
|
||||
internal state.
|
||||
|
||||
## 15. Deletion
|
||||
|
||||
Deleting a state record should:
|
||||
|
||||
1. Publish an encrypted tombstone for the addressable record.
|
||||
2. Publish a NIP-09 deletion request for the prior event where appropriate.
|
||||
3. Retain a local deletion high-water mark.
|
||||
4. Clear decrypted local cache.
|
||||
5. Notify the application.
|
||||
|
||||
Deletion from BitLogin does not guarantee every relay or third-party app has
|
||||
deleted prior copies.
|
||||
|
||||
## 16. Export and migration
|
||||
|
||||
Users should be able to export:
|
||||
|
||||
- All encrypted app-state events
|
||||
- Records for one application
|
||||
- One logical record
|
||||
- A user-readable decrypted export after explicit confirmation
|
||||
|
||||
Encrypted BitLogin recovery exports should include the latest state events and
|
||||
tombstones so the user's app experience can be restored even if relays lose the
|
||||
records.
|
||||
|
||||
## 17. Privacy and security
|
||||
|
||||
Application state may reveal sensitive preferences, subscriptions, block lists,
|
||||
health-related choices, or private workflow information.
|
||||
|
||||
Implementations must:
|
||||
|
||||
- Encrypt all non-public state locally.
|
||||
- Avoid application IDs and record names in public tags where practical.
|
||||
- Never include plaintext state in logs or analytics.
|
||||
- Prevent one app from reading another app's namespace.
|
||||
- Show the requesting account and origin during authorization.
|
||||
- Treat HTML, scripts, and executable payloads as untrusted data.
|
||||
- Enforce size and nesting limits before parsing.
|
||||
- Clear decrypted state when sessions lock or permissions are revoked.
|
||||
|
||||
A malicious BitLogin client or compromised device can still read all state
|
||||
available in that session.
|
||||
|
||||
## 18. Acceptance criteria
|
||||
|
||||
The feature is complete when:
|
||||
|
||||
1. A compatible app can write at least two independent encrypted records.
|
||||
2. A clean device can restore those records after BitLogin login or recovery.
|
||||
3. Updating one record does not replace another record.
|
||||
4. One application cannot read another application's state through the public
|
||||
API.
|
||||
5. Conflicting generations are detected and surfaced.
|
||||
6. Deleted records do not silently reappear from stale relay results on a device
|
||||
with a newer high-water mark.
|
||||
7. App-state events are included in encrypted account exports.
|
||||
8. Switching BitLogin accounts switches the entire visible app-state namespace.
|
||||
9. Relay-visible metadata does not expose plaintext application content.
|
||||
10. Invalid, oversized, or unsupported records fail closed.
|
||||
|
||||
## 19. Recommended implementation order
|
||||
|
||||
1. Finalize the key root and opaque record-ID design.
|
||||
2. Implement local encrypted records and validation.
|
||||
3. Add per-application namespace authorization.
|
||||
4. Add relay synchronization and quorum readback.
|
||||
5. Add rollback and conflict detection.
|
||||
6. Add export, deletion, and account-switch integration.
|
||||
7. Publish test vectors and a minimal example application.
|
||||
Reference in new issue
Block a user