canary-kit 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CANARY.md +1065 -0
  2. package/INTEGRATION.md +351 -0
  3. package/LICENSE +21 -0
  4. package/NIP-CANARY.md +624 -0
  5. package/README.md +187 -0
  6. package/SECURITY.md +92 -0
  7. package/dist/beacon.d.ts +104 -0
  8. package/dist/beacon.d.ts.map +1 -0
  9. package/dist/beacon.js +197 -0
  10. package/dist/beacon.js.map +1 -0
  11. package/dist/counter.d.ts +37 -0
  12. package/dist/counter.d.ts.map +1 -0
  13. package/dist/counter.js +62 -0
  14. package/dist/counter.js.map +1 -0
  15. package/dist/crypto.d.ts +111 -0
  16. package/dist/crypto.d.ts.map +1 -0
  17. package/dist/crypto.js +309 -0
  18. package/dist/crypto.js.map +1 -0
  19. package/dist/derive.d.ts +68 -0
  20. package/dist/derive.d.ts.map +1 -0
  21. package/dist/derive.js +85 -0
  22. package/dist/derive.js.map +1 -0
  23. package/dist/encoding.d.ts +56 -0
  24. package/dist/encoding.d.ts.map +1 -0
  25. package/dist/encoding.js +98 -0
  26. package/dist/encoding.js.map +1 -0
  27. package/dist/group.d.ts +185 -0
  28. package/dist/group.d.ts.map +1 -0
  29. package/dist/group.js +263 -0
  30. package/dist/group.js.map +1 -0
  31. package/dist/index.d.ts +10 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +12 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/nostr.d.ts +134 -0
  36. package/dist/nostr.d.ts.map +1 -0
  37. package/dist/nostr.js +175 -0
  38. package/dist/nostr.js.map +1 -0
  39. package/dist/presets.d.ts +26 -0
  40. package/dist/presets.d.ts.map +1 -0
  41. package/dist/presets.js +39 -0
  42. package/dist/presets.js.map +1 -0
  43. package/dist/session.d.ts +114 -0
  44. package/dist/session.d.ts.map +1 -0
  45. package/dist/session.js +173 -0
  46. package/dist/session.js.map +1 -0
  47. package/dist/sync-crypto.d.ts +66 -0
  48. package/dist/sync-crypto.d.ts.map +1 -0
  49. package/dist/sync-crypto.js +125 -0
  50. package/dist/sync-crypto.js.map +1 -0
  51. package/dist/sync.d.ts +191 -0
  52. package/dist/sync.d.ts.map +1 -0
  53. package/dist/sync.js +568 -0
  54. package/dist/sync.js.map +1 -0
  55. package/dist/token.d.ts +186 -0
  56. package/dist/token.d.ts.map +1 -0
  57. package/dist/token.js +344 -0
  58. package/dist/token.js.map +1 -0
  59. package/dist/verify.d.ts +45 -0
  60. package/dist/verify.d.ts.map +1 -0
  61. package/dist/verify.js +59 -0
  62. package/dist/verify.js.map +1 -0
  63. package/dist/wordlist.d.ts +28 -0
  64. package/dist/wordlist.d.ts.map +1 -0
  65. package/dist/wordlist.js +297 -0
  66. package/dist/wordlist.js.map +1 -0
  67. package/llms-full.txt +1461 -0
  68. package/llms.txt +180 -0
  69. package/package.json +144 -0
