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/README.md ADDED
@@ -0,0 +1,187 @@
1
+ # canary-kit
2
+
3
+ > Deepfake-proof identity verification. Open protocol, minimal dependencies.
4
+
5
+ [![npm](https://img.shields.io/npm/v/canary-kit)](https://www.npmjs.com/package/canary-kit)
6
+ [![CI](https://github.com/TheCryptoDonkey/canary-kit/actions/workflows/ci.yml/badge.svg)](https://github.com/TheCryptoDonkey/canary-kit/actions)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-native-blue)](https://www.typescriptlang.org/)
9
+ [![Coverage](https://img.shields.io/badge/coverage-95%25-brightgreen)](vitest.config.ts)
10
+
11
+ **[Interactive Demo](https://thecryptodonkey.github.io/canary-kit/)** · [Protocol Spec](CANARY.md) · [Nostr Binding](NIP-CANARY.md) · [Integration Guide](INTEGRATION.md)
12
+
13
+ ## The Problem
14
+
15
+ Voice phishing surged 442% in 2025. AI can clone a voice from three seconds of
16
+ audio. The tools that were supposed to protect us are failing:
17
+
18
+ - **Security questions** are one-directional and socially engineerable
19
+ - **Voice biometrics** — 91% of US banks are reconsidering after deepfake attacks
20
+ - **TOTP codes** prove you to a server, but never prove the server to you
21
+ - **"Family safe words"** are static, never rotate, and have no duress signalling
22
+
23
+ CANARY is the first protocol that combines **bidirectional verification** (both
24
+ sides prove identity), **coercion resistance** (duress tokens), and **spoken-word
25
+ output** — three properties that have never existed together in a standard.
26
+
27
+ It works because cloning a voice doesn't help you derive the right word. Only
28
+ knowledge of the shared secret does.
29
+
30
+ ## Quick Start
31
+
32
+ ```bash
33
+ npm install canary-kit
34
+ ```
35
+
36
+ ### Phone Verification (Insurance, Banking)
37
+
38
+ ```typescript
39
+ import { createSession } from 'canary-kit/session'
40
+
41
+ const session = createSession({
42
+ secret: sharedSeed,
43
+ namespace: 'aviva',
44
+ roles: ['caller', 'agent'],
45
+ myRole: 'agent',
46
+ preset: 'call',
47
+ })
48
+
49
+ session.myToken() // "choose" — what I speak
50
+ session.theirToken() // "bid" — what I expect to hear
51
+ session.verify('bid') // { status: 'valid' }
52
+ ```
53
+
54
+ ### Family / Team Verification
55
+
56
+ ```typescript
57
+ import { createGroup, getCurrentWord, verifyWord, getCounter } from 'canary-kit'
58
+
59
+ const group = createGroup({
60
+ name: 'Family',
61
+ members: [alicePubkey, bobPubkey],
62
+ preset: 'family',
63
+ })
64
+
65
+ getCurrentWord(group) // "falcon"
66
+ ```
67
+
68
+ ## Use Cases
69
+
70
+ | Use case | Preset | Rotation | What it replaces |
71
+ |----------|--------|----------|------------------|
72
+ | Insurance phone calls | `call` | 30 seconds | Security questions |
73
+ | Banking phone calls | `call` | 30 seconds | Voice biometrics, callbacks |
74
+ | Rideshare/delivery handoff | `handoff` | Single-use | Random PINs |
75
+ | Family safety | `family` | 7 days | Static safe words |
76
+ | Journalism / activism | `field-ops` | 24 hours | Nothing (no existing standard) |
77
+ | Enterprise incident response | `enterprise` | 48 hours | Challenge-response over email |
78
+
79
+ ## Why Not Just...
80
+
81
+ | Solution | Limitation CANARY solves |
82
+ |----------|------------------------|
83
+ | Security questions | One-directional. Socially engineerable. No rotation. |
84
+ | Voice biometrics | Defeated by AI voice cloning. One-directional. |
85
+ | TOTP (Google Auth) | Machine-readable digits, not spoken words. No duress. One-directional. |
86
+ | Callback numbers | Slow. Doesn't prove the agent's identity. |
87
+ | BIP-39 wordlist | No verification protocol. No rotation. No duress. |
88
+ | "Family safe word" | Static. No rotation. No duress. No protocol. |
89
+ | **CANARY** | **Bidirectional. Deepfake-proof. Duress-aware. Rotating. Offline. Open.** |
90
+
91
+ ## Why Canary
92
+
93
+ **Bidirectional.** Both sides prove identity. The caller proves they know the secret, and the agent proves it back. Neither can impersonate the other.
94
+
95
+ **Built on proven primitives.** CANARY extends the HMAC-counter pattern from HOTP (RFC 4226) and TOTP (RFC 6238) to human-to-human spoken verification, adding duress signalling and coercion resistance.
96
+
97
+ **Offline-first.** Words are derived locally from a shared seed and a time-based counter. No network is required after initial setup.
98
+
99
+ **Duress-aware.** Every party has a personal duress word distinct from the verification word. Speaking it silently alerts the system while giving the attacker plausible deniability.
100
+
101
+ **Automatic rotation.** Configurable intervals — 30 seconds for phone calls, 7 days for family groups.
102
+
103
+ **Minimal dependencies.** Core crypto is pure JavaScript. Only `@scure/bip32` and `@scure/bip39` for mnemonic key recovery. Requires `globalThis.crypto` (Web Crypto API): all browsers, Node.js 22+, Deno, and edge runtimes.
104
+
105
+ **Protocol-grade.** Formal specification with published test vectors and a curated 2048-word spoken-clarity wordlist.
106
+
107
+ ## Compatibility
108
+
109
+ | Runtime | Version | Notes |
110
+ |---------|---------|-------|
111
+ | Node.js | 22+ | Full support (`globalThis.crypto` required) |
112
+ | Deno | 1.x+ | Full support |
113
+ | Bun | 1.x+ | Full support |
114
+ | Browsers | All modern | Chrome, Firefox, Safari, Edge |
115
+ | Cloudflare Workers | Yes | Web Crypto API available |
116
+ | React Native | Via polyfill | Needs `crypto.subtle` polyfill |
117
+
118
+ ESM-only. Eight subpath exports for tree-shaking:
119
+
120
+ ```typescript
121
+ import { createSession } from 'canary-kit/session' // just sessions
122
+ import { deriveToken } from 'canary-kit/token' // just derivation
123
+ import { encodeAsWords } from 'canary-kit/encoding' // just encoding
124
+ import { WORDLIST } from 'canary-kit/wordlist' // just the wordlist
125
+ import { buildGroupEvent } from 'canary-kit/nostr' // just Nostr
126
+ import { encryptBeacon } from 'canary-kit/beacon' // just beacons
127
+ import { applySyncMessage } from 'canary-kit/sync' // just sync protocol
128
+ ```
129
+
130
+ ## Security
131
+
132
+ - **Minimal runtime dependencies** — only `@scure/bip32` and `@scure/bip39` for mnemonic key recovery; core crypto is pure JS
133
+ - **Automated publishing** — GitHub Actions with OIDC trusted publishing, no stored tokens
134
+ - **Provenance signed** — npm provenance attestation enabled
135
+ - **Protocol-grade test vectors** — frozen canonical vectors in both CANARY.md and NIP-CANARY.md; any conformant implementation must produce identical results
136
+ - **Timing-safe byte compare** — `timingSafeEqual()` utility provided for constant-time byte operations
137
+ - **Bounded tolerance** — `MAX_TOLERANCE` cap prevents pathological iteration
138
+
139
+ See [SECURITY.md](SECURITY.md) for vulnerability disclosure and known limitations. See [CANARY.md](CANARY.md) for the full security analysis.
140
+
141
+ ## API
142
+
143
+ | Subpath export | Key functions |
144
+ |---|---|
145
+ | `canary-kit/session` | `createSession`, `generateSeed`, `deriveSeed` |
146
+ | `canary-kit/token` | `deriveToken`, `verifyToken`, `deriveDuressToken`, `deriveLivenessToken` |
147
+ | `canary-kit/encoding` | `encodeAsWords`, `encodeAsPin`, `encodeAsHex` |
148
+ | `canary-kit` | `createGroup`, `getCurrentWord`, `verifyWord`, `addMember`, `reseed` |
149
+ | `canary-kit/nostr` | `buildGroupEvent`, `buildBeaconEvent`, + 4 more builders |
150
+ | `canary-kit/beacon` | `encryptBeacon`, `decryptBeacon`, `buildDuressAlert` |
151
+ | `canary-kit/sync` | `applySyncMessage`, `encodeSyncMessage`, `deriveGroupKey` |
152
+ | `canary-kit/wordlist` | `WORDLIST`, `getWord`, `indexOf` |
153
+
154
+ Full API documentation with signatures, types, and presets: **[API.md](API.md)**
155
+
156
+ ## Protocol
157
+
158
+ The full protocol specification is in [CANARY.md](CANARY.md). The Nostr binding is in [NIP-CANARY.md](NIP-CANARY.md). The integration guide for finance/enterprise is in [INTEGRATION.md](INTEGRATION.md).
159
+
160
+ | Event | Kind | Type |
161
+ |---|---|---|
162
+ | Group announcement | `38800` | Replaceable |
163
+ | Seed distribution | `28800` | Ephemeral |
164
+ | Member update | `38801` | Replaceable |
165
+ | Reseed | `28801` | Ephemeral |
166
+ | Word used | `28802` | Ephemeral |
167
+ | Encrypted location beacon | `20800` | Ephemeral |
168
+
169
+ Content is encrypted with **NIP-44**. Events may carry a **NIP-40** `expiration` tag.
170
+
171
+ ## For AI Assistants
172
+
173
+ - [llms.txt](llms.txt) — concise API summary
174
+ - [llms-full.txt](llms-full.txt) — complete reference with all type signatures
175
+
176
+ ## Support
177
+
178
+ For issues and feature requests, see [GitHub Issues](https://github.com/TheCryptoDonkey/canary-kit/issues).
179
+
180
+ If you find canary-kit useful, consider sending a tip:
181
+
182
+ - **Lightning:** `thedonkey@strike.me`
183
+ - **Nostr zaps:** `npub1mgvlrnf5hm9yf0n5mf9nqmvarhvxkc6remu5ec3vf8r0txqkuk7su0e7q2`
184
+
185
+ ## Licence
186
+
187
+ MIT
package/SECURITY.md ADDED
@@ -0,0 +1,92 @@
1
+ # Security Policy
2
+
3
+ ## Supported Versions
4
+
5
+ | Version | Supported |
6
+ |---------|-----------|
7
+ | 1.x.x | Yes |
8
+ | < 1.0.0 | No |
9
+
10
+ ## Reporting a Vulnerability
11
+
12
+ **Please report security vulnerabilities through GitHub Security Advisories only.**
13
+
14
+ 1. Go to [Security Advisories](https://github.com/TheCryptoDonkey/canary-kit/security/advisories)
15
+ 2. Click **"New draft security advisory"**
16
+ 3. Fill in the details of the vulnerability
17
+
18
+ **Do not** report security vulnerabilities through public GitHub issues, pull requests, or any other public channel.
19
+
20
+ ### What to Include
21
+
22
+ - Description of the vulnerability
23
+ - Steps to reproduce
24
+ - Affected versions
25
+ - Potential impact
26
+ - Suggested fix (if any)
27
+
28
+ ### Response Timeline
29
+
30
+ - **Acknowledgement:** Within 72 hours
31
+ - **Initial assessment:** Within 1 week
32
+ - **Fix timeline:** Depends on severity; Critical issues targeted within 2 weeks
33
+
34
+ ### Severity Definitions
35
+
36
+ | Level | Definition |
37
+ |-------|-----------|
38
+ | Critical | Breaks a core security property (token unpredictability, duress indistinguishability, coercion resistance) |
39
+ | High | Significant weakness exploitable by a defined adversary profile |
40
+ | Medium | Defence-in-depth gap; exploitable under specific conditions |
41
+ | Low | Minor issue; hardening opportunity |
42
+
43
+ ### Scope
44
+
45
+ The following components are in scope for security reports:
46
+
47
+ - Protocol specification (`CANARY.md`, `NIP-CANARY.md`)
48
+ - Reference implementation (`src/*.ts`)
49
+ - Cryptographic primitives (`src/crypto.ts`)
50
+ - Sync protocol (`src/sync.ts`, `src/sync-crypto.ts`)
51
+
52
+ Out of scope: demo application (`app/`), build tooling, CI/CD.
53
+
54
+ ## Security Model
55
+
56
+ CANARY's security rests on the secrecy of the shared seed and the properties of HMAC-SHA256. The protocol does **not** protect against:
57
+
58
+ - Compromise of the shared seed (all tokens derivable until reseed)
59
+ - Side-channel attacks inherent to JavaScript runtimes (timing, memory access patterns)
60
+ - An attacker who can observe both parties' tokens in real time
61
+
62
+ ### Known Limitations of JavaScript Cryptography
63
+
64
+ - **No constant-time guarantees.** JavaScript engines may optimise away constant-time patterns. A `timingSafeEqual()` utility is provided and used for all token comparisons. The CANARY threat model (spoken-word verification over voice calls) makes sub-millisecond timing attacks impractical.
65
+ - **HMAC-SHA256 is synchronous.** The core derivation uses a pure JavaScript SHA-256 implementation rather than Web Crypto API, because derivation must be synchronous (called frequently, deterministic, offline). The implementation follows FIPS 180-4.
66
+ - **AES-256-GCM is async.** Beacon encryption uses `crypto.subtle` (Web Crypto API) and is the only async operation in the library.
67
+
68
+ ### Browser Storage
69
+
70
+ The demo/alpha browser application stores group seeds and private keys in `localStorage`:
71
+
72
+ - **Without PIN:** Secrets are stored in plaintext. Any script running on the page origin can read them.
73
+ - **With PIN:** Secrets are encrypted with AES-256-GCM via a PBKDF2-derived key (600,000 iterations, non-extractable `CryptoKey`). Secrets are only readable in memory while the app is unlocked.
74
+
75
+ **This is NOT suitable for enterprise or high-security deployments.** Production implementations on native platforms MUST use platform secure storage (iOS Keychain, Android Keystore, OS credential manager).
76
+
77
+ The browser app mitigates this with:
78
+ - A strict `Content-Security-Policy` (`script-src 'self'`) blocking injected scripts
79
+ - Zero third-party runtime dependencies in the production bundle
80
+ - Auto-lock after configurable inactivity period (default: 5 minutes)
81
+
82
+ ### Supply Chain
83
+
84
+ - **Minimal runtime dependencies.** Only `@scure/bip32` and `@scure/bip39` for mnemonic key recovery; core crypto is pure JS.
85
+ - **Automated publishing.** Releases are built and published via GitHub Actions with OIDC trusted publishing — no npm tokens stored.
86
+ - **Provenance signed.** npm provenance attestation is enabled.
87
+
88
+ ## Security Documents
89
+
90
+ - [Threat Model](THREAT-MODEL.md) — Adversary profiles, security properties, attack trees
91
+ - [Audit Report](AUDIT.md) — Adversarial security audit with findings register
92
+ - [Protocol Specification](CANARY.md) — Full protocol spec with security analysis
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Encrypted location beacons and duress alerts for canary groups.
3
+ *
4
+ * Key derivation: sync (HMAC-SHA256 from crypto.ts)
5
+ * Encryption: async (AES-256-GCM via crypto.subtle)
6
+ *
7
+ * The sync/async split is intentional:
8
+ * - Word derivation stays sync (called frequently, deterministic)
9
+ * - Beacon/duress encryption is async (event-driven, one call per publish)
10
+ */
11
+ /**
12
+ * Derive a 256-bit AES key from the group seed for beacon encryption.
13
+ * Deterministic: same seed always produces the same key.
14
+ *
15
+ * @param seedHex - Group seed as a 64-character lowercase hex string (32 bytes).
16
+ * @returns 32-byte AES-256 key derived via HMAC-SHA256.
17
+ * @throws {Error} If seedHex is not a valid 64-character hex string.
18
+ */
19
+ export declare function deriveBeaconKey(seedHex: string): Uint8Array;
20
+ /**
21
+ * Derive a 256-bit AES key from the group seed for duress alert encryption.
22
+ * Uses a distinct HMAC info string from beacon keys for domain separation —
23
+ * prevents cross-type key reuse between normal beacons and duress alerts.
24
+ *
25
+ * @param seedHex - Group seed as a 64-character lowercase hex string (32 bytes).
26
+ * @returns 32-byte AES-256 key derived via HMAC-SHA256 with duress-specific info.
27
+ * @throws {Error} If seedHex is not a valid 64-character hex string.
28
+ */
29
+ export declare function deriveDuressKey(seedHex: string): Uint8Array;
30
+ /** Decrypted content of a kind 20800 location beacon event. */
31
+ export interface BeaconPayload {
32
+ geohash: string;
33
+ precision: number;
34
+ timestamp: number;
35
+ }
36
+ /**
37
+ * Encrypt a location beacon payload with the group's beacon key.
38
+ * Returns a base64 string suitable for a Nostr event's `content` field.
39
+ *
40
+ * @param key - 32-byte AES-256 key from {@link deriveBeaconKey}.
41
+ * @param geohash - Geohash string representing the location.
42
+ * @param precision - Geohash precision level (1-11).
43
+ * @returns Base64-encoded ciphertext (12-byte IV prepended to AES-GCM output).
44
+ * @throws {Error} If key is not 32 bytes.
45
+ */
46
+ export declare function encryptBeacon(key: Uint8Array, geohash: string, precision: number): Promise<string>;
47
+ /**
48
+ * Decrypt a location beacon event's content.
49
+ * Throws if the key is wrong or the ciphertext is tampered with (AES-GCM authentication).
50
+ *
51
+ * @param key - 32-byte AES-256 key from {@link deriveBeaconKey}.
52
+ * @param content - Base64-encoded ciphertext from the beacon event's content field.
53
+ * @returns Decrypted {@link BeaconPayload} with geohash, precision, and timestamp.
54
+ * @throws {Error} If decryption fails, ciphertext is tampered, or payload is malformed.
55
+ */
56
+ export declare function decryptBeacon(key: Uint8Array, content: string): Promise<BeaconPayload>;
57
+ /** Decrypted content of a duress alert beacon (kind 20800, AES-GCM encrypted). */
58
+ export interface DuressAlert {
59
+ type: 'duress';
60
+ member: string;
61
+ geohash: string;
62
+ precision: number;
63
+ locationSource: 'beacon' | 'verifier' | 'none';
64
+ timestamp: number;
65
+ }
66
+ /** Location info for a duress alert. Null means no location available. */
67
+ export interface DuressLocation {
68
+ geohash: string;
69
+ precision: number;
70
+ locationSource: 'beacon' | 'verifier';
71
+ }
72
+ /**
73
+ * Construct a duress alert payload.
74
+ *
75
+ * The caller is responsible for geohash encoding and precision upgrade
76
+ * (e.g. using geohash-kit to re-encode at precision 11 for duress).
77
+ * This function just assembles the payload.
78
+ *
79
+ * @param memberPubkey - 64-character lowercase hex pubkey of the member under duress.
80
+ * @param location - Location info with geohash, precision, and source; or null if unavailable.
81
+ * @returns A {@link DuressAlert} payload ready for encryption.
82
+ * @throws {Error} If memberPubkey is not a valid 64-character hex string.
83
+ */
84
+ export declare function buildDuressAlert(memberPubkey: string, location: DuressLocation | null): DuressAlert;
85
+ /**
86
+ * Encrypt a duress alert with the group's duress key (from deriveDuressKey).
87
+ * Returns a base64 string for the Nostr event's `content` field.
88
+ *
89
+ * @param key - 32-byte AES-256 key from {@link deriveDuressKey}.
90
+ * @param alert - The {@link DuressAlert} payload to encrypt.
91
+ * @returns Base64-encoded ciphertext (12-byte IV prepended to AES-GCM output).
92
+ * @throws {Error} If key is not 32 bytes.
93
+ */
94
+ export declare function encryptDuressAlert(key: Uint8Array, alert: DuressAlert): Promise<string>;
95
+ /**
96
+ * Decrypt a duress alert event's content.
97
+ *
98
+ * @param key - 32-byte AES-256 key from {@link deriveDuressKey}.
99
+ * @param content - Base64-encoded ciphertext from the duress alert event's content field.
100
+ * @returns Decrypted {@link DuressAlert} payload.
101
+ * @throws {Error} If decryption fails, ciphertext is tampered, or payload is malformed.
102
+ */
103
+ export declare function decryptDuressAlert(key: Uint8Array, content: string): Promise<DuressAlert>;
104
+ //# sourceMappingURL=beacon.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"beacon.d.ts","sourceRoot":"","sources":["../src/beacon.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAyBH;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAG3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAG3D;AA2CD,+DAA+D;AAC/D,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,CAAC,CAOjB;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,aAAa,CAAC,CAaxB;AAMD,kFAAkF;AAClF,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,QAAQ,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,QAAQ,GAAG,UAAU,GAAG,MAAM,CAAA;IAC9C,SAAS,EAAE,MAAM,CAAA;CAClB;AAED,0EAA0E;AAC1E,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;IACjB,cAAc,EAAE,QAAQ,GAAG,UAAU,CAAA;CACtC;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,cAAc,GAAG,IAAI,GAC9B,WAAW,CAsBb;AAED;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,UAAU,EACf,KAAK,EAAE,WAAW,GACjB,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED;;;;;;;GAOG;AACH,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,WAAW,CAAC,CAqBtB"}
package/dist/beacon.js ADDED
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Encrypted location beacons and duress alerts for canary groups.
3
+ *
4
+ * Key derivation: sync (HMAC-SHA256 from crypto.ts)
5
+ * Encryption: async (AES-256-GCM via crypto.subtle)
6
+ *
7
+ * The sync/async split is intentional:
8
+ * - Word derivation stays sync (called frequently, deterministic)
9
+ * - Beacon/duress encryption is async (event-driven, one call per publish)
10
+ */
11
+ import { hmacSha256, hexToBytes, bytesToBase64, base64ToBytes } from './crypto.js';
12
+ const HEX_64_RE = /^[0-9a-f]{64}$/;
13
+ // ---------------------------------------------------------------------------
14
+ // Key Derivation (sync)
15
+ // ---------------------------------------------------------------------------
16
+ const BEACON_KEY_INFO = new TextEncoder().encode('canary:beacon:key');
17
+ const DURESS_KEY_INFO = new TextEncoder().encode('canary:duress:key');
18
+ function validateSeedHex(seedHex) {
19
+ if (!HEX_64_RE.test(seedHex)) {
20
+ throw new Error('seedHex must be a 64-character lowercase hex string (32 bytes)');
21
+ }
22
+ }
23
+ function validateAesKey(key) {
24
+ if (key.length !== 32) {
25
+ throw new Error('AES-256-GCM requires a 32-byte key');
26
+ }
27
+ }
28
+ /**
29
+ * Derive a 256-bit AES key from the group seed for beacon encryption.
30
+ * Deterministic: same seed always produces the same key.
31
+ *
32
+ * @param seedHex - Group seed as a 64-character lowercase hex string (32 bytes).
33
+ * @returns 32-byte AES-256 key derived via HMAC-SHA256.
34
+ * @throws {Error} If seedHex is not a valid 64-character hex string.
35
+ */
36
+ export function deriveBeaconKey(seedHex) {
37
+ validateSeedHex(seedHex);
38
+ return hmacSha256(hexToBytes(seedHex), BEACON_KEY_INFO);
39
+ }
40
+ /**
41
+ * Derive a 256-bit AES key from the group seed for duress alert encryption.
42
+ * Uses a distinct HMAC info string from beacon keys for domain separation —
43
+ * prevents cross-type key reuse between normal beacons and duress alerts.
44
+ *
45
+ * @param seedHex - Group seed as a 64-character lowercase hex string (32 bytes).
46
+ * @returns 32-byte AES-256 key derived via HMAC-SHA256 with duress-specific info.
47
+ * @throws {Error} If seedHex is not a valid 64-character hex string.
48
+ */
49
+ export function deriveDuressKey(seedHex) {
50
+ validateSeedHex(seedHex);
51
+ return hmacSha256(hexToBytes(seedHex), DURESS_KEY_INFO);
52
+ }
53
+ // ---------------------------------------------------------------------------
54
+ // AES-256-GCM helpers (async, shared by beacon and duress)
55
+ // ---------------------------------------------------------------------------
56
+ async function aesGcmEncrypt(key, plaintext) {
57
+ validateAesKey(key);
58
+ const iv = crypto.getRandomValues(new Uint8Array(12));
59
+ const cryptoKey = await crypto.subtle.importKey('raw', key, { name: 'AES-GCM' }, false, ['encrypt']);
60
+ const ciphertext = new Uint8Array(await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, cryptoKey, plaintext));
61
+ // Prepend 12-byte IV to ciphertext, then base64
62
+ const combined = new Uint8Array(12 + ciphertext.length);
63
+ combined.set(iv);
64
+ combined.set(ciphertext, 12);
65
+ return bytesToBase64(combined);
66
+ }
67
+ async function aesGcmDecrypt(key, content) {
68
+ validateAesKey(key);
69
+ const combined = base64ToBytes(content);
70
+ const MIN_CIPHERTEXT_LEN = 28; // 12-byte IV + 16-byte GCM auth tag
71
+ if (combined.length < MIN_CIPHERTEXT_LEN) {
72
+ throw new Error('Invalid ciphertext: too short (minimum 28 bytes: 12-byte IV + 16-byte GCM tag)');
73
+ }
74
+ const iv = combined.slice(0, 12);
75
+ const ciphertext = combined.slice(12);
76
+ const cryptoKey = await crypto.subtle.importKey('raw', key, { name: 'AES-GCM' }, false, ['decrypt']);
77
+ return new Uint8Array(await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, cryptoKey, ciphertext));
78
+ }
79
+ /**
80
+ * Encrypt a location beacon payload with the group's beacon key.
81
+ * Returns a base64 string suitable for a Nostr event's `content` field.
82
+ *
83
+ * @param key - 32-byte AES-256 key from {@link deriveBeaconKey}.
84
+ * @param geohash - Geohash string representing the location.
85
+ * @param precision - Geohash precision level (1-11).
86
+ * @returns Base64-encoded ciphertext (12-byte IV prepended to AES-GCM output).
87
+ * @throws {Error} If key is not 32 bytes.
88
+ */
89
+ export async function encryptBeacon(key, geohash, precision) {
90
+ const payload = {
91
+ geohash,
92
+ precision,
93
+ timestamp: Math.floor(Date.now() / 1000),
94
+ };
95
+ return aesGcmEncrypt(key, new TextEncoder().encode(JSON.stringify(payload)));
96
+ }
97
+ /**
98
+ * Decrypt a location beacon event's content.
99
+ * Throws if the key is wrong or the ciphertext is tampered with (AES-GCM authentication).
100
+ *
101
+ * @param key - 32-byte AES-256 key from {@link deriveBeaconKey}.
102
+ * @param content - Base64-encoded ciphertext from the beacon event's content field.
103
+ * @returns Decrypted {@link BeaconPayload} with geohash, precision, and timestamp.
104
+ * @throws {Error} If decryption fails, ciphertext is tampered, or payload is malformed.
105
+ */
106
+ export async function decryptBeacon(key, content) {
107
+ const plaintext = await aesGcmDecrypt(key, content);
108
+ let parsed;
109
+ try {
110
+ parsed = JSON.parse(new TextDecoder().decode(plaintext));
111
+ }
112
+ catch {
113
+ throw new Error('Invalid beacon payload: decrypted content is not valid JSON');
114
+ }
115
+ const obj = parsed;
116
+ if (typeof obj.geohash !== 'string' || typeof obj.precision !== 'number' || typeof obj.timestamp !== 'number') {
117
+ throw new Error('Invalid beacon payload: missing or malformed required fields');
118
+ }
119
+ return parsed;
120
+ }
121
+ /**
122
+ * Construct a duress alert payload.
123
+ *
124
+ * The caller is responsible for geohash encoding and precision upgrade
125
+ * (e.g. using geohash-kit to re-encode at precision 11 for duress).
126
+ * This function just assembles the payload.
127
+ *
128
+ * @param memberPubkey - 64-character lowercase hex pubkey of the member under duress.
129
+ * @param location - Location info with geohash, precision, and source; or null if unavailable.
130
+ * @returns A {@link DuressAlert} payload ready for encryption.
131
+ * @throws {Error} If memberPubkey is not a valid 64-character hex string.
132
+ */
133
+ export function buildDuressAlert(memberPubkey, location) {
134
+ if (!HEX_64_RE.test(memberPubkey)) {
135
+ throw new Error(`Invalid member pubkey: expected 64 lowercase hex characters, got ${memberPubkey.length} chars`);
136
+ }
137
+ if (location) {
138
+ return {
139
+ type: 'duress',
140
+ member: memberPubkey,
141
+ geohash: location.geohash,
142
+ precision: location.precision,
143
+ locationSource: location.locationSource,
144
+ timestamp: Math.floor(Date.now() / 1000),
145
+ };
146
+ }
147
+ return {
148
+ type: 'duress',
149
+ member: memberPubkey,
150
+ geohash: '',
151
+ precision: 0,
152
+ locationSource: 'none',
153
+ timestamp: Math.floor(Date.now() / 1000),
154
+ };
155
+ }
156
+ /**
157
+ * Encrypt a duress alert with the group's duress key (from deriveDuressKey).
158
+ * Returns a base64 string for the Nostr event's `content` field.
159
+ *
160
+ * @param key - 32-byte AES-256 key from {@link deriveDuressKey}.
161
+ * @param alert - The {@link DuressAlert} payload to encrypt.
162
+ * @returns Base64-encoded ciphertext (12-byte IV prepended to AES-GCM output).
163
+ * @throws {Error} If key is not 32 bytes.
164
+ */
165
+ export async function encryptDuressAlert(key, alert) {
166
+ return aesGcmEncrypt(key, new TextEncoder().encode(JSON.stringify(alert)));
167
+ }
168
+ /**
169
+ * Decrypt a duress alert event's content.
170
+ *
171
+ * @param key - 32-byte AES-256 key from {@link deriveDuressKey}.
172
+ * @param content - Base64-encoded ciphertext from the duress alert event's content field.
173
+ * @returns Decrypted {@link DuressAlert} payload.
174
+ * @throws {Error} If decryption fails, ciphertext is tampered, or payload is malformed.
175
+ */
176
+ export async function decryptDuressAlert(key, content) {
177
+ const plaintext = await aesGcmDecrypt(key, content);
178
+ let parsed;
179
+ try {
180
+ parsed = JSON.parse(new TextDecoder().decode(plaintext));
181
+ }
182
+ catch {
183
+ throw new Error('Invalid duress alert payload: decrypted content is not valid JSON');
184
+ }
185
+ const obj = parsed;
186
+ const VALID_SOURCES = new Set(['beacon', 'verifier', 'none']);
187
+ if (obj.type !== 'duress' ||
188
+ typeof obj.member !== 'string' ||
189
+ typeof obj.timestamp !== 'number' ||
190
+ typeof obj.geohash !== 'string' ||
191
+ typeof obj.precision !== 'number' ||
192
+ !VALID_SOURCES.has(obj.locationSource)) {
193
+ throw new Error('Invalid duress alert payload: missing or malformed required fields');
194
+ }
195
+ return parsed;
196
+ }
197
+ //# sourceMappingURL=beacon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"beacon.js","sourceRoot":"","sources":["../src/beacon.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ /** Default rotation interval: 7 days in seconds. */
2
+ export declare const DEFAULT_ROTATION_INTERVAL = 604800;
3
+ /**
4
+ * Maximum allowed usage offset above the current time-based counter.
5
+ * Implementations MUST reject counter updates where effective counter > time-based counter + MAX_COUNTER_OFFSET.
6
+ * See CANARY spec §Counter Acceptance.
7
+ */
8
+ export declare const MAX_COUNTER_OFFSET = 100;
9
+ /**
10
+ * Derive the current counter from a unix timestamp and rotation interval.
11
+ * Counter = floor(timestamp / interval).
12
+ *
13
+ * @param timestampSec - Unix timestamp in seconds (non-negative finite number).
14
+ * @param rotationIntervalSec - Rotation interval in seconds (positive finite number, default: 604800 = 7 days).
15
+ * @returns Integer counter value within uint32 range.
16
+ * @throws {RangeError} If timestampSec is negative/non-finite, rotationIntervalSec is non-positive/non-finite, or counter exceeds uint32.
17
+ */
18
+ export declare function getCounter(timestampSec: number, rotationIntervalSec?: number): number;
19
+ /**
20
+ * Derive a counter from an event identifier (e.g. a task ID or Nostr event ID).
21
+ * Uses SHA-256 truncated to 32 bits for a deterministic, uniformly distributed counter.
22
+ * Per CANARY spec §Counter Schemes: event-based counters are deterministic from event ID.
23
+ *
24
+ * @param eventId - String identifier to derive the counter from (e.g. a Nostr event ID).
25
+ * @returns Unsigned 32-bit integer derived from SHA-256 of the event ID.
26
+ */
27
+ export declare function counterFromEventId(eventId: string): number;
28
+ /**
29
+ * Serialise a counter to an 8-byte big-endian Uint8Array.
30
+ * Same encoding as TOTP (RFC 6238).
31
+ *
32
+ * @param counter - Non-negative safe integer to serialise.
33
+ * @returns 8-byte big-endian Uint8Array representation of the counter.
34
+ * @throws {RangeError} If counter is negative, not an integer, or exceeds Number.MAX_SAFE_INTEGER.
35
+ */
36
+ export declare function counterToBytes(counter: number): Uint8Array;
37
+ //# sourceMappingURL=counter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"counter.d.ts","sourceRoot":"","sources":["../src/counter.ts"],"names":[],"mappings":"AAEA,oDAAoD;AACpD,eAAO,MAAM,yBAAyB,SAAU,CAAA;AAEhD;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,MAAM,CAAA;AAErC;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACxB,YAAY,EAAE,MAAM,EACpB,mBAAmB,GAAE,MAAkC,GACtD,MAAM,CAYR;AAED;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAI1D;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAQ1D"}