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.
- package/CANARY.md +1065 -0
- package/INTEGRATION.md +351 -0
- package/LICENSE +21 -0
- package/NIP-CANARY.md +624 -0
- package/README.md +187 -0
- package/SECURITY.md +92 -0
- package/dist/beacon.d.ts +104 -0
- package/dist/beacon.d.ts.map +1 -0
- package/dist/beacon.js +197 -0
- package/dist/beacon.js.map +1 -0
- package/dist/counter.d.ts +37 -0
- package/dist/counter.d.ts.map +1 -0
- package/dist/counter.js +62 -0
- package/dist/counter.js.map +1 -0
- package/dist/crypto.d.ts +111 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +309 -0
- package/dist/crypto.js.map +1 -0
- package/dist/derive.d.ts +68 -0
- package/dist/derive.d.ts.map +1 -0
- package/dist/derive.js +85 -0
- package/dist/derive.js.map +1 -0
- package/dist/encoding.d.ts +56 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +98 -0
- package/dist/encoding.js.map +1 -0
- package/dist/group.d.ts +185 -0
- package/dist/group.d.ts.map +1 -0
- package/dist/group.js +263 -0
- package/dist/group.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/nostr.d.ts +134 -0
- package/dist/nostr.d.ts.map +1 -0
- package/dist/nostr.js +175 -0
- package/dist/nostr.js.map +1 -0
- package/dist/presets.d.ts +26 -0
- package/dist/presets.d.ts.map +1 -0
- package/dist/presets.js +39 -0
- package/dist/presets.js.map +1 -0
- package/dist/session.d.ts +114 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +173 -0
- package/dist/session.js.map +1 -0
- package/dist/sync-crypto.d.ts +66 -0
- package/dist/sync-crypto.d.ts.map +1 -0
- package/dist/sync-crypto.js +125 -0
- package/dist/sync-crypto.js.map +1 -0
- package/dist/sync.d.ts +191 -0
- package/dist/sync.d.ts.map +1 -0
- package/dist/sync.js +568 -0
- package/dist/sync.js.map +1 -0
- package/dist/token.d.ts +186 -0
- package/dist/token.d.ts.map +1 -0
- package/dist/token.js +344 -0
- package/dist/token.js.map +1 -0
- package/dist/verify.d.ts +45 -0
- package/dist/verify.d.ts.map +1 -0
- package/dist/verify.js +59 -0
- package/dist/verify.js.map +1 -0
- package/dist/wordlist.d.ts +28 -0
- package/dist/wordlist.d.ts.map +1 -0
- package/dist/wordlist.js +297 -0
- package/dist/wordlist.js.map +1 -0
- package/llms-full.txt +1461 -0
- package/llms.txt +180 -0
- 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
|
+
[](https://www.npmjs.com/package/canary-kit)
|
|
6
|
+
[](https://github.com/TheCryptoDonkey/canary-kit/actions)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
9
|
+
[](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
|
package/dist/beacon.d.ts
ADDED
|
@@ -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"}
|