canary-kit 0.9.0 → 0.11.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 CHANGED
@@ -31,6 +31,32 @@ The protocol is defined in three layers:
31
31
  monitoring)
32
32
  3. **CANARY-WORDLIST** — Spoken-word encoding optimised for voice clarity
33
33
 
34
+ ## Protocol Layering
35
+
36
+ CANARY builds on two generic protocol specifications:
37
+
38
+ - **[Spoken Token Protocol](https://github.com/TheCryptoDonkey/spoken-token/blob/main/PROTOCOL.md)**
39
+ — defines SPOKEN-DERIVE (the core HMAC-counter-to-words derivation) and SPOKEN-ENCODE
40
+ (word/PIN/hex encoding). CANARY-DERIVE is a superset of SPOKEN-DERIVE. The generic
41
+ protocol is implemented by the `spoken-token` npm package.
42
+
43
+ - **[Simple Shared Secret Groups](GROUPS.md)** — defines the group lifecycle (creation,
44
+ member management, seed rotation, sync protocol, replay protection). CANARY groups
45
+ are an application of this generic group protocol with additional duress, liveness,
46
+ and beacon extensions.
47
+
48
+ The Nostr transport binding is defined in two layers:
49
+
50
+ - **[NIP-XX: Simple Shared Secret Groups](NIP-XX.md)** — maps the generic group
51
+ protocol onto existing Nostr kinds (30078, NIP-17, 20078). Zero new event kinds.
52
+
53
+ - **[NIP-CANARY](NIP-CANARY.md)** — application profile of NIP-XX adding
54
+ CANARY-specific signal types (duress alerts, beacons) and Meshtastic fallback.
55
+
56
+ CANARY's unique contributions beyond the generic layers are: **duress detection**
57
+ (CANARY-DURESS), **liveness monitoring** (dead man's switch), **threat-profile
58
+ presets**, and **encrypted location beacons**.
59
+
34
60
  ## Motivation
35
61
 
36
62
  AI voice cloning now requires as little as three seconds of audio. A thirty-second clip
@@ -96,6 +122,12 @@ separate keys or counters.
96
122
 
97
123
  Core deterministic token derivation. The universal primitive that all other layers build on.
98
124
 
125
+ > **Generic layer:** The core derivation algorithm (HMAC-SHA256 with context and counter)
126
+ > is specified generically in the [Spoken Token Protocol](https://github.com/TheCryptoDonkey/spoken-token/blob/main/PROTOCOL.md)
127
+ > as SPOKEN-DERIVE. CANARY-DERIVE is identical to SPOKEN-DERIVE — this section
128
+ > documents it in CANARY's context for completeness. The `spoken-token` npm package
129
+ > provides a standalone implementation of the generic layer.
130
+
99
131
  ### Algorithm
100
132
 
101
133
  ```
@@ -163,6 +195,12 @@ re-sync messages) MUST enforce the following rules:
163
195
 
164
196
  ## CANARY-SYNC: Transport-Agnostic Synchronisation
165
197
 
198
+ > **Generic layer:** The core group management protocol (creation, member management,
199
+ > seed rotation, counter sync, replay protection) is specified generically in
200
+ > [Simple Shared Secret Groups](GROUPS.md). CANARY-SYNC extends it with
201
+ > application-specific message types: `beacon`, `duress-alert`, `duress-clear`,
202
+ > and `liveness-checkin`.
203
+
166
204
  CANARY-SYNC is the protocol layer for propagating group state mutations and telemetry
167
205
  across any transport without depending on Nostr or any specific relay infrastructure.
168
206
  It operates over any channel capable of delivering authenticated, ordered or unordered
package/NIP-CANARY.md CHANGED
@@ -13,6 +13,32 @@ providing group management, seed distribution, and counter synchronisation over
13
13
  Nostr relays. The core protocol (CANARY-DERIVE, CANARY-DURESS, CANARY-WORDLIST)
14
14
  is defined in the transport-agnostic [CANARY specification](CANARY.md).
15
15
 
16
+ ## Protocol Layering
17
+
18
+ NIP-CANARY is an **application profile** of [NIP-XX: Simple Shared Secret Groups](NIP-XX.md).
19
+
20
+ The generic group transport (NIP-XX) defines how to manage shared-secret groups over
21
+ Nostr using existing event kinds:
22
+ - **Kind 30078** (NIP-78) for durable group state
23
+ - **NIP-17 gift wraps** for secret distribution
24
+ - **Kind 20078** (ephemeral) for real-time signals
25
+
26
+ NIP-CANARY extends NIP-XX with CANARY-specific features:
27
+ - **Duress signal semantics** in kind 20078 payloads (`duress-alert`, `duress-clear`)
28
+ - **Encrypted location beacons** in kind 20078 payloads
29
+ - **Liveness check-in signals** for dead man's switch
30
+ - **Meshtastic fallback transport** for offline/mesh operation
31
+
32
+ The six custom event kinds defined below (38800–20800) represent the **current
33
+ implementation**. A future version of this NIP will migrate to NIP-XX's transport
34
+ mapping (zero new kinds). The custom kinds are documented here for compatibility
35
+ with existing deployments.
36
+
37
+ > **Migration path:** New implementations SHOULD use NIP-XX transport (kind 30078,
38
+ > NIP-17, kind 20078) with the `ssg/` tag prefix and CANARY-specific signal types.
39
+ > Existing implementations using kinds 38800–20800 will continue to work — clients
40
+ > MAY support both during the transition period.
41
+
16
42
  ## Nostr Canary Groups
17
43
 
18
44
  This section defines a Nostr application layer built on the CANARY protocol, providing
package/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
  [![TypeScript](https://img.shields.io/badge/TypeScript-native-blue)](https://www.typescriptlang.org/)
9
9
  [![Coverage](https://img.shields.io/badge/coverage-95%25-brightgreen)](vitest.config.ts)
10
10
 
11
- **[Interactive Demo](https://thecryptodonkey.github.io/canary-kit/)** · [Protocol Spec](CANARY.md) · [Nostr Binding](NIP-CANARY.md) · [Integration Guide](INTEGRATION.md)
11
+ **[Interactive Demo](https://canary.trotters.cc/)** · [Protocol Spec](CANARY.md) · [Nostr Binding](NIP-CANARY.md) · [Integration Guide](INTEGRATION.md)
12
12
 
13
13
  ## The Problem
14
14
 
package/dist/counter.d.ts CHANGED
@@ -1,37 +1,2 @@
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;
1
+ export { getCounter, counterFromEventId, counterToBytes, DEFAULT_ROTATION_INTERVAL, MAX_COUNTER_OFFSET, } from 'spoken-token';
37
2
  //# sourceMappingURL=counter.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"counter.d.ts","sourceRoot":"","sources":["../src/counter.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EAAE,kBAAkB,EAAE,cAAc,EAC9C,yBAAyB,EAAE,kBAAkB,GAC9C,MAAM,cAAc,CAAA"}
package/dist/counter.js CHANGED
@@ -1,62 +1,2 @@
1
- import { sha256 } from './crypto.js';
2
- /** Default rotation interval: 7 days in seconds. */
3
- export const DEFAULT_ROTATION_INTERVAL = 604_800;
4
- /**
5
- * Maximum allowed usage offset above the current time-based counter.
6
- * Implementations MUST reject counter updates where effective counter > time-based counter + MAX_COUNTER_OFFSET.
7
- * See CANARY spec §Counter Acceptance.
8
- */
9
- export const MAX_COUNTER_OFFSET = 100;
10
- /**
11
- * Derive the current counter from a unix timestamp and rotation interval.
12
- * Counter = floor(timestamp / interval).
13
- *
14
- * @param timestampSec - Unix timestamp in seconds (non-negative finite number).
15
- * @param rotationIntervalSec - Rotation interval in seconds (positive finite number, default: 604800 = 7 days).
16
- * @returns Integer counter value within uint32 range.
17
- * @throws {RangeError} If timestampSec is negative/non-finite, rotationIntervalSec is non-positive/non-finite, or counter exceeds uint32.
18
- */
19
- export function getCounter(timestampSec, rotationIntervalSec = DEFAULT_ROTATION_INTERVAL) {
20
- if (!Number.isFinite(timestampSec) || timestampSec < 0) {
21
- throw new RangeError(`timestampSec must be a non-negative finite number, got ${timestampSec}`);
22
- }
23
- if (!Number.isFinite(rotationIntervalSec) || rotationIntervalSec <= 0) {
24
- throw new RangeError(`rotationIntervalSec must be a positive finite number, got ${rotationIntervalSec}`);
25
- }
26
- const result = Math.floor(timestampSec / rotationIntervalSec);
27
- if (result > 0xFFFFFFFF) {
28
- throw new RangeError(`Counter exceeds uint32 range (${result}). Use a larger rotation interval.`);
29
- }
30
- return result;
31
- }
32
- /**
33
- * Derive a counter from an event identifier (e.g. a task ID or Nostr event ID).
34
- * Uses SHA-256 truncated to 32 bits for a deterministic, uniformly distributed counter.
35
- * Per CANARY spec §Counter Schemes: event-based counters are deterministic from event ID.
36
- *
37
- * @param eventId - String identifier to derive the counter from (e.g. a Nostr event ID).
38
- * @returns Unsigned 32-bit integer derived from SHA-256 of the event ID.
39
- */
40
- export function counterFromEventId(eventId) {
41
- const hash = sha256(new TextEncoder().encode(eventId));
42
- // Read first 4 bytes as unsigned 32-bit big-endian integer
43
- return (hash[0] << 24 | hash[1] << 16 | hash[2] << 8 | hash[3]) >>> 0;
44
- }
45
- /**
46
- * Serialise a counter to an 8-byte big-endian Uint8Array.
47
- * Same encoding as TOTP (RFC 6238).
48
- *
49
- * @param counter - Non-negative safe integer to serialise.
50
- * @returns 8-byte big-endian Uint8Array representation of the counter.
51
- * @throws {RangeError} If counter is negative, not an integer, or exceeds Number.MAX_SAFE_INTEGER.
52
- */
53
- export function counterToBytes(counter) {
54
- if (!Number.isInteger(counter) || counter < 0 || counter > Number.MAX_SAFE_INTEGER) {
55
- throw new RangeError(`Counter must be a non-negative safe integer, got ${counter}`);
56
- }
57
- const buf = new Uint8Array(8);
58
- const view = new DataView(buf.buffer);
59
- view.setBigUint64(0, BigInt(counter), false); // false = big-endian
60
- return buf;
61
- }
1
+ export { getCounter, counterFromEventId, counterToBytes, DEFAULT_ROTATION_INTERVAL, MAX_COUNTER_OFFSET, } from 'spoken-token';
62
2
  //# sourceMappingURL=counter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"counter.js","sourceRoot":"","sources":["../src/counter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAEpC,oDAAoD;AACpD,MAAM,CAAC,MAAM,yBAAyB,GAAG,OAAO,CAAA;AAEhD;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAA;AAErC;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CACxB,YAAoB,EACpB,sBAA8B,yBAAyB;IAEvD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,YAAY,GAAG,CAAC,EAAE,CAAC;QACvD,MAAM,IAAI,UAAU,CAAC,0DAA0D,YAAY,EAAE,CAAC,CAAA;IAChG,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,mBAAmB,CAAC,IAAI,mBAAmB,IAAI,CAAC,EAAE,CAAC;QACtE,MAAM,IAAI,UAAU,CAAC,6DAA6D,mBAAmB,EAAE,CAAC,CAAA;IAC1G,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,GAAG,mBAAmB,CAAC,CAAA;IAC7D,IAAI,MAAM,GAAG,UAAU,EAAE,CAAC;QACxB,MAAM,IAAI,UAAU,CAAC,iCAAiC,MAAM,oCAAoC,CAAC,CAAA;IACnG,CAAC;IACD,OAAO,MAAM,CAAA;AACf,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,OAAe;IAChD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAA;IACtD,2DAA2D;IAC3D,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,GAAG,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;AACvE,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,OAAe;IAC5C,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,OAAO,GAAG,CAAC,IAAI,OAAO,GAAG,MAAM,CAAC,gBAAgB,EAAE,CAAC;QACnF,MAAM,IAAI,UAAU,CAAC,oDAAoD,OAAO,EAAE,CAAC,CAAA;IACrF,CAAC;IACD,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,CAAC,CAAC,CAAA;IAC7B,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;IACrC,IAAI,CAAC,YAAY,CAAC,CAAC,EAAE,MAAM,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,CAAA,CAAC,qBAAqB;IAClE,OAAO,GAAG,CAAA;AACZ,CAAC"}
1
+ {"version":3,"file":"counter.js","sourceRoot":"","sources":["../src/counter.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EAAE,kBAAkB,EAAE,cAAc,EAC9C,yBAAyB,EAAE,kBAAkB,GAC9C,MAAM,cAAc,CAAA"}
package/dist/crypto.d.ts CHANGED
@@ -1,111 +1,2 @@
1
- /**
2
- * Universal synchronous crypto primitives — Node.js and browser compatible.
3
- *
4
- * SHA-256: FIPS 180-4
5
- * HMAC: RFC 2104
6
- *
7
- * Uses only Uint8Array and the global `crypto` object (Web Crypto API).
8
- * No async, no Web Crypto subtle API, no Buffer.
9
- */
10
- /**
11
- * Compute SHA-256 of `data`.
12
- * Implements FIPS 180-4 sections 5 (padding), 6.2 (hash computation).
13
- *
14
- * @param data - Input bytes to hash.
15
- * @returns 32-byte SHA-256 digest.
16
- */
17
- export declare function sha256(data: Uint8Array): Uint8Array;
18
- /**
19
- * Compute HMAC-SHA256(key, data) and return the raw 32-byte digest.
20
- *
21
- * RFC 2104:
22
- * H(K XOR opad, H(K XOR ipad, data))
23
- * ipad = 0x36 repeated, opad = 0x5c repeated.
24
- * Keys longer than the block size are hashed first.
25
- * Keys shorter than the block size are zero-padded on the right.
26
- *
27
- * @param key - HMAC key bytes (hashed if longer than 64 bytes, zero-padded if shorter).
28
- * @param data - Input data bytes.
29
- * @returns 32-byte HMAC-SHA256 digest.
30
- */
31
- export declare function hmacSha256(key: Uint8Array, data: Uint8Array): Uint8Array;
32
- /**
33
- * Generate a cryptographically secure 32-byte seed as a 64-character hex string.
34
- * Uses the global `crypto.getRandomValues` (Web Crypto API).
35
- *
36
- * @returns 64-character lowercase hex string (32 random bytes).
37
- */
38
- export declare function randomSeed(): string;
39
- /**
40
- * Convert a hex string to a Uint8Array. Replaces `Buffer.from(hex, 'hex')`.
41
- *
42
- * @param hex - Even-length hex string (case-insensitive).
43
- * @returns Decoded byte array.
44
- * @throws {Error} If hex has odd length.
45
- * @throws {TypeError} If hex contains invalid characters.
46
- */
47
- export declare function hexToBytes(hex: string): Uint8Array;
48
- /**
49
- * Convert a Uint8Array to a lowercase hex string. Replaces `buffer.toString('hex')`.
50
- *
51
- * @param bytes - Input byte array.
52
- * @returns Lowercase hex string (2 characters per byte).
53
- */
54
- export declare function bytesToHex(bytes: Uint8Array): string;
55
- /**
56
- * Read an unsigned 16-bit big-endian integer from `bytes` at `offset`.
57
- * Replaces `buffer.readUInt16BE(offset)`.
58
- *
59
- * @param bytes - Source byte array.
60
- * @param offset - Byte offset to read from.
61
- * @returns Unsigned 16-bit integer value.
62
- * @throws {RangeError} If offset is out of bounds.
63
- */
64
- export declare function readUint16BE(bytes: Uint8Array, offset: number): number;
65
- /**
66
- * Concatenate multiple Uint8Arrays into one.
67
- * Replaces `Buffer.concat([...])`.
68
- *
69
- * @param arrays - One or more Uint8Arrays to concatenate.
70
- * @returns A single Uint8Array containing all input bytes in order.
71
- */
72
- export declare function concatBytes(...arrays: Uint8Array[]): Uint8Array;
73
- /**
74
- * Encode a Uint8Array as a base64 string. Available in Node 16+ and all browsers.
75
- *
76
- * @param bytes - Input byte array.
77
- * @returns Base64-encoded string.
78
- */
79
- export declare function bytesToBase64(bytes: Uint8Array): string;
80
- /**
81
- * Decode a base64 string to a Uint8Array.
82
- *
83
- * @param base64 - Base64-encoded string.
84
- * @returns Decoded byte array.
85
- */
86
- export declare function base64ToBytes(base64: string): Uint8Array;
87
- /**
88
- * Best-effort constant-time comparison of two byte arrays.
89
- * Pads both arrays to equal length to avoid leaking length via timing.
90
- *
91
- * **Caveat:** JavaScript runtimes do not guarantee constant-time execution —
92
- * JIT compilation and speculative execution may introduce timing variation.
93
- * This is a defence-in-depth measure, not a cryptographic guarantee. For
94
- * high-assurance environments, pair with rate limiting and consider
95
- * platform-native constant-time primitives.
96
- *
97
- * @param a - First byte array.
98
- * @param b - Second byte array.
99
- * @returns `true` if arrays are equal in length and content, `false` otherwise.
100
- */
101
- export declare function timingSafeEqual(a: Uint8Array, b: Uint8Array): boolean;
102
- /**
103
- * Best-effort constant-time comparison of two strings (UTF-8 encoded, then byte-compared).
104
- * See {@link timingSafeEqual} caveats.
105
- *
106
- * @param a - First string.
107
- * @param b - Second string.
108
- * @returns `true` if strings are equal, `false` otherwise.
109
- */
110
- export declare function timingSafeStringEqual(a: string, b: string): boolean;
1
+ export { sha256, hmacSha256, randomSeed, hexToBytes, bytesToHex, readUint16BE, concatBytes, bytesToBase64, base64ToBytes, timingSafeEqual, timingSafeStringEqual, } from 'spoken-token';
111
2
  //# sourceMappingURL=crypto.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAqCH;;;;;;GAMG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAiFnD;AAQD;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,UAAU,GAAG,UAAU,CA6BxE;AAMD;;;;;GAKG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAInC;AAMD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAWlD;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAMpD;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGtE;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,GAAG,MAAM,EAAE,UAAU,EAAE,GAAG,UAAU,CAS/D;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAIvD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CAKxD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,UAAU,GAAG,OAAO,CAYrE;AAID;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,OAAO,CAEnE"}
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AACA,OAAO,EACL,MAAM,EAAE,UAAU,EAAE,UAAU,EAC9B,UAAU,EAAE,UAAU,EAAE,YAAY,EAAE,WAAW,EACjD,aAAa,EAAE,aAAa,EAC5B,eAAe,EAAE,qBAAqB,GACvC,MAAM,cAAc,CAAA"}
package/dist/crypto.js CHANGED
@@ -1,309 +1,3 @@
1
- /**
2
- * Universal synchronous crypto primitives Node.js and browser compatible.
3
- *
4
- * SHA-256: FIPS 180-4
5
- * HMAC: RFC 2104
6
- *
7
- * Uses only Uint8Array and the global `crypto` object (Web Crypto API).
8
- * No async, no Web Crypto subtle API, no Buffer.
9
- */
10
- // ---------------------------------------------------------------------------
11
- // SHA-256 — FIPS 180-4
12
- // ---------------------------------------------------------------------------
13
- /** Initial hash values H0–H7 (first 32 bits of fractional parts of sqrt of first 8 primes). */
14
- const H0 = [
15
- 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
16
- 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
17
- ];
18
- /** Round constants K[0..63] (first 32 bits of fractional parts of cbrt of first 64 primes). */
19
- const K = new Uint32Array([
20
- 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
21
- 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
22
- 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
23
- 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
24
- 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
25
- 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
26
- 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
27
- 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
28
- 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
29
- 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
30
- 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
31
- 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
32
- 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
33
- 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
34
- 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
35
- 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
36
- ]);
37
- /** Rotate-right a 32-bit integer by n bits. */
38
- function rotr32(x, n) {
39
- return ((x >>> n) | (x << (32 - n))) >>> 0;
40
- }
41
- /**
42
- * Compute SHA-256 of `data`.
43
- * Implements FIPS 180-4 sections 5 (padding), 6.2 (hash computation).
44
- *
45
- * @param data - Input bytes to hash.
46
- * @returns 32-byte SHA-256 digest.
47
- */
48
- export function sha256(data) {
49
- // --- Pre-processing: padding (FIPS 180-4 §5.1.1) ---
50
- const bitLen = data.length * 8;
51
- // Append 0x80, then zero bytes, then 8-byte big-endian bit-length.
52
- // Total padded length must be a multiple of 64 bytes (512 bits).
53
- // After the message and 0x80 byte we need at least 8 bytes for the length,
54
- // so we pad to the next multiple of 64 that satisfies this.
55
- const padded = new Uint8Array(Math.ceil((data.length + 9) / 64) * 64);
56
- padded.set(data);
57
- padded[data.length] = 0x80;
58
- // Write 64-bit big-endian bit length at the end.
59
- // bitLen fits in a 53-bit JS number so we can split safely.
60
- const view = new DataView(padded.buffer);
61
- view.setUint32(padded.length - 8, Math.floor(bitLen / 0x100000000), false);
62
- view.setUint32(padded.length - 4, bitLen >>> 0, false);
63
- // --- Processing blocks (FIPS 180-4 §6.2.2) ---
64
- // Working variables
65
- let [h0, h1, h2, h3, h4, h5, h6, h7] = H0;
66
- const W = new Uint32Array(64);
67
- for (let offset = 0; offset < padded.length; offset += 64) {
68
- // Prepare message schedule W[0..63]
69
- for (let t = 0; t < 16; t++) {
70
- W[t] = view.getUint32(offset + t * 4, false);
71
- }
72
- for (let t = 16; t < 64; t++) {
73
- const w15 = W[t - 15];
74
- const w2 = W[t - 2];
75
- const s0 = rotr32(w15, 7) ^ rotr32(w15, 18) ^ (w15 >>> 3);
76
- const s1 = rotr32(w2, 17) ^ rotr32(w2, 19) ^ (w2 >>> 10);
77
- W[t] = (W[t - 16] + s0 + W[t - 7] + s1) >>> 0;
78
- }
79
- // Initialise working variables
80
- let a = h0, b = h1, c = h2, d = h3;
81
- let e = h4, f = h5, g = h6, hh = h7;
82
- // 64 rounds
83
- for (let t = 0; t < 64; t++) {
84
- const S1 = rotr32(e, 6) ^ rotr32(e, 11) ^ rotr32(e, 25);
85
- const ch = (e & f) ^ (~e & g);
86
- const tmp1 = (hh + S1 + ch + K[t] + W[t]) >>> 0;
87
- const S0 = rotr32(a, 2) ^ rotr32(a, 13) ^ rotr32(a, 22);
88
- const maj = (a & b) ^ (a & c) ^ (b & c);
89
- const tmp2 = (S0 + maj) >>> 0;
90
- hh = g;
91
- g = f;
92
- f = e;
93
- e = (d + tmp1) >>> 0;
94
- d = c;
95
- c = b;
96
- b = a;
97
- a = (tmp1 + tmp2) >>> 0;
98
- }
99
- // Add the compressed chunk to the current hash value
100
- h0 = (h0 + a) >>> 0;
101
- h1 = (h1 + b) >>> 0;
102
- h2 = (h2 + c) >>> 0;
103
- h3 = (h3 + d) >>> 0;
104
- h4 = (h4 + e) >>> 0;
105
- h5 = (h5 + f) >>> 0;
106
- h6 = (h6 + g) >>> 0;
107
- h7 = (h7 + hh) >>> 0;
108
- }
109
- // Produce the final hash value (big-endian)
110
- const digest = new Uint8Array(32);
111
- const dv = new DataView(digest.buffer);
112
- dv.setUint32(0, h0, false);
113
- dv.setUint32(4, h1, false);
114
- dv.setUint32(8, h2, false);
115
- dv.setUint32(12, h3, false);
116
- dv.setUint32(16, h4, false);
117
- dv.setUint32(20, h5, false);
118
- dv.setUint32(24, h6, false);
119
- dv.setUint32(28, h7, false);
120
- return digest;
121
- }
122
- // ---------------------------------------------------------------------------
123
- // HMAC-SHA256 — RFC 2104
124
- // ---------------------------------------------------------------------------
125
- const BLOCK_SIZE = 64; // SHA-256 block size in bytes
126
- /**
127
- * Compute HMAC-SHA256(key, data) and return the raw 32-byte digest.
128
- *
129
- * RFC 2104:
130
- * H(K XOR opad, H(K XOR ipad, data))
131
- * ipad = 0x36 repeated, opad = 0x5c repeated.
132
- * Keys longer than the block size are hashed first.
133
- * Keys shorter than the block size are zero-padded on the right.
134
- *
135
- * @param key - HMAC key bytes (hashed if longer than 64 bytes, zero-padded if shorter).
136
- * @param data - Input data bytes.
137
- * @returns 32-byte HMAC-SHA256 digest.
138
- */
139
- export function hmacSha256(key, data) {
140
- // If the key is longer than the block size, hash it first.
141
- const normalised = key.length > BLOCK_SIZE ? sha256(key) : key;
142
- // Pad/extend to block size.
143
- const k = new Uint8Array(BLOCK_SIZE);
144
- k.set(normalised);
145
- // Build inner and outer padded keys.
146
- const ipad = new Uint8Array(BLOCK_SIZE);
147
- const opad = new Uint8Array(BLOCK_SIZE);
148
- for (let i = 0; i < BLOCK_SIZE; i++) {
149
- ipad[i] = k[i] ^ 0x36;
150
- opad[i] = k[i] ^ 0x5c;
151
- }
152
- // inner = sha256(ipad || data)
153
- const inner = sha256(concatBytes(ipad, data));
154
- // outer = sha256(opad || inner)
155
- const result = sha256(concatBytes(opad, inner));
156
- // Best-effort zeroing of key material and intermediate state
157
- k.fill(0);
158
- ipad.fill(0);
159
- opad.fill(0);
160
- inner.fill(0);
161
- return result;
162
- }
163
- // ---------------------------------------------------------------------------
164
- // Random seed
165
- // ---------------------------------------------------------------------------
166
- /**
167
- * Generate a cryptographically secure 32-byte seed as a 64-character hex string.
168
- * Uses the global `crypto.getRandomValues` (Web Crypto API).
169
- *
170
- * @returns 64-character lowercase hex string (32 random bytes).
171
- */
172
- export function randomSeed() {
173
- const bytes = new Uint8Array(32);
174
- crypto.getRandomValues(bytes);
175
- return bytesToHex(bytes);
176
- }
177
- // ---------------------------------------------------------------------------
178
- // Byte / hex utilities
179
- // ---------------------------------------------------------------------------
180
- /**
181
- * Convert a hex string to a Uint8Array. Replaces `Buffer.from(hex, 'hex')`.
182
- *
183
- * @param hex - Even-length hex string (case-insensitive).
184
- * @returns Decoded byte array.
185
- * @throws {Error} If hex has odd length.
186
- * @throws {TypeError} If hex contains invalid characters.
187
- */
188
- export function hexToBytes(hex) {
189
- if (hex.length % 2 !== 0) {
190
- throw new Error(`hexToBytes: odd-length hex string (${hex.length} chars)`);
191
- }
192
- const bytes = new Uint8Array(hex.length / 2);
193
- for (let i = 0; i < bytes.length; i++) {
194
- const pair = hex.slice(i * 2, i * 2 + 2);
195
- if (!/^[0-9a-fA-F]{2}$/.test(pair))
196
- throw new TypeError(`Invalid hex character at position ${i * 2}`);
197
- bytes[i] = parseInt(pair, 16);
198
- }
199
- return bytes;
200
- }
201
- /**
202
- * Convert a Uint8Array to a lowercase hex string. Replaces `buffer.toString('hex')`.
203
- *
204
- * @param bytes - Input byte array.
205
- * @returns Lowercase hex string (2 characters per byte).
206
- */
207
- export function bytesToHex(bytes) {
208
- let hex = '';
209
- for (let i = 0; i < bytes.length; i++) {
210
- hex += bytes[i].toString(16).padStart(2, '0');
211
- }
212
- return hex;
213
- }
214
- /**
215
- * Read an unsigned 16-bit big-endian integer from `bytes` at `offset`.
216
- * Replaces `buffer.readUInt16BE(offset)`.
217
- *
218
- * @param bytes - Source byte array.
219
- * @param offset - Byte offset to read from.
220
- * @returns Unsigned 16-bit integer value.
221
- * @throws {RangeError} If offset is out of bounds.
222
- */
223
- export function readUint16BE(bytes, offset) {
224
- if (offset < 0 || offset + 1 >= bytes.length)
225
- throw new RangeError(`readUint16BE: offset ${offset} out of bounds for length ${bytes.length}`);
226
- return ((bytes[offset] << 8) | bytes[offset + 1]) >>> 0;
227
- }
228
- /**
229
- * Concatenate multiple Uint8Arrays into one.
230
- * Replaces `Buffer.concat([...])`.
231
- *
232
- * @param arrays - One or more Uint8Arrays to concatenate.
233
- * @returns A single Uint8Array containing all input bytes in order.
234
- */
235
- export function concatBytes(...arrays) {
236
- const total = arrays.reduce((n, a) => n + a.length, 0);
237
- const out = new Uint8Array(total);
238
- let offset = 0;
239
- for (const arr of arrays) {
240
- out.set(arr, offset);
241
- offset += arr.length;
242
- }
243
- return out;
244
- }
245
- /**
246
- * Encode a Uint8Array as a base64 string. Available in Node 16+ and all browsers.
247
- *
248
- * @param bytes - Input byte array.
249
- * @returns Base64-encoded string.
250
- */
251
- export function bytesToBase64(bytes) {
252
- let binary = '';
253
- for (let i = 0; i < bytes.length; i++)
254
- binary += String.fromCharCode(bytes[i]);
255
- return btoa(binary);
256
- }
257
- /**
258
- * Decode a base64 string to a Uint8Array.
259
- *
260
- * @param base64 - Base64-encoded string.
261
- * @returns Decoded byte array.
262
- */
263
- export function base64ToBytes(base64) {
264
- const binary = atob(base64);
265
- const bytes = new Uint8Array(binary.length);
266
- for (let i = 0; i < binary.length; i++)
267
- bytes[i] = binary.charCodeAt(i);
268
- return bytes;
269
- }
270
- /**
271
- * Best-effort constant-time comparison of two byte arrays.
272
- * Pads both arrays to equal length to avoid leaking length via timing.
273
- *
274
- * **Caveat:** JavaScript runtimes do not guarantee constant-time execution —
275
- * JIT compilation and speculative execution may introduce timing variation.
276
- * This is a defence-in-depth measure, not a cryptographic guarantee. For
277
- * high-assurance environments, pair with rate limiting and consider
278
- * platform-native constant-time primitives.
279
- *
280
- * @param a - First byte array.
281
- * @param b - Second byte array.
282
- * @returns `true` if arrays are equal in length and content, `false` otherwise.
283
- */
284
- export function timingSafeEqual(a, b) {
285
- const len = Math.max(a.length, b.length);
286
- // Pre-allocate zero-padded copies to eliminate branch in the comparison loop
287
- const paddedA = new Uint8Array(len);
288
- const paddedB = new Uint8Array(len);
289
- paddedA.set(a);
290
- paddedB.set(b);
291
- let diff = a.length ^ b.length; // non-zero if lengths differ
292
- for (let i = 0; i < len; i++) {
293
- diff |= paddedA[i] ^ paddedB[i];
294
- }
295
- return diff === 0;
296
- }
297
- const stringEncoder = new TextEncoder();
298
- /**
299
- * Best-effort constant-time comparison of two strings (UTF-8 encoded, then byte-compared).
300
- * See {@link timingSafeEqual} caveats.
301
- *
302
- * @param a - First string.
303
- * @param b - Second string.
304
- * @returns `true` if strings are equal, `false` otherwise.
305
- */
306
- export function timingSafeStringEqual(a, b) {
307
- return timingSafeEqual(stringEncoder.encode(a), stringEncoder.encode(b));
308
- }
1
+ // Re-export from spoken-token for backwards compatibility
2
+ export { sha256, hmacSha256, randomSeed, hexToBytes, bytesToHex, readUint16BE, concatBytes, bytesToBase64, base64ToBytes, timingSafeEqual, timingSafeStringEqual, } from 'spoken-token';
309
3
  //# sourceMappingURL=crypto.js.map