package/INTEGRATION.md ADDED
@@ -0,0 +1,351 @@
1
+ # CANARY Integration Guide
2
+
3
+ How to integrate CANARY spoken-word verification into your systems.
4
+
5
+ ## Overview
6
+
7
+ CANARY provides bidirectional, deepfake-proof identity verification using shared
8
+ secrets and spoken words. Both parties on a call independently derive the same
9
+ pair of words from a shared seed — one word per role. Cloning a voice does not
10
+ help derive the correct word. Only knowledge of the shared secret does.
11
+
12
+ This guide covers seed establishment patterns and call centre integration for
13
+ insurance, banking, and enterprise use cases.
14
+
15
+ ## Seed Establishment Patterns
16
+
17
+ The shared seed is the foundation of CANARY verification. How it gets to both
18
+ parties depends on your infrastructure.
19
+
20
+ ### Pattern 1: App-Derived Seed (Primary — Insurance/Banking)
21
+
22
+ The customer authenticates to the insurer's app. The seed is derived server-side
23
+ and synced during login.
24
+
25
+ ```
26
+ Customer opens Aviva app
27
+ → Authenticates (biometrics / password / MFA)
28
+ → App requests session seed from Aviva backend
29
+ → Backend: seed = HMAC-SHA256(master_key, customer_id || seed_version)
30
+ → Seed delivered over TLS to the app
31
+ → App stores seed in secure storage (Keychain / KeyStore)
32
+ → Call centre agent's system derives the same seed from the same inputs
33
+ ```
34
+
35
+ Key properties:
36
+ - **No extra setup step** — seed arrives during normal app login
37
+ - **`seed_version`** allows rotation without re-enrolment
38
+ - **Master key** is HSM-protected server-side
39
+ - **Offline-capable** — the app derives tokens locally after initial sync
40
+ - **Works with existing app infrastructure**
41
+
42
+ ```typescript
43
+ import { deriveSeed, createSession } from 'canary-kit/session'
44
+
45
+ // Server-side: derive seed for a customer
46
+ const seed = deriveSeed(masterKey, customerId, seedVersion.toString())
47
+
48
+ // Both agent and customer derive the same seed independently
49
+ const session = createSession({
50
+ secret: seed,
51
+ namespace: 'aviva',
52
+ roles: ['caller', 'agent'],
53
+ myRole: 'agent',
54
+ preset: 'call',
55
+ theirIdentity: customerId,
56
+ })
57
+ ```
58
+
59
+ ### Pattern 2: Enrolment QR (Branch / In-Person)
60
+
61
+ For customers without the app, generate a QR code containing the seed:
62
+
63
+ ```
64
+ Customer visits branch / receives postal letter
65
+ → Staff generates CANARY enrolment QR code
66
+ → QR contains: { seed, namespace, roles, rotation }
67
+ → Customer scans with phone
68
+ → Seed stored locally
69
+ ```
70
+
71
+ ### Pattern 3: SMS/Email OTP Bootstrap (Lower Security)
72
+
73
+ For customers with no app and no branch visit:
74
+
75
+ ```
76
+ Customer calls insurer
77
+ → Agent sends one-time setup code via SMS/email
78
+ → Customer enters code into a web page
79
+ → Page derives seed from code + customer ID + server nonce
80
+ → Page stores seed in browser/app
81
+ ```
82
+
83
+ Weakest pattern (SMS is interceptable). Stepping stone to Pattern 1.
84
+
85
+ ### Pattern 4: Task-Derived Seed (Rideshare / Delivery)
86
+
87
+ For event-based verification where both parties are matched dynamically:
88
+
89
+ ```
90
+ Task accepted on platform
91
+ → Platform generates task_secret (256-bit random)
92
+ → Shared with both parties via encrypted channel
93
+ → Both parties: seed = deriveSeed(task_secret, requester_id, provider_id)
94
+ ```
95
+
96
+ ```typescript
97
+ import { deriveSeed, createSession } from 'canary-kit/session'
98
+
99
+ const seed = deriveSeed(taskSecret, requesterId, providerId)
100
+
101
+ const session = createSession({
102
+ secret: seed,
103
+ namespace: 'dispatch',
104
+ roles: ['requester', 'provider'],
105
+ myRole: 'provider',
106
+ preset: 'handoff',
107
+ counter: taskId,
108
+ })
109
+ ```
110
+
111
+ ## Call Centre Integration
112
+
113
+ ### Architecture
114
+
115
+ ```
116
+ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐
117
+ │ Customer │ │ Agent │ │ Seed Service │
118
+ │ (app) │ │ (desktop) │ │ (backend) │
119
+ │ │ │ │ │ │
120
+ │ seed ───────┼─────┼── seed ──────┼─────┼── master_key │
121
+ │ │ │ │ │ + customer_id │
122
+ │ createSession │ createSession│ │ + seed_version │
123
+ │ │ │ │ │ │
124
+ │ myToken() ──┼──→──┼── verify() ──│ │ │
125
+ │ │ │ │ │ │
126
+ │ verify() ←──┼──←──┼── myToken() │ │ │
127
+ └─────────────┘ └──────────────┘ └──────────────────┘
128
+ ```
129
+
130
+ ### Agent UX
131
+
132
+ The agent's screen shows:
133
+ - **Expect to hear:** the caller's current word (derived from `theirToken()`)
134
+ - **Your word:** the agent's current word (derived from `myToken()`)
135
+ - Countdown bar showing time until rotation
136
+ - Verification status: green tick (valid), red cross (invalid), amber alert (duress)
137
+
138
+ On verification:
139
+ 1. Agent asks: "What's your verification word?"
140
+ 2. Customer speaks their word
141
+ 3. Agent types it into the system → `session.verify(spoken)`
142
+ 4. System shows result (green/red/amber)
143
+ 5. Agent reads their word aloud for the customer to verify
144
+
145
+ ### Customer UX
146
+
147
+ The customer's app shows:
148
+ - **Your word:** the word to speak to the agent
149
+ - **Expect to hear:** the word the agent should speak back
150
+ - Countdown bar showing rotation timer
151
+ - Tap to reveal (hidden by default for shoulder-surfing protection)
152
+
153
+ ### Duress Handling
154
+
155
+ When duress is detected (caller speaks their duress word instead of their
156
+ verification word), the agent's system:
157
+
158
+ 1. **Shows normal "verified" to the agent** — maintaining plausible deniability
159
+ 2. **Silently triggers the duress protocol** — security team alert, call recording flagged
160
+ 3. **Logs the event** for compliance and investigation
161
+
162
+ The caller never reveals they are under coercion. The attacker sees a normal
163
+ verification succeed and has no way to know the duress path was triggered.
164
+
165
+ ```typescript
166
+ const result = session.verify(spokenWord)
167
+
168
+ if (result.status === 'duress') {
169
+ // Show normal success to the agent (deniability)
170
+ showVerified()
171
+ // Silently alert security team
172
+ triggerDuressProtocol(result.identities)
173
+ }
174
+ ```
175
+
176
+ ## Security Considerations
177
+
178
+ ### Master Key Management
179
+
180
+ - Store master keys in HSMs (Hardware Security Modules)
181
+ - Rotate master keys on a schedule
182
+ - `seed_version` allows seamless customer migration during rotation
183
+ - Never expose master keys to application code — derive seeds via a secure service
184
+
185
+ ### Seed Storage
186
+
187
+ | Side | Storage | Protection |
188
+ |------|---------|------------|
189
+ | Client (iOS) | Keychain | Biometric / device PIN |
190
+ | Client (Android) | KeyStore | Biometric / device PIN |
191
+ | Client (browser) | IndexedDB | Encrypted with user PIN |
192
+ | Server | Encrypted database | Access-controlled, audit-logged |
193
+
194
+ ### Rotation Strategy
195
+
196
+ - **Quarterly rotation** for standard accounts (update `seed_version`)
197
+ - **Immediate rotation** after any suspected compromise
198
+ - `seed_version` increment does not require customer re-enrolment
199
+ - Old seeds should be invalidated server-side after rotation grace period
200
+
201
+ ### Threat Model
202
+
203
+ | Threat | Mitigated? | How |
204
+ |--------|-----------|-----|
205
+ | Voice cloning (deepfake) | Yes | Token derived from secret, not voice |
206
+ | Eavesdropping | Partially | Tokens rotate; old tokens are useless |
207
+ | Stolen device | Partially | Seed in secure storage behind biometrics/PIN |
208
+ | Compromised master key | No | Requires HSM + procedural controls |
209
+ | Social engineering | Yes | Bidirectional — attacker must know the secret |
210
+ | Coercion / duress | Yes | Distinct duress token triggers silent alert |
211
+
212
+ ## Example: Insurance Phone Verification
213
+
214
+ End-to-end walkthrough using Aviva as the example:
215
+
216
+ ```typescript
217
+ import { createSession } from 'canary-kit/session'
218
+
219
+ // Agent's system — seed was derived from master_key + customer_id
220
+ const agentSession = createSession({
221
+ secret: customerSeed,
222
+ namespace: 'aviva',
223
+ roles: ['caller', 'agent'],
224
+ myRole: 'agent',
225
+ preset: 'call',
226
+ theirIdentity: customerId,
227
+ })
228
+
229
+ // 1. Agent asks: "What's your verification word?"
230
+ // 2. Customer speaks their word
231
+ const result = agentSession.verify(spokenWord)
232
+
233
+ if (result.status === 'valid') {
234
+ // ✓ Customer identity confirmed
235
+ // 3. Agent speaks their word for the customer to verify
236
+ const agentWord = agentSession.myToken()
237
+ // Agent reads aloud: "Your confirmation word is: choose"
238
+ } else if (result.status === 'duress') {
239
+ // ⚠ Customer is under coercion — silent alert
240
+ showVerified() // maintain deniability
241
+ triggerDuressProtocol(result.identities)
242
+ } else {
243
+ // ✗ Verification failed — escalate
244
+ escalateToSecurity()
245
+ }
246
+ ```
247
+
248
+ ## Beacon Privacy: Timing Correlation
249
+
250
+ Location beacons (`encryptBeacon`) encrypt the geohash payload with AES-256-GCM,
251
+ but the Nostr event metadata — `created_at`, publisher pubkey, and the `h` group
252
+ tag — is visible to relay operators and traffic analysts.
253
+
254
+ **Risk:** If beacons are published at a fixed interval (e.g. every 300 seconds),
255
+ an observer can:
256
+
257
+ - **Link beacons over time** by matching the regular cadence from a single pubkey
258
+ - **Detect online/offline transitions** when the cadence stops or resumes
259
+ - **Correlate pubkey rotations** by matching timing patterns across old and new keys
260
+
261
+ The content (location) remains encrypted, but the *pattern of publishing* leaks
262
+ information about a member's connectivity and movement schedule.
263
+
264
+ ### Mitigation: Publish Jitter
265
+
266
+ Applications SHOULD add random jitter to the beacon publish interval. canary-kit
267
+ encrypts beacon payloads but does not control scheduling — jitter must be applied
268
+ at the application layer.
269
+
270
+ Recommended jitter by threat profile:
271
+
272
+ | Preset | Jitter | Rationale |
273
+ |--------------|---------------------|----------------------------------------------|
274
+ | `family` | None required | Members know each other; timing is not sensitive |
275
+ | `enterprise` | ±20% of interval | Reduces cadence fingerprinting across employees |
276
+ | `field-ops` | ±30–50% of interval | Online/offline patterns are operationally sensitive |
277
+
278
+ Example (applying jitter in your publish loop):
279
+
280
+ ```typescript
281
+ const baseInterval = group.beaconInterval // e.g. 300
282
+ const jitterFraction = 0.3 // ±30% for field-ops
283
+ const jitter = baseInterval * jitterFraction * (2 * Math.random() - 1)
284
+ const nextPublishIn = baseInterval + jitter
285
+ setTimeout(() => publishBeacon(), nextPublishIn * 1000)
286
+ ```
287
+
288
+ For high-threat deployments, also consider:
289
+
290
+ - **Variable-rate publishing** — draw intervals from an exponential distribution
291
+ rather than a jittered fixed interval
292
+ - **Relay diversity** — publish each beacon to a different relay to prevent any
293
+ single operator from seeing the full cadence
294
+ - **Batch windows** — all members publish within a coordinated time window so
295
+ individual cadences are masked by group activity
296
+
297
+ ## Cross-Device State Sync
298
+
299
+ Group-based deployments (family safety, field operations) must consider how
300
+ group state reaches a user's second device. The seed, members, and settings
301
+ are security-critical — they must not be transmitted in the clear.
302
+
303
+ ### Recommended: Encrypted Vault
304
+
305
+ Encrypt the full group state with the user's own key and store it on an
306
+ available transport. On login from a new device, fetch and decrypt the vault.
307
+
308
+ Properties an implementation should preserve:
309
+
310
+ | Property | Why |
311
+ |----------|-----|
312
+ | **Self-encrypted** | Only the user's own key can decrypt — the storage layer sees an opaque blob |
313
+ | **No metadata leakage** | The transport must not reveal how many groups exist, who the members are, or that the blob is a CANARY vault |
314
+ | **Transport-agnostic** | The vault is a single encrypted blob. It can be stored on a relay, a cloud bucket, a USB stick, or passed over a mesh radio |
315
+ | **Conflict resolution** | When two devices have diverged, the merge strategy must be deterministic. Recommend: higher epoch wins (rekey happened), then higher counter wins, otherwise keep local |
316
+ | **Offline-first** | A device that has never synced must still function. The vault is an enhancement, not a dependency |
317
+
318
+ ### What to store
319
+
320
+ Include everything needed to derive tokens and verify members:
321
+
322
+ - Group seed, counter, epoch, usage offset
323
+ - Member list and admin list
324
+ - Rotation interval, word count, tolerance, encoding format
325
+ - Relay configuration (for online groups)
326
+ - Display names (advisory, not security-critical)
327
+
328
+ Exclude ephemeral or device-local state:
329
+
330
+ - Beacon positions (device-local, stale quickly)
331
+ - Liveness check-in timestamps (device-local)
332
+ - UI preferences that are per-device
333
+
334
+ ### What NOT to do
335
+
336
+ - **Do not sync seeds in plaintext** — even over TLS, storage backends may log or cache
337
+ - **Do not use a shared encryption key** — each user encrypts their own vault with their own key
338
+ - **Do not expose group count** — an attacker learning "this person has 3 safety groups" is itself sensitive information
339
+ - **Do not require sync for operation** — the app must work offline after initial group setup
340
+
341
+ ### Reference implementation
342
+
343
+ The canary-kit demo app (`app/nostr/vault.ts`) implements this pattern using
344
+ NIP-44 self-encryption and NIP-78 application-specific replaceable events on
345
+ Nostr relays. The vault is a single JSON blob containing all group states,
346
+ encrypted with the user's own keypair, published as a kind 30078 event with
347
+ a `d` tag of `canary:vault`.
348
+
349
+ ## Licence
350
+
351
+ MIT — same as canary-kit.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TheCryptoDonkey
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.