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 +38 -0
- package/NIP-CANARY.md +26 -0
- package/README.md +1 -1
- package/dist/counter.d.ts +1 -36
- package/dist/counter.d.ts.map +1 -1
- package/dist/counter.js +1 -61
- package/dist/counter.js.map +1 -1
- package/dist/crypto.d.ts +1 -110
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +2 -308
- package/dist/crypto.js.map +1 -1
- package/dist/encoding.d.ts +1 -55
- package/dist/encoding.d.ts.map +1 -1
- package/dist/encoding.js +1 -97
- package/dist/encoding.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/nostr.d.ts +104 -88
- package/dist/nostr.d.ts.map +1 -1
- package/dist/nostr.js +123 -111
- package/dist/nostr.js.map +1 -1
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +13 -2
- package/dist/sync.js.map +1 -1
- package/dist/token.d.ts +4 -71
- package/dist/token.d.ts.map +1 -1
- package/dist/token.js +17 -111
- package/dist/token.js.map +1 -1
- package/dist/wordlist.d.ts +1 -27
- package/dist/wordlist.d.ts.map +1 -1
- package/dist/wordlist.js +1 -296
- package/dist/wordlist.js.map +1 -1
- package/llms-full.txt +1 -1
- package/llms.txt +1 -1
- package/package.json +3 -2
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
|
[](https://www.typescriptlang.org/)
|
|
9
9
|
[](vitest.config.ts)
|
|
10
10
|
|
|
11
|
-
**[Interactive Demo](https://
|
|
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
|
-
|
|
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
|
package/dist/counter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"counter.d.ts","sourceRoot":"","sources":["../src/counter.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
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
|
package/dist/counter.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"counter.js","sourceRoot":"","sources":["../src/counter.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
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
|
package/dist/crypto.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
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
|