@agentdock/crypto 0.0.1
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/README.md +146 -0
- package/dist/aes.d.ts +32 -0
- package/dist/aes.d.ts.map +1 -0
- package/dist/aes.js +95 -0
- package/dist/aes.js.map +1 -0
- package/dist/auth.d.ts +55 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +69 -0
- package/dist/auth.js.map +1 -0
- package/dist/box.d.ts +50 -0
- package/dist/box.d.ts.map +1 -0
- package/dist/box.js +73 -0
- package/dist/box.js.map +1 -0
- package/dist/content.d.ts +30 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +40 -0
- package/dist/content.js.map +1 -0
- package/dist/encoding.d.ts +25 -0
- package/dist/encoding.d.ts.map +1 -0
- package/dist/encoding.js +55 -0
- package/dist/encoding.js.map +1 -0
- package/dist/hmac.d.ts +13 -0
- package/dist/hmac.d.ts.map +1 -0
- package/dist/hmac.js +25 -0
- package/dist/hmac.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/keys.d.ts +47 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +67 -0
- package/dist/keys.js.map +1 -0
- package/dist/random.d.ts +14 -0
- package/dist/random.d.ts.map +1 -0
- package/dist/random.js +20 -0
- package/dist/random.js.map +1 -0
- package/dist/secretbox.d.ts +27 -0
- package/dist/secretbox.d.ts.map +1 -0
- package/dist/secretbox.js +51 -0
- package/dist/secretbox.js.map +1 -0
- package/dist/session.d.ts +56 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +106 -0
- package/dist/session.js.map +1 -0
- package/package.json +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# @agentdock/crypto
|
|
2
|
+
|
|
3
|
+
> E2E 加密模块 — AES-256-GCM + Ed25519 + NaCl Box/SecretBox,浏览器与 Node.js 22+ 通用。
|
|
4
|
+
|
|
5
|
+
## 概述
|
|
6
|
+
|
|
7
|
+
crypto 包实现 AgentDock 的端到端加密。**所有敏感数据在离开用户设备前加密,服务端永远看不到明文**。
|
|
8
|
+
|
|
9
|
+
支持两种运行环境:
|
|
10
|
+
|
|
11
|
+
- **浏览器**:Web Crypto API(AES-GCM、HKDF、HMAC)
|
|
12
|
+
- **Node.js 22+**:同样走 Web Crypto API(`globalThis.crypto.subtle`)
|
|
13
|
+
- **tweetnacl**:NaCl Box / SecretBox(跨平台兼容)
|
|
14
|
+
|
|
15
|
+
## 在架构中的位置
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
wire → crypto → sdk → web
|
|
19
|
+
wire → crypto → daemon
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
依赖 wire 的类型定义,被 sdk/daemon/web 使用。
|
|
23
|
+
|
|
24
|
+
## 模块结构
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
src/
|
|
28
|
+
├── aes.ts # AES-256-GCM 加密/解密(Web Crypto API)
|
|
29
|
+
├── auth.ts # Ed25519 认证挑战(签名/验签)
|
|
30
|
+
├── box.ts # NaCl Box 非对称加密(密钥交付)
|
|
31
|
+
├── secretbox.ts # NaCl SecretBox 对称加密(legacy 兼容)
|
|
32
|
+
├── content.ts # Content keypair 派生(从密钥树推导 Box 密钥对)
|
|
33
|
+
├── keys.ts # 密钥派生树(HKDF-SHA512 分层派生)
|
|
34
|
+
├── hmac.ts # HMAC-SHA512(消息认证码)
|
|
35
|
+
├── encoding.ts # Base64 / Base64URL 编解码
|
|
36
|
+
├── random.ts # 安全随机数生成
|
|
37
|
+
└── index.ts # Barrel exports
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## API 参考
|
|
41
|
+
|
|
42
|
+
### AES-256-GCM (aes.ts)
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { encryptAesGcm, decryptAesGcm } from '@agentdock/crypto';
|
|
46
|
+
|
|
47
|
+
// 加密:返回 { c: base64(iv + ciphertext + tag), n: base64(nonce) }
|
|
48
|
+
const bundle = await encryptAesGcm(data: Uint8Array, key: Uint8Array);
|
|
49
|
+
|
|
50
|
+
// 解密:返回原始明文 Uint8Array
|
|
51
|
+
const plaintext = await decryptAesGcm(bundle, key);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Bundle 格式:`iv(12 bytes) + ciphertext + tag(16 bytes)`,整体 Base64 编码。
|
|
55
|
+
|
|
56
|
+
### Ed25519 认证 (auth.ts)
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
import { authChallenge, verifyChallenge } from '@agentdock/crypto';
|
|
60
|
+
|
|
61
|
+
// 从 seed 生成 Ed25519 密钥对 + 签名挑战
|
|
62
|
+
const result = await authChallenge(seed: Uint8Array);
|
|
63
|
+
// { publicKey, secretKey, challenge, signature }
|
|
64
|
+
|
|
65
|
+
// 验证签名
|
|
66
|
+
const valid = await verifyChallenge(challenge, signature, publicKey);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**注意**:直接使用 seed 作为 Ed25519 种子,不做额外 deriveKey(与 Happy 对齐,见 L23)。
|
|
70
|
+
|
|
71
|
+
### NaCl Box (box.ts)
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { encryptBox, decryptBox, boxPublicKeyFromSecretKey } from '@agentdock/crypto';
|
|
75
|
+
|
|
76
|
+
// 非对称加密(发送方用接收方公钥加密)
|
|
77
|
+
const bundle = encryptBox(data: Uint8Array, recipientPublicKey: Uint8Array);
|
|
78
|
+
|
|
79
|
+
// 解密(接收方用自己的私钥 + 发送方公钥解密)
|
|
80
|
+
const plaintext = decryptBox(bundle, secretKey: Uint8Array);
|
|
81
|
+
|
|
82
|
+
// 从 X25519 私钥推导公钥
|
|
83
|
+
const publicKey = boxPublicKeyFromSecretKey(secretKey);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### NaCl SecretBox (secretbox.ts)
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { encryptSecretBox, decryptSecretBox } from '@agentdock/crypto';
|
|
90
|
+
|
|
91
|
+
// 对称加密(legacy 兼容)
|
|
92
|
+
const bundle = encryptSecretBox(data: Uint8Array, secret: Uint8Array);
|
|
93
|
+
const plaintext = decryptSecretBox(bundle, secret);
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### 密钥派生树 (keys.ts)
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { deriveSecretKeyTreeRoot, deriveSecretKeyTreeChild, deriveKey } from '@agentdock/crypto';
|
|
100
|
+
|
|
101
|
+
// 从根密钥 + 标签派生子密钥
|
|
102
|
+
const root = await deriveSecretKeyTreeRoot(masterSecret, 'Happy EnCoder');
|
|
103
|
+
const child = await deriveSecretKeyTreeChild(root, 'content');
|
|
104
|
+
|
|
105
|
+
// 底层:HKDF-SHA512 派生
|
|
106
|
+
const key = await deriveKey(secret, label, segments);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Content Keypair (content.ts)
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
import { deriveContentKeyPair } from '@agentdock/crypto';
|
|
113
|
+
|
|
114
|
+
// 'Happy EnCoder' + ['content'] + SHA-512[0:32] → NaCl Box keypair
|
|
115
|
+
const { publicKey, secretKey } = await deriveContentKeyPair(secret);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### HMAC-SHA512 (hmac.ts)
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
import { hmacSha512 } from '@agentdock/crypto';
|
|
122
|
+
const mac = await hmacSha512(key, data);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### 编码 (encoding.ts)
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
import { encodeBase64, decodeBase64, encodeBase64Url, decodeBase64Url } from '@agentdock/crypto';
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## 开发
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# 运行测试(111 tests)
|
|
135
|
+
pnpm --filter @agentdock/crypto test
|
|
136
|
+
|
|
137
|
+
# 覆盖率(目标 95%+)
|
|
138
|
+
pnpm --filter @agentdock/crypto test:coverage
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## 设计决策
|
|
142
|
+
|
|
143
|
+
- **Web Crypto API 优先**:AES-GCM、HKDF、HMAC 全走 `crypto.subtle`,不引入额外依赖
|
|
144
|
+
- **tweetnacl 用于 NaCl**:Box/SecretBox 使用 tweetnacl,因为 Web Crypto 不支持 X25519 Box
|
|
145
|
+
- **密钥派生路径与 Happy 完全一致**:确保两端互操作(L23 教训)
|
|
146
|
+
- **TypeScript `as BufferSource` 断言**:TS 5.7 的 Uint8Array 类型不兼容 Web Crypto(L7 教训)
|
package/dist/aes.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM encryption and decryption using the Web Crypto API.
|
|
3
|
+
*
|
|
4
|
+
* Bundle format (compatible with Happy protocol):
|
|
5
|
+
* version (1 byte) | nonce (12 bytes) | ciphertext + authTag
|
|
6
|
+
*
|
|
7
|
+
* - version: always 0 for this implementation
|
|
8
|
+
* - nonce: 12-byte IV for AES-GCM
|
|
9
|
+
* - ciphertext + authTag: Web Crypto API returns these concatenated
|
|
10
|
+
* (authTag is the last 16 bytes)
|
|
11
|
+
*
|
|
12
|
+
* Data is JSON-serialized before encryption and JSON-parsed after decryption.
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Encrypt arbitrary JSON-serializable data with AES-256-GCM.
|
|
16
|
+
*
|
|
17
|
+
* @param data - Any JSON-serializable value.
|
|
18
|
+
* @param key - 32-byte (256-bit) encryption key.
|
|
19
|
+
* @returns Encrypted bundle as Uint8Array.
|
|
20
|
+
* @throws If the key is not exactly 32 bytes.
|
|
21
|
+
*/
|
|
22
|
+
export declare function encryptAesGcm(data: unknown, key: Uint8Array): Promise<Uint8Array>;
|
|
23
|
+
/**
|
|
24
|
+
* Decrypt an AES-256-GCM bundle back to the original data.
|
|
25
|
+
*
|
|
26
|
+
* @param bundle - Encrypted bundle produced by `encryptAesGcm`.
|
|
27
|
+
* @param key - 32-byte (256-bit) decryption key (must match encryption key).
|
|
28
|
+
* @returns The decrypted data, or `null` if decryption fails.
|
|
29
|
+
* @throws If the key is not exactly 32 bytes.
|
|
30
|
+
*/
|
|
31
|
+
export declare function decryptAesGcm(bundle: Uint8Array, key: Uint8Array): Promise<unknown | null>;
|
|
32
|
+
//# sourceMappingURL=aes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aes.d.ts","sourceRoot":"","sources":["../src/aes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAmBH;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,UAAU,CAAC,CAqBvF;AAED;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CA+BhG"}
|
package/dist/aes.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM encryption and decryption using the Web Crypto API.
|
|
3
|
+
*
|
|
4
|
+
* Bundle format (compatible with Happy protocol):
|
|
5
|
+
* version (1 byte) | nonce (12 bytes) | ciphertext + authTag
|
|
6
|
+
*
|
|
7
|
+
* - version: always 0 for this implementation
|
|
8
|
+
* - nonce: 12-byte IV for AES-GCM
|
|
9
|
+
* - ciphertext + authTag: Web Crypto API returns these concatenated
|
|
10
|
+
* (authTag is the last 16 bytes)
|
|
11
|
+
*
|
|
12
|
+
* Data is JSON-serialized before encryption and JSON-parsed after decryption.
|
|
13
|
+
*/
|
|
14
|
+
import { getRandomBytes } from './random.js';
|
|
15
|
+
/** Current bundle format version. */
|
|
16
|
+
const BUNDLE_VERSION = 0;
|
|
17
|
+
/** AES-GCM nonce size in bytes. */
|
|
18
|
+
const NONCE_SIZE = 12;
|
|
19
|
+
/** AES-GCM authentication tag size in bytes. */
|
|
20
|
+
const AUTH_TAG_SIZE = 16;
|
|
21
|
+
/** Minimum bundle size: version(1) + nonce(12) + authTag(16). */
|
|
22
|
+
const MIN_BUNDLE_SIZE = 1 + NONCE_SIZE + AUTH_TAG_SIZE;
|
|
23
|
+
/** Required AES-256 key size in bytes. */
|
|
24
|
+
const KEY_SIZE = 32;
|
|
25
|
+
/**
|
|
26
|
+
* Encrypt arbitrary JSON-serializable data with AES-256-GCM.
|
|
27
|
+
*
|
|
28
|
+
* @param data - Any JSON-serializable value.
|
|
29
|
+
* @param key - 32-byte (256-bit) encryption key.
|
|
30
|
+
* @returns Encrypted bundle as Uint8Array.
|
|
31
|
+
* @throws If the key is not exactly 32 bytes.
|
|
32
|
+
*/
|
|
33
|
+
export async function encryptAesGcm(data, key) {
|
|
34
|
+
assertKeySize(key);
|
|
35
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(data));
|
|
36
|
+
const nonce = getRandomBytes(NONCE_SIZE);
|
|
37
|
+
const cryptoKey = await importKey(key, ['encrypt']);
|
|
38
|
+
const encrypted = await globalThis.crypto.subtle.encrypt({ name: 'AES-GCM', iv: nonce }, cryptoKey, plaintext);
|
|
39
|
+
// Build bundle: version + nonce + (ciphertext + authTag)
|
|
40
|
+
const encryptedBytes = new Uint8Array(encrypted);
|
|
41
|
+
const bundle = new Uint8Array(1 + NONCE_SIZE + encryptedBytes.length);
|
|
42
|
+
bundle[0] = BUNDLE_VERSION;
|
|
43
|
+
bundle.set(nonce, 1);
|
|
44
|
+
bundle.set(encryptedBytes, 1 + NONCE_SIZE);
|
|
45
|
+
return bundle;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Decrypt an AES-256-GCM bundle back to the original data.
|
|
49
|
+
*
|
|
50
|
+
* @param bundle - Encrypted bundle produced by `encryptAesGcm`.
|
|
51
|
+
* @param key - 32-byte (256-bit) decryption key (must match encryption key).
|
|
52
|
+
* @returns The decrypted data, or `null` if decryption fails.
|
|
53
|
+
* @throws If the key is not exactly 32 bytes.
|
|
54
|
+
*/
|
|
55
|
+
export async function decryptAesGcm(bundle, key) {
|
|
56
|
+
assertKeySize(key);
|
|
57
|
+
// Validate minimum bundle size
|
|
58
|
+
if (bundle.length < MIN_BUNDLE_SIZE) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
// Validate version
|
|
62
|
+
if (bundle[0] !== BUNDLE_VERSION) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
// Extract nonce and ciphertext+authTag
|
|
66
|
+
const nonce = bundle.slice(1, 1 + NONCE_SIZE);
|
|
67
|
+
const ciphertextWithTag = bundle.slice(1 + NONCE_SIZE);
|
|
68
|
+
try {
|
|
69
|
+
const cryptoKey = await importKey(key, ['decrypt']);
|
|
70
|
+
const decrypted = await globalThis.crypto.subtle.decrypt({ name: 'AES-GCM', iv: nonce }, cryptoKey, ciphertextWithTag);
|
|
71
|
+
const json = new TextDecoder().decode(decrypted);
|
|
72
|
+
return JSON.parse(json);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// Decryption or JSON parsing failed -- return null per contract
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Import a raw key for AES-GCM operations.
|
|
81
|
+
*/
|
|
82
|
+
async function importKey(key, usages) {
|
|
83
|
+
return globalThis.crypto.subtle.importKey('raw', key, 'AES-GCM', false, [
|
|
84
|
+
...usages,
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Assert that a key is exactly 32 bytes (AES-256).
|
|
89
|
+
*/
|
|
90
|
+
function assertKeySize(key) {
|
|
91
|
+
if (key.length !== KEY_SIZE) {
|
|
92
|
+
throw new Error(`AES-256-GCM requires a 32-byte key, got ${key.length} bytes`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=aes.js.map
|
package/dist/aes.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aes.js","sourceRoot":"","sources":["../src/aes.ts"],"names":[],"mappings":"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"}
|
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ed25519 authentication challenge — aligned with Happy.
|
|
3
|
+
*
|
|
4
|
+
* Uses tweetnacl for Ed25519 signing/verification, which works in
|
|
5
|
+
* ALL contexts (including non-secure HTTP). The raw 32-byte seed is
|
|
6
|
+
* used directly as the Ed25519 private key seed, WITHOUT any extra
|
|
7
|
+
* key derivation step, matching Happy's tweetnacl.sign.keyPair.fromSeed().
|
|
8
|
+
*
|
|
9
|
+
* Previous implementation used Web Crypto API (crypto.subtle) which
|
|
10
|
+
* requires a secure context (HTTPS or localhost). This broke mobile
|
|
11
|
+
* PWA access over LAN (http://192.168.x.x).
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Result of an authentication challenge.
|
|
15
|
+
* All fields are immutable Uint8Array instances.
|
|
16
|
+
*/
|
|
17
|
+
export type AuthChallengeResult = {
|
|
18
|
+
readonly challenge: Uint8Array;
|
|
19
|
+
readonly publicKey: Uint8Array;
|
|
20
|
+
readonly signature: Uint8Array;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Generate an authentication challenge signed with an Ed25519 key
|
|
24
|
+
* derived from the given seed.
|
|
25
|
+
*
|
|
26
|
+
* The seed is used directly as the Ed25519 private key seed (no extra
|
|
27
|
+
* key derivation), matching Happy's tweetnacl.sign.keyPair.fromSeed().
|
|
28
|
+
*
|
|
29
|
+
* @param seed - 32-byte seed for deterministic Ed25519 key pair
|
|
30
|
+
* @returns Challenge result with random nonce, public key, and signature
|
|
31
|
+
*/
|
|
32
|
+
export declare function authChallenge(seed: Uint8Array): Promise<AuthChallengeResult>;
|
|
33
|
+
/**
|
|
34
|
+
* Sign an externally-provided challenge nonce with an Ed25519 key
|
|
35
|
+
* derived from the given seed. Used for server-driven auth flows
|
|
36
|
+
* where the server issues the nonce.
|
|
37
|
+
*
|
|
38
|
+
* @param nonce - Challenge nonce provided by the server
|
|
39
|
+
* @param seed - 32-byte seed for deterministic Ed25519 key pair
|
|
40
|
+
* @returns Public key and signature over the nonce
|
|
41
|
+
*/
|
|
42
|
+
export declare function signChallenge(nonce: Uint8Array, seed: Uint8Array): Promise<{
|
|
43
|
+
readonly publicKey: Uint8Array;
|
|
44
|
+
readonly signature: Uint8Array;
|
|
45
|
+
}>;
|
|
46
|
+
/**
|
|
47
|
+
* Verify an authentication challenge signature.
|
|
48
|
+
*
|
|
49
|
+
* @param challenge - The challenge nonce
|
|
50
|
+
* @param publicKey - The 32-byte Ed25519 public key
|
|
51
|
+
* @param signature - The 64-byte Ed25519 signature
|
|
52
|
+
* @returns true if the signature is valid
|
|
53
|
+
*/
|
|
54
|
+
export declare function verifyChallenge(challenge: Uint8Array, publicKey: Uint8Array, signature: Uint8Array): Promise<boolean>;
|
|
55
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;CAChC,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAclF;AAED;;;;;;;;GAQG;AACH,wBAAsB,aAAa,CACjC,KAAK,EAAE,UAAU,EACjB,IAAI,EAAE,UAAU,GACf,OAAO,CAAC;IAAE,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAA;CAAE,CAAC,CAQ7E;AAED;;;;;;;GAOG;AACH,wBAAsB,eAAe,CACnC,SAAS,EAAE,UAAU,EACrB,SAAS,EAAE,UAAU,EACrB,SAAS,EAAE,UAAU,GACpB,OAAO,CAAC,OAAO,CAAC,CAMlB"}
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ed25519 authentication challenge — aligned with Happy.
|
|
3
|
+
*
|
|
4
|
+
* Uses tweetnacl for Ed25519 signing/verification, which works in
|
|
5
|
+
* ALL contexts (including non-secure HTTP). The raw 32-byte seed is
|
|
6
|
+
* used directly as the Ed25519 private key seed, WITHOUT any extra
|
|
7
|
+
* key derivation step, matching Happy's tweetnacl.sign.keyPair.fromSeed().
|
|
8
|
+
*
|
|
9
|
+
* Previous implementation used Web Crypto API (crypto.subtle) which
|
|
10
|
+
* requires a secure context (HTTPS or localhost). This broke mobile
|
|
11
|
+
* PWA access over LAN (http://192.168.x.x).
|
|
12
|
+
*/
|
|
13
|
+
import nacl from 'tweetnacl';
|
|
14
|
+
/**
|
|
15
|
+
* Generate an authentication challenge signed with an Ed25519 key
|
|
16
|
+
* derived from the given seed.
|
|
17
|
+
*
|
|
18
|
+
* The seed is used directly as the Ed25519 private key seed (no extra
|
|
19
|
+
* key derivation), matching Happy's tweetnacl.sign.keyPair.fromSeed().
|
|
20
|
+
*
|
|
21
|
+
* @param seed - 32-byte seed for deterministic Ed25519 key pair
|
|
22
|
+
* @returns Challenge result with random nonce, public key, and signature
|
|
23
|
+
*/
|
|
24
|
+
export async function authChallenge(seed) {
|
|
25
|
+
const keyPair = nacl.sign.keyPair.fromSeed(seed);
|
|
26
|
+
// Generate random 32-byte challenge
|
|
27
|
+
const challenge = nacl.randomBytes(32);
|
|
28
|
+
// Sign the challenge
|
|
29
|
+
const signature = nacl.sign.detached(challenge, keyPair.secretKey);
|
|
30
|
+
return {
|
|
31
|
+
challenge,
|
|
32
|
+
publicKey: keyPair.publicKey,
|
|
33
|
+
signature,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Sign an externally-provided challenge nonce with an Ed25519 key
|
|
38
|
+
* derived from the given seed. Used for server-driven auth flows
|
|
39
|
+
* where the server issues the nonce.
|
|
40
|
+
*
|
|
41
|
+
* @param nonce - Challenge nonce provided by the server
|
|
42
|
+
* @param seed - 32-byte seed for deterministic Ed25519 key pair
|
|
43
|
+
* @returns Public key and signature over the nonce
|
|
44
|
+
*/
|
|
45
|
+
export async function signChallenge(nonce, seed) {
|
|
46
|
+
const keyPair = nacl.sign.keyPair.fromSeed(seed);
|
|
47
|
+
const signature = nacl.sign.detached(nonce, keyPair.secretKey);
|
|
48
|
+
return {
|
|
49
|
+
publicKey: keyPair.publicKey,
|
|
50
|
+
signature,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Verify an authentication challenge signature.
|
|
55
|
+
*
|
|
56
|
+
* @param challenge - The challenge nonce
|
|
57
|
+
* @param publicKey - The 32-byte Ed25519 public key
|
|
58
|
+
* @param signature - The 64-byte Ed25519 signature
|
|
59
|
+
* @returns true if the signature is valid
|
|
60
|
+
*/
|
|
61
|
+
export async function verifyChallenge(challenge, publicKey, signature) {
|
|
62
|
+
try {
|
|
63
|
+
return nacl.sign.detached.verify(challenge, signature, publicKey);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAY7B;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAgB;IAClD,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAEjD,oCAAoC;IACpC,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;IAEvC,qBAAqB;IACrB,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IAEnE,OAAO;QACL,SAAS;QACT,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,SAAS;KACV,CAAC;AACJ,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,KAAiB,EACjB,IAAgB;IAEhB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACjD,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;IAE/D,OAAO;QACL,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,SAAS;KACV,CAAC;AACJ,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,SAAqB,EACrB,SAAqB,EACrB,SAAqB;IAErB,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
|
package/dist/box.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NaCl Box encryption — aligned with Happy.
|
|
3
|
+
*
|
|
4
|
+
* Ephemeral X25519 Diffie-Hellman key agreement + authenticated encryption.
|
|
5
|
+
* Used for encrypting data for a specific recipient (key delivery).
|
|
6
|
+
*
|
|
7
|
+
* Bundle format: ephemeral_pubkey(32) + nonce(24) + ciphertext
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Encrypt data for a recipient's public key using NaCl Box.
|
|
11
|
+
*
|
|
12
|
+
* Generates an ephemeral keypair, performs DH key agreement,
|
|
13
|
+
* and encrypts with authenticated encryption (XSalsa20-Poly1305).
|
|
14
|
+
*
|
|
15
|
+
* @param data - Plaintext data to encrypt
|
|
16
|
+
* @param recipientPublicKey - 32-byte X25519 public key of the recipient
|
|
17
|
+
* @returns Bundle: ephemeral_pubkey(32) + nonce(24) + ciphertext
|
|
18
|
+
*/
|
|
19
|
+
export declare function encryptBox(data: Uint8Array, recipientPublicKey: Uint8Array): Uint8Array;
|
|
20
|
+
/**
|
|
21
|
+
* Decrypt a NaCl Box bundle using the recipient's secret key.
|
|
22
|
+
*
|
|
23
|
+
* @param bundle - Bundle: ephemeral_pubkey(32) + nonce(24) + ciphertext
|
|
24
|
+
* @param recipientSecretKey - 32-byte X25519 secret key
|
|
25
|
+
* @returns Decrypted plaintext, or null if decryption fails
|
|
26
|
+
*/
|
|
27
|
+
export declare function decryptBox(bundle: Uint8Array, recipientSecretKey: Uint8Array): Uint8Array | null;
|
|
28
|
+
/** X25519 keypair for NaCl Box operations. */
|
|
29
|
+
export interface BoxKeyPair {
|
|
30
|
+
readonly publicKey: Uint8Array;
|
|
31
|
+
readonly secretKey: Uint8Array;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Generate a random X25519 keypair for NaCl Box operations.
|
|
35
|
+
*
|
|
36
|
+
* @returns BoxKeyPair with 32-byte publicKey and secretKey
|
|
37
|
+
*/
|
|
38
|
+
export declare function generateBoxKeyPair(): BoxKeyPair;
|
|
39
|
+
/**
|
|
40
|
+
* Derive a NaCl Box public key from a secret key.
|
|
41
|
+
*
|
|
42
|
+
* NOTE: This matches libsodium's behavior — tweetnacl requires the
|
|
43
|
+
* secret key to already be the hashed form. For seed-based derivation,
|
|
44
|
+
* use deriveContentKeyPair() instead.
|
|
45
|
+
*
|
|
46
|
+
* @param secretKey - 32-byte X25519 secret key
|
|
47
|
+
* @returns 32-byte X25519 public key
|
|
48
|
+
*/
|
|
49
|
+
export declare function boxPublicKeyFromSecretKey(secretKey: Uint8Array): Uint8Array;
|
|
50
|
+
//# sourceMappingURL=box.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"box.d.ts","sourceRoot":"","sources":["../src/box.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAKH;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,kBAAkB,EAAE,UAAU,GAAG,UAAU,CAWvF;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,kBAAkB,EAAE,UAAU,GAAG,UAAU,GAAG,IAAI,CAShG;AAED,8CAA8C;AAC9C,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;CAChC;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,IAAI,UAAU,CAM/C;AAED;;;;;;;;;GASG;AACH,wBAAgB,yBAAyB,CAAC,SAAS,EAAE,UAAU,GAAG,UAAU,CAE3E"}
|
package/dist/box.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NaCl Box encryption — aligned with Happy.
|
|
3
|
+
*
|
|
4
|
+
* Ephemeral X25519 Diffie-Hellman key agreement + authenticated encryption.
|
|
5
|
+
* Used for encrypting data for a specific recipient (key delivery).
|
|
6
|
+
*
|
|
7
|
+
* Bundle format: ephemeral_pubkey(32) + nonce(24) + ciphertext
|
|
8
|
+
*/
|
|
9
|
+
import nacl from 'tweetnacl';
|
|
10
|
+
import { getRandomBytes } from './random.js';
|
|
11
|
+
/**
|
|
12
|
+
* Encrypt data for a recipient's public key using NaCl Box.
|
|
13
|
+
*
|
|
14
|
+
* Generates an ephemeral keypair, performs DH key agreement,
|
|
15
|
+
* and encrypts with authenticated encryption (XSalsa20-Poly1305).
|
|
16
|
+
*
|
|
17
|
+
* @param data - Plaintext data to encrypt
|
|
18
|
+
* @param recipientPublicKey - 32-byte X25519 public key of the recipient
|
|
19
|
+
* @returns Bundle: ephemeral_pubkey(32) + nonce(24) + ciphertext
|
|
20
|
+
*/
|
|
21
|
+
export function encryptBox(data, recipientPublicKey) {
|
|
22
|
+
const ephemeralKeyPair = nacl.box.keyPair();
|
|
23
|
+
const nonce = getRandomBytes(nacl.box.nonceLength);
|
|
24
|
+
const encrypted = nacl.box(data, nonce, recipientPublicKey, ephemeralKeyPair.secretKey);
|
|
25
|
+
// Bundle: ephemeral pubkey(32) + nonce(24) + ciphertext
|
|
26
|
+
const result = new Uint8Array(32 + 24 + encrypted.length);
|
|
27
|
+
result.set(ephemeralKeyPair.publicKey, 0);
|
|
28
|
+
result.set(nonce, 32);
|
|
29
|
+
result.set(encrypted, 56);
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Decrypt a NaCl Box bundle using the recipient's secret key.
|
|
34
|
+
*
|
|
35
|
+
* @param bundle - Bundle: ephemeral_pubkey(32) + nonce(24) + ciphertext
|
|
36
|
+
* @param recipientSecretKey - 32-byte X25519 secret key
|
|
37
|
+
* @returns Decrypted plaintext, or null if decryption fails
|
|
38
|
+
*/
|
|
39
|
+
export function decryptBox(bundle, recipientSecretKey) {
|
|
40
|
+
if (bundle.length < 32 + 24)
|
|
41
|
+
return null;
|
|
42
|
+
const ephemeralPublicKey = bundle.slice(0, 32);
|
|
43
|
+
const nonce = bundle.slice(32, 56);
|
|
44
|
+
const ciphertext = bundle.slice(56);
|
|
45
|
+
const decrypted = nacl.box.open(ciphertext, nonce, ephemeralPublicKey, recipientSecretKey);
|
|
46
|
+
return decrypted ? new Uint8Array(decrypted) : null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Generate a random X25519 keypair for NaCl Box operations.
|
|
50
|
+
*
|
|
51
|
+
* @returns BoxKeyPair with 32-byte publicKey and secretKey
|
|
52
|
+
*/
|
|
53
|
+
export function generateBoxKeyPair() {
|
|
54
|
+
const kp = nacl.box.keyPair();
|
|
55
|
+
return {
|
|
56
|
+
publicKey: new Uint8Array(kp.publicKey),
|
|
57
|
+
secretKey: new Uint8Array(kp.secretKey),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Derive a NaCl Box public key from a secret key.
|
|
62
|
+
*
|
|
63
|
+
* NOTE: This matches libsodium's behavior — tweetnacl requires the
|
|
64
|
+
* secret key to already be the hashed form. For seed-based derivation,
|
|
65
|
+
* use deriveContentKeyPair() instead.
|
|
66
|
+
*
|
|
67
|
+
* @param secretKey - 32-byte X25519 secret key
|
|
68
|
+
* @returns 32-byte X25519 public key
|
|
69
|
+
*/
|
|
70
|
+
export function boxPublicKeyFromSecretKey(secretKey) {
|
|
71
|
+
return new Uint8Array(nacl.box.keyPair.fromSecretKey(secretKey).publicKey);
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=box.js.map
|
package/dist/box.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"box.js","sourceRoot":"","sources":["../src/box.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7C;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU,CAAC,IAAgB,EAAE,kBAA8B;IACzE,MAAM,gBAAgB,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;IAC5C,MAAM,KAAK,GAAG,cAAc,CAAC,IAAI,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;IACnD,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,kBAAkB,EAAE,gBAAgB,CAAC,SAAS,CAAC,CAAC;IAExF,wDAAwD;IACxD,MAAM,MAAM,GAAG,IAAI,UAAU,CAAC,EAAE,GAAG,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IAC1D,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC;IAC1C,MAAM,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACtB,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IAC1B,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,MAAkB,EAAE,kBAA8B;IAC3E,IAAI,MAAM,CAAC,MAAM,GAAG,EAAE,GAAG,EAAE;QAAE,OAAO,IAAI,CAAC;IAEzC,MAAM,kBAAkB,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;IACnC,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAEpC,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,EAAE,kBAAkB,EAAE,kBAAkB,CAAC,CAAC;IAC3F,OAAO,SAAS,CAAC,CAAC,CAAC,IAAI,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;AACtD,CAAC;AAQD;;;;GAIG;AACH,MAAM,UAAU,kBAAkB;IAChC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;IAC9B,OAAO;QACL,SAAS,EAAE,IAAI,UAAU,CAAC,EAAE,CAAC,SAAS,CAAC;QACvC,SAAS,EAAE,IAAI,UAAU,CAAC,EAAE,CAAC,SAAS,CAAC;KACxC,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,yBAAyB,CAAC,SAAqB;IAC7D,OAAO,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC,SAAS,CAAC,CAAC;AAC7E,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content keypair derivation — aligned with Happy.
|
|
3
|
+
*
|
|
4
|
+
* Derives an X25519 NaCl Box keypair from a secret using the key tree.
|
|
5
|
+
* Usage string: 'Happy EnCoder', path: ['content'].
|
|
6
|
+
*
|
|
7
|
+
* The derived seed is SHA-512 hashed and the first 32 bytes become the
|
|
8
|
+
* Box secret key, matching libsodium's crypto_box_seed_keypair behavior.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Derive a NaCl Box keypair for content encryption.
|
|
12
|
+
*
|
|
13
|
+
* Follows Happy's deriveContentKeyPair():
|
|
14
|
+
* 1. deriveKey(secret, 'Happy EnCoder', ['content']) → 32-byte seed
|
|
15
|
+
* 2. SHA-512(seed)[0:32] → box secret key (matching libsodium)
|
|
16
|
+
* 3. tweetnacl.box.keyPair.fromSecretKey(boxSecretKey) → keypair
|
|
17
|
+
*
|
|
18
|
+
* Since we use Web Crypto (no node:crypto), we approximate SHA-512
|
|
19
|
+
* using HMAC-SHA512 with the seed as both key and data, then take
|
|
20
|
+
* the first 32 bytes. This produces a deterministic 32-byte output
|
|
21
|
+
* for a given seed, matching the intent of SHA-512(seed)[0:32].
|
|
22
|
+
*
|
|
23
|
+
* @param secret - Master secret for key derivation
|
|
24
|
+
* @returns NaCl Box keypair { publicKey, secretKey }
|
|
25
|
+
*/
|
|
26
|
+
export declare function deriveContentKeyPair(secret: Uint8Array): Promise<{
|
|
27
|
+
readonly publicKey: Uint8Array;
|
|
28
|
+
readonly secretKey: Uint8Array;
|
|
29
|
+
}>;
|
|
30
|
+
//# sourceMappingURL=content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,oBAAoB,CACxC,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC;IAAE,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,SAAS,EAAE,UAAU,CAAA;CAAE,CAAC,CAa7E"}
|
package/dist/content.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content keypair derivation — aligned with Happy.
|
|
3
|
+
*
|
|
4
|
+
* Derives an X25519 NaCl Box keypair from a secret using the key tree.
|
|
5
|
+
* Usage string: 'Happy EnCoder', path: ['content'].
|
|
6
|
+
*
|
|
7
|
+
* The derived seed is SHA-512 hashed and the first 32 bytes become the
|
|
8
|
+
* Box secret key, matching libsodium's crypto_box_seed_keypair behavior.
|
|
9
|
+
*/
|
|
10
|
+
import nacl from 'tweetnacl';
|
|
11
|
+
import { deriveKey } from './keys.js';
|
|
12
|
+
/**
|
|
13
|
+
* Derive a NaCl Box keypair for content encryption.
|
|
14
|
+
*
|
|
15
|
+
* Follows Happy's deriveContentKeyPair():
|
|
16
|
+
* 1. deriveKey(secret, 'Happy EnCoder', ['content']) → 32-byte seed
|
|
17
|
+
* 2. SHA-512(seed)[0:32] → box secret key (matching libsodium)
|
|
18
|
+
* 3. tweetnacl.box.keyPair.fromSecretKey(boxSecretKey) → keypair
|
|
19
|
+
*
|
|
20
|
+
* Since we use Web Crypto (no node:crypto), we approximate SHA-512
|
|
21
|
+
* using HMAC-SHA512 with the seed as both key and data, then take
|
|
22
|
+
* the first 32 bytes. This produces a deterministic 32-byte output
|
|
23
|
+
* for a given seed, matching the intent of SHA-512(seed)[0:32].
|
|
24
|
+
*
|
|
25
|
+
* @param secret - Master secret for key derivation
|
|
26
|
+
* @returns NaCl Box keypair { publicKey, secretKey }
|
|
27
|
+
*/
|
|
28
|
+
export async function deriveContentKeyPair(secret) {
|
|
29
|
+
const seed = await deriveKey(secret, 'Happy EnCoder', ['content']);
|
|
30
|
+
// Match libsodium: crypto_box_seed_keypair does SHA-512(seed)[0:32]
|
|
31
|
+
// We use Web Crypto SHA-512 directly
|
|
32
|
+
const hashBuffer = await crypto.subtle.digest('SHA-512', seed);
|
|
33
|
+
const boxSecretKey = new Uint8Array(hashBuffer).slice(0, 32);
|
|
34
|
+
const keyPair = nacl.box.keyPair.fromSecretKey(boxSecretKey);
|
|
35
|
+
return {
|
|
36
|
+
publicKey: new Uint8Array(keyPair.publicKey),
|
|
37
|
+
secretKey: new Uint8Array(keyPair.secretKey),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=content.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content.js","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAEtC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,MAAkB;IAElB,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IAEnE,oEAAoE;IACpE,qCAAqC;IACrC,MAAM,UAAU,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,EAAE,IAAoB,CAAC,CAAC;IAC/E,MAAM,YAAY,GAAG,IAAI,UAAU,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAE7D,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC;IAC7D,OAAO;QACL,SAAS,EAAE,IAAI,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC;QAC5C,SAAS,EAAE,IAAI,UAAU,CAAC,OAAO,CAAC,SAAS,CAAC;KAC7C,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base64 and Base64URL encoding/decoding utilities.
|
|
3
|
+
*
|
|
4
|
+
* Uses browser-compatible APIs (no Node.js Buffer dependency).
|
|
5
|
+
* Works in both browser and Node.js 22+ environments.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Encode a Uint8Array to a standard Base64 string.
|
|
9
|
+
*/
|
|
10
|
+
export declare function encodeBase64(buffer: Uint8Array): string;
|
|
11
|
+
/**
|
|
12
|
+
* Decode a standard Base64 string to a Uint8Array.
|
|
13
|
+
*/
|
|
14
|
+
export declare function decodeBase64(base64: string): Uint8Array;
|
|
15
|
+
/**
|
|
16
|
+
* Encode a Uint8Array to a Base64URL string (no padding).
|
|
17
|
+
*
|
|
18
|
+
* Base64URL replaces `+` with `-`, `/` with `_`, and strips `=` padding.
|
|
19
|
+
*/
|
|
20
|
+
export declare function encodeBase64Url(buffer: Uint8Array): string;
|
|
21
|
+
/**
|
|
22
|
+
* Decode a Base64URL string to a Uint8Array.
|
|
23
|
+
*/
|
|
24
|
+
export declare function decodeBase64Url(base64url: string): Uint8Array;
|
|
25
|
+
//# sourceMappingURL=encoding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"encoding.d.ts","sourceRoot":"","sources":["../src/encoding.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CASvD;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,CASvD;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CAG1D;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,CAU7D"}
|