@debros/orama 0.122.4-nightly
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/LICENSE +21 -0
- package/README.md +665 -0
- package/dist/index.d.ts +1334 -0
- package/dist/index.js +2553 -0
- package/dist/index.js.map +1 -0
- package/package.json +82 -0
- package/src/auth/client.ts +276 -0
- package/src/auth/index.ts +3 -0
- package/src/auth/types.ts +62 -0
- package/src/cache/client.ts +203 -0
- package/src/cache/index.ts +14 -0
- package/src/core/http.ts +541 -0
- package/src/core/index.ts +10 -0
- package/src/core/interfaces/IAuthStrategy.ts +28 -0
- package/src/core/interfaces/IHttpTransport.ts +73 -0
- package/src/core/interfaces/IRetryPolicy.ts +20 -0
- package/src/core/interfaces/IWebSocketClient.ts +60 -0
- package/src/core/interfaces/index.ts +4 -0
- package/src/core/transport/AuthHeaderStrategy.ts +108 -0
- package/src/core/transport/RequestLogger.ts +116 -0
- package/src/core/transport/RequestRetryPolicy.ts +53 -0
- package/src/core/transport/TLSConfiguration.ts +53 -0
- package/src/core/transport/index.ts +4 -0
- package/src/core/ws.ts +246 -0
- package/src/db/client.ts +126 -0
- package/src/db/index.ts +13 -0
- package/src/db/qb.ts +111 -0
- package/src/db/repository.ts +128 -0
- package/src/db/types.ts +67 -0
- package/src/errors.ts +38 -0
- package/src/functions/client.ts +62 -0
- package/src/functions/index.ts +2 -0
- package/src/functions/types.ts +21 -0
- package/src/index.ts +201 -0
- package/src/network/client.ts +119 -0
- package/src/network/index.ts +7 -0
- package/src/pubsub/client.ts +361 -0
- package/src/pubsub/index.ts +12 -0
- package/src/pubsub/types.ts +46 -0
- package/src/storage/client.ts +272 -0
- package/src/storage/index.ts +7 -0
- package/src/utils/codec.ts +68 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/platform.ts +44 -0
- package/src/utils/retry.ts +58 -0
- package/src/vault/auth.ts +98 -0
- package/src/vault/client.ts +197 -0
- package/src/vault/crypto/aes.ts +271 -0
- package/src/vault/crypto/hkdf.ts +42 -0
- package/src/vault/crypto/index.ts +27 -0
- package/src/vault/crypto/shamir.ts +173 -0
- package/src/vault/index.ts +65 -0
- package/src/vault/quorum.ts +16 -0
- package/src/vault/transport/fanout.ts +94 -0
- package/src/vault/transport/guardian.ts +285 -0
- package/src/vault/transport/index.ts +19 -0
- package/src/vault/transport/types.ts +101 -0
- package/src/vault/types.ts +62 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { AuthClient } from './auth';
|
|
2
|
+
import type { GuardianClient } from './transport/guardian';
|
|
3
|
+
import { withTimeout, withRetry } from './transport/fanout';
|
|
4
|
+
import { split, combine } from './crypto/shamir';
|
|
5
|
+
import type { Share } from './crypto/shamir';
|
|
6
|
+
import { adaptiveThreshold, writeQuorum } from './quorum';
|
|
7
|
+
import type {
|
|
8
|
+
VaultConfig,
|
|
9
|
+
StoreResult,
|
|
10
|
+
RetrieveResult,
|
|
11
|
+
ListResult,
|
|
12
|
+
DeleteResult,
|
|
13
|
+
GuardianResult,
|
|
14
|
+
} from './types';
|
|
15
|
+
|
|
16
|
+
const PULL_TIMEOUT_MS = 10_000;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* High-level client for the orama-vault distributed secrets store.
|
|
20
|
+
*
|
|
21
|
+
* Handles:
|
|
22
|
+
* - Authentication with guardian nodes
|
|
23
|
+
* - Shamir split/combine for data distribution
|
|
24
|
+
* - Quorum-based writes and reads
|
|
25
|
+
* - V2 CRUD operations (store, retrieve, list, delete)
|
|
26
|
+
*/
|
|
27
|
+
export class VaultClient {
|
|
28
|
+
private config: VaultConfig;
|
|
29
|
+
private auth: AuthClient;
|
|
30
|
+
|
|
31
|
+
constructor(config: VaultConfig) {
|
|
32
|
+
this.config = config;
|
|
33
|
+
this.auth = new AuthClient(config.identityHex, config.timeoutMs);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Store a secret across guardian nodes using Shamir splitting.
|
|
38
|
+
*
|
|
39
|
+
* @param name - Secret name (alphanumeric, _, -, max 128 chars)
|
|
40
|
+
* @param data - Secret data to store
|
|
41
|
+
* @param version - Monotonic version number (must be > previous)
|
|
42
|
+
*/
|
|
43
|
+
async store(name: string, data: Uint8Array, version: number): Promise<StoreResult> {
|
|
44
|
+
const guardians = this.config.guardians;
|
|
45
|
+
const n = guardians.length;
|
|
46
|
+
const k = adaptiveThreshold(n);
|
|
47
|
+
|
|
48
|
+
// Shamir split the data
|
|
49
|
+
const shares = split(data, n, k);
|
|
50
|
+
|
|
51
|
+
// Authenticate and push to all guardians
|
|
52
|
+
const authed = await this.auth.authenticateAll(guardians);
|
|
53
|
+
|
|
54
|
+
const results = await Promise.allSettled(
|
|
55
|
+
authed.map(async ({ client, endpoint }, _i) => {
|
|
56
|
+
// Find the share for this guardian's index
|
|
57
|
+
const guardianIdx = guardians.indexOf(endpoint);
|
|
58
|
+
const share = shares[guardianIdx];
|
|
59
|
+
if (!share) throw new Error('share index out of bounds');
|
|
60
|
+
|
|
61
|
+
// Encode share as [x:1byte][y:rest]
|
|
62
|
+
const shareBytes = new Uint8Array(1 + share.y.length);
|
|
63
|
+
shareBytes[0] = share.x;
|
|
64
|
+
shareBytes.set(share.y, 1);
|
|
65
|
+
|
|
66
|
+
return withRetry(() => client.putSecret(name, shareBytes, version));
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Wipe shares
|
|
71
|
+
for (const share of shares) {
|
|
72
|
+
share.y.fill(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const guardianResults: GuardianResult[] = authed.map(({ endpoint }, i) => {
|
|
76
|
+
const ep = `${endpoint.address}:${endpoint.port}`;
|
|
77
|
+
const r = results[i]!;
|
|
78
|
+
if (r.status === 'fulfilled') {
|
|
79
|
+
return { endpoint: ep, success: true };
|
|
80
|
+
}
|
|
81
|
+
return { endpoint: ep, success: false, error: (r.reason as Error).message };
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const ackCount = results.filter((r) => r.status === 'fulfilled').length;
|
|
85
|
+
const failCount = results.filter((r) => r.status === 'rejected').length;
|
|
86
|
+
const w = writeQuorum(n);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
ackCount,
|
|
90
|
+
totalContacted: authed.length,
|
|
91
|
+
failCount,
|
|
92
|
+
quorumMet: ackCount >= w,
|
|
93
|
+
guardianResults,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Retrieve and reconstruct a secret from guardian nodes.
|
|
99
|
+
*
|
|
100
|
+
* @param name - Secret name
|
|
101
|
+
*/
|
|
102
|
+
async retrieve(name: string): Promise<RetrieveResult> {
|
|
103
|
+
const guardians = this.config.guardians;
|
|
104
|
+
const n = guardians.length;
|
|
105
|
+
const k = adaptiveThreshold(n);
|
|
106
|
+
|
|
107
|
+
// Authenticate and pull from all guardians
|
|
108
|
+
const authed = await this.auth.authenticateAll(guardians);
|
|
109
|
+
|
|
110
|
+
const pullResults = await Promise.allSettled(
|
|
111
|
+
authed.map(async ({ client }) => {
|
|
112
|
+
const resp = await withTimeout(client.getSecret(name), PULL_TIMEOUT_MS);
|
|
113
|
+
const shareBytes = resp.share;
|
|
114
|
+
if (shareBytes.length < 2) throw new Error('Share too short');
|
|
115
|
+
return {
|
|
116
|
+
x: shareBytes[0]!,
|
|
117
|
+
y: shareBytes.slice(1),
|
|
118
|
+
} as Share;
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const shares: Share[] = [];
|
|
123
|
+
for (const r of pullResults) {
|
|
124
|
+
if (r.status === 'fulfilled') {
|
|
125
|
+
shares.push(r.value);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (shares.length < k) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`Not enough shares: collected ${shares.length} of ${k} required (contacted ${authed.length} guardians)`,
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Reconstruct
|
|
136
|
+
const data = combine(shares);
|
|
137
|
+
|
|
138
|
+
// Wipe collected shares
|
|
139
|
+
for (const share of shares) {
|
|
140
|
+
share.y.fill(0);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
data,
|
|
145
|
+
sharesCollected: shares.length,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* List all secrets for this identity.
|
|
151
|
+
* Queries the first reachable guardian (metadata is replicated).
|
|
152
|
+
*/
|
|
153
|
+
async list(): Promise<ListResult> {
|
|
154
|
+
const guardians = this.config.guardians;
|
|
155
|
+
const authed = await this.auth.authenticateAll(guardians);
|
|
156
|
+
|
|
157
|
+
if (authed.length === 0) {
|
|
158
|
+
throw new Error('No guardians reachable');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Query first authenticated guardian
|
|
162
|
+
const resp = await authed[0]!.client.listSecrets();
|
|
163
|
+
return { secrets: resp.secrets };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Delete a secret from all guardian nodes.
|
|
168
|
+
*
|
|
169
|
+
* @param name - Secret name to delete
|
|
170
|
+
*/
|
|
171
|
+
async delete(name: string): Promise<DeleteResult> {
|
|
172
|
+
const guardians = this.config.guardians;
|
|
173
|
+
const n = guardians.length;
|
|
174
|
+
|
|
175
|
+
const authed = await this.auth.authenticateAll(guardians);
|
|
176
|
+
|
|
177
|
+
const results = await Promise.allSettled(
|
|
178
|
+
authed.map(async ({ client }) => {
|
|
179
|
+
return withRetry(() => client.deleteSecret(name));
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const ackCount = results.filter((r) => r.status === 'fulfilled').length;
|
|
184
|
+
const w = writeQuorum(n);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
ackCount,
|
|
188
|
+
totalContacted: authed.length,
|
|
189
|
+
quorumMet: ackCount >= w,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Clear all cached auth sessions. */
|
|
194
|
+
clearSessions(): void {
|
|
195
|
+
this.auth.clearSessions();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM Encryption
|
|
3
|
+
*
|
|
4
|
+
* Implements authenticated encryption using AES-256 in Galois/Counter Mode.
|
|
5
|
+
* Uses @noble/ciphers for platform-agnostic, audited cryptographic operations.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Authenticated encryption (confidentiality + integrity)
|
|
9
|
+
* - 256-bit keys for strong security
|
|
10
|
+
* - 96-bit nonces (randomly generated)
|
|
11
|
+
* - 128-bit authentication tags
|
|
12
|
+
*
|
|
13
|
+
* Security considerations:
|
|
14
|
+
* - Never reuse a nonce with the same key
|
|
15
|
+
* - Nonces are randomly generated and prepended to ciphertext
|
|
16
|
+
* - Authentication tags are verified before decryption
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { gcm } from '@noble/ciphers/aes';
|
|
20
|
+
import { randomBytes } from '@noble/ciphers/webcrypto';
|
|
21
|
+
import { bytesToHex, hexToBytes, concatBytes } from '@noble/hashes/utils';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Size constants
|
|
25
|
+
*/
|
|
26
|
+
export const KEY_SIZE = 32; // 256 bits
|
|
27
|
+
export const NONCE_SIZE = 12; // 96 bits (recommended for GCM)
|
|
28
|
+
export const TAG_SIZE = 16; // 128 bits
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Encrypted data structure
|
|
32
|
+
*/
|
|
33
|
+
export interface EncryptedData {
|
|
34
|
+
/** Ciphertext including authentication tag */
|
|
35
|
+
ciphertext: Uint8Array;
|
|
36
|
+
/** Nonce used for encryption */
|
|
37
|
+
nonce: Uint8Array;
|
|
38
|
+
/** Additional authenticated data (optional) */
|
|
39
|
+
aad?: Uint8Array;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Serialized encrypted data (nonce prepended to ciphertext)
|
|
44
|
+
*/
|
|
45
|
+
export interface SerializedEncryptedData {
|
|
46
|
+
/** Combined nonce + ciphertext + tag */
|
|
47
|
+
data: Uint8Array;
|
|
48
|
+
/** Additional authenticated data (optional) */
|
|
49
|
+
aad?: Uint8Array;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Encrypts data using AES-256-GCM
|
|
54
|
+
*/
|
|
55
|
+
export function encrypt(
|
|
56
|
+
plaintext: Uint8Array,
|
|
57
|
+
key: Uint8Array,
|
|
58
|
+
aad?: Uint8Array
|
|
59
|
+
): EncryptedData {
|
|
60
|
+
validateKey(key);
|
|
61
|
+
|
|
62
|
+
const nonce = randomBytes(NONCE_SIZE);
|
|
63
|
+
const cipher = gcm(key, nonce, aad);
|
|
64
|
+
const ciphertext = cipher.encrypt(plaintext);
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
ciphertext,
|
|
68
|
+
nonce,
|
|
69
|
+
aad,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Decrypts data using AES-256-GCM
|
|
75
|
+
*/
|
|
76
|
+
export function decrypt(encryptedData: EncryptedData, key: Uint8Array): Uint8Array {
|
|
77
|
+
validateKey(key);
|
|
78
|
+
validateNonce(encryptedData.nonce);
|
|
79
|
+
|
|
80
|
+
const cipher = gcm(key, encryptedData.nonce, encryptedData.aad);
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
return cipher.decrypt(encryptedData.ciphertext);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new Error('Decryption failed: invalid ciphertext or authentication tag');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Encrypts a string message
|
|
91
|
+
*/
|
|
92
|
+
export function encryptString(
|
|
93
|
+
message: string,
|
|
94
|
+
key: Uint8Array,
|
|
95
|
+
aad?: Uint8Array
|
|
96
|
+
): EncryptedData {
|
|
97
|
+
const plaintext = new TextEncoder().encode(message);
|
|
98
|
+
try {
|
|
99
|
+
return encrypt(plaintext, key, aad);
|
|
100
|
+
} finally {
|
|
101
|
+
plaintext.fill(0);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Decrypts to a string message
|
|
107
|
+
*/
|
|
108
|
+
export function decryptString(encryptedData: EncryptedData, key: Uint8Array): string {
|
|
109
|
+
const plaintext = decrypt(encryptedData, key);
|
|
110
|
+
try {
|
|
111
|
+
return new TextDecoder().decode(plaintext);
|
|
112
|
+
} finally {
|
|
113
|
+
plaintext.fill(0);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Serializes encrypted data (prepends nonce to ciphertext)
|
|
119
|
+
*/
|
|
120
|
+
export function serialize(encryptedData: EncryptedData): SerializedEncryptedData {
|
|
121
|
+
const data = concatBytes(encryptedData.nonce, encryptedData.ciphertext);
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
data,
|
|
125
|
+
aad: encryptedData.aad,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Deserializes encrypted data
|
|
131
|
+
*/
|
|
132
|
+
export function deserialize(serialized: SerializedEncryptedData): EncryptedData {
|
|
133
|
+
if (serialized.data.length < NONCE_SIZE + TAG_SIZE) {
|
|
134
|
+
throw new Error('Invalid serialized data: too short');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const nonce = serialized.data.slice(0, NONCE_SIZE);
|
|
138
|
+
const ciphertext = serialized.data.slice(NONCE_SIZE);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
ciphertext,
|
|
142
|
+
nonce,
|
|
143
|
+
aad: serialized.aad,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Encrypts and serializes data in one step
|
|
149
|
+
*/
|
|
150
|
+
export function encryptAndSerialize(
|
|
151
|
+
plaintext: Uint8Array,
|
|
152
|
+
key: Uint8Array,
|
|
153
|
+
aad?: Uint8Array
|
|
154
|
+
): SerializedEncryptedData {
|
|
155
|
+
const encrypted = encrypt(plaintext, key, aad);
|
|
156
|
+
return serialize(encrypted);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Deserializes and decrypts data in one step
|
|
161
|
+
*/
|
|
162
|
+
export function deserializeAndDecrypt(
|
|
163
|
+
serialized: SerializedEncryptedData,
|
|
164
|
+
key: Uint8Array
|
|
165
|
+
): Uint8Array {
|
|
166
|
+
const encrypted = deserialize(serialized);
|
|
167
|
+
return decrypt(encrypted, key);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Converts encrypted data to hex string
|
|
172
|
+
*/
|
|
173
|
+
export function toHex(encryptedData: EncryptedData): string {
|
|
174
|
+
const serialized = serialize(encryptedData);
|
|
175
|
+
return bytesToHex(serialized.data);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Parses encrypted data from hex string
|
|
180
|
+
*/
|
|
181
|
+
export function fromHex(hex: string, aad?: Uint8Array): EncryptedData {
|
|
182
|
+
const normalized = hex.startsWith('0x') ? hex.slice(2) : hex;
|
|
183
|
+
const data = hexToBytes(normalized);
|
|
184
|
+
|
|
185
|
+
return deserialize({ data, aad });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Converts encrypted data to base64 string
|
|
190
|
+
*/
|
|
191
|
+
export function toBase64(encryptedData: EncryptedData): string {
|
|
192
|
+
const serialized = serialize(encryptedData);
|
|
193
|
+
|
|
194
|
+
if (typeof btoa === 'function') {
|
|
195
|
+
return btoa(String.fromCharCode(...serialized.data));
|
|
196
|
+
} else {
|
|
197
|
+
return Buffer.from(serialized.data).toString('base64');
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Parses encrypted data from base64 string
|
|
203
|
+
*/
|
|
204
|
+
export function fromBase64(base64: string, aad?: Uint8Array): EncryptedData {
|
|
205
|
+
let data: Uint8Array;
|
|
206
|
+
|
|
207
|
+
if (typeof atob === 'function') {
|
|
208
|
+
const binary = atob(base64);
|
|
209
|
+
data = new Uint8Array(binary.length);
|
|
210
|
+
for (let i = 0; i < binary.length; i++) {
|
|
211
|
+
data[i] = binary.charCodeAt(i);
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
data = new Uint8Array(Buffer.from(base64, 'base64'));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return deserialize({ data, aad });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function validateKey(key: Uint8Array): void {
|
|
221
|
+
if (!(key instanceof Uint8Array)) {
|
|
222
|
+
throw new Error('Key must be a Uint8Array');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (key.length !== KEY_SIZE) {
|
|
226
|
+
throw new Error(`Invalid key length: expected ${KEY_SIZE}, got ${key.length}`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function validateNonce(nonce: Uint8Array): void {
|
|
231
|
+
if (!(nonce instanceof Uint8Array)) {
|
|
232
|
+
throw new Error('Nonce must be a Uint8Array');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (nonce.length !== NONCE_SIZE) {
|
|
236
|
+
throw new Error(`Invalid nonce length: expected ${NONCE_SIZE}, got ${nonce.length}`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Generates a random encryption key
|
|
242
|
+
*/
|
|
243
|
+
export function generateKey(): Uint8Array {
|
|
244
|
+
return randomBytes(KEY_SIZE);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Generates a random nonce
|
|
249
|
+
*/
|
|
250
|
+
export function generateNonce(): Uint8Array {
|
|
251
|
+
return randomBytes(NONCE_SIZE);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Securely clears a key from memory
|
|
256
|
+
*/
|
|
257
|
+
export function clearKey(key: Uint8Array): void {
|
|
258
|
+
key.fill(0);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Checks if encrypted data appears valid (basic structure check)
|
|
263
|
+
*/
|
|
264
|
+
export function isValidEncryptedData(data: EncryptedData): boolean {
|
|
265
|
+
return (
|
|
266
|
+
data.nonce instanceof Uint8Array &&
|
|
267
|
+
data.nonce.length === NONCE_SIZE &&
|
|
268
|
+
data.ciphertext instanceof Uint8Array &&
|
|
269
|
+
data.ciphertext.length >= TAG_SIZE
|
|
270
|
+
);
|
|
271
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HKDF Key Derivation
|
|
3
|
+
*
|
|
4
|
+
* Derives deterministic sub-keys from a master secret using HKDF-SHA256 (RFC 5869).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { hkdf } from '@noble/hashes/hkdf';
|
|
8
|
+
import { sha256 } from '@noble/hashes/sha256';
|
|
9
|
+
|
|
10
|
+
/** Default output length in bytes (256 bits) */
|
|
11
|
+
const DEFAULT_KEY_LENGTH = 32;
|
|
12
|
+
|
|
13
|
+
/** Maximum allowed output length (255 * SHA-256 output = 8160 bytes) */
|
|
14
|
+
const MAX_KEY_LENGTH = 255 * 32;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Derives a sub-key from input key material using HKDF-SHA256.
|
|
18
|
+
*
|
|
19
|
+
* @param ikm - Input key material (e.g., wallet private key). MUST be high-entropy.
|
|
20
|
+
* @param salt - Domain separation salt. Can be a string or bytes.
|
|
21
|
+
* @param info - Context-specific info. Can be a string or bytes.
|
|
22
|
+
* @param length - Output key length in bytes (default: 32).
|
|
23
|
+
* @returns Derived key as Uint8Array. Caller MUST zero this after use.
|
|
24
|
+
*/
|
|
25
|
+
export function deriveKeyHKDF(
|
|
26
|
+
ikm: Uint8Array,
|
|
27
|
+
salt: string | Uint8Array,
|
|
28
|
+
info: string | Uint8Array,
|
|
29
|
+
length: number = DEFAULT_KEY_LENGTH,
|
|
30
|
+
): Uint8Array {
|
|
31
|
+
if (!ikm || ikm.length === 0) {
|
|
32
|
+
throw new Error('HKDF: input key material must not be empty');
|
|
33
|
+
}
|
|
34
|
+
if (length <= 0 || length > MAX_KEY_LENGTH) {
|
|
35
|
+
throw new Error(`HKDF: output length must be between 1 and ${MAX_KEY_LENGTH}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const saltBytes = typeof salt === 'string' ? new TextEncoder().encode(salt) : salt;
|
|
39
|
+
const infoBytes = typeof info === 'string' ? new TextEncoder().encode(info) : info;
|
|
40
|
+
|
|
41
|
+
return hkdf(sha256, ikm, saltBytes, infoBytes, length);
|
|
42
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export {
|
|
2
|
+
encrypt,
|
|
3
|
+
decrypt,
|
|
4
|
+
encryptString,
|
|
5
|
+
decryptString,
|
|
6
|
+
serialize,
|
|
7
|
+
deserialize,
|
|
8
|
+
encryptAndSerialize,
|
|
9
|
+
deserializeAndDecrypt,
|
|
10
|
+
toHex,
|
|
11
|
+
fromHex,
|
|
12
|
+
toBase64,
|
|
13
|
+
fromBase64,
|
|
14
|
+
generateKey,
|
|
15
|
+
generateNonce,
|
|
16
|
+
clearKey,
|
|
17
|
+
isValidEncryptedData,
|
|
18
|
+
KEY_SIZE,
|
|
19
|
+
NONCE_SIZE,
|
|
20
|
+
TAG_SIZE,
|
|
21
|
+
} from './aes';
|
|
22
|
+
export type { EncryptedData, SerializedEncryptedData } from './aes';
|
|
23
|
+
|
|
24
|
+
export { deriveKeyHKDF } from './hkdf';
|
|
25
|
+
|
|
26
|
+
export { split as shamirSplit, combine as shamirCombine } from './shamir';
|
|
27
|
+
export type { Share as ShamirShare } from './shamir';
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shamir's Secret Sharing over GF(2^8)
|
|
3
|
+
*
|
|
4
|
+
* Information-theoretic secret splitting: any K shares reconstruct the secret,
|
|
5
|
+
* K-1 shares reveal zero information.
|
|
6
|
+
*
|
|
7
|
+
* Uses GF(2^8) with irreducible polynomial x^8 + x^4 + x^3 + x + 1 (0x11B),
|
|
8
|
+
* same as AES. This is the standard choice for byte-level SSS.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { randomBytes } from '@noble/ciphers/webcrypto';
|
|
12
|
+
|
|
13
|
+
// ── GF(2^8) Arithmetic ─────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const IRREDUCIBLE = 0x11b;
|
|
16
|
+
|
|
17
|
+
/** Exponential table: exp[log[a] + log[b]] = a * b */
|
|
18
|
+
const EXP_TABLE = new Uint8Array(512);
|
|
19
|
+
|
|
20
|
+
/** Logarithm table: log[a] for a in 1..255 (log[0] is undefined) */
|
|
21
|
+
const LOG_TABLE = new Uint8Array(256);
|
|
22
|
+
|
|
23
|
+
// Build log/exp tables using generator 3
|
|
24
|
+
(function buildTables() {
|
|
25
|
+
let x = 1;
|
|
26
|
+
for (let i = 0; i < 255; i++) {
|
|
27
|
+
EXP_TABLE[i] = x;
|
|
28
|
+
LOG_TABLE[x] = i;
|
|
29
|
+
x = x ^ (x << 1); // multiply by generator (3 is primitive in this field)
|
|
30
|
+
if (x >= 256) x ^= IRREDUCIBLE;
|
|
31
|
+
}
|
|
32
|
+
// Extend exp table for easy modular arithmetic (avoid mod 255)
|
|
33
|
+
for (let i = 255; i < 512; i++) {
|
|
34
|
+
EXP_TABLE[i] = EXP_TABLE[i - 255]!;
|
|
35
|
+
}
|
|
36
|
+
})();
|
|
37
|
+
|
|
38
|
+
/** GF(2^8) addition: XOR */
|
|
39
|
+
function gfAdd(a: number, b: number): number {
|
|
40
|
+
return a ^ b;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** GF(2^8) multiplication via log/exp tables */
|
|
44
|
+
function gfMul(a: number, b: number): number {
|
|
45
|
+
if (a === 0 || b === 0) return 0;
|
|
46
|
+
return EXP_TABLE[LOG_TABLE[a]! + LOG_TABLE[b]!]!;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** GF(2^8) multiplicative inverse */
|
|
50
|
+
function gfInv(a: number): number {
|
|
51
|
+
if (a === 0) throw new Error('GF(2^8): division by zero');
|
|
52
|
+
return EXP_TABLE[255 - LOG_TABLE[a]!]!;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** GF(2^8) division: a / b */
|
|
56
|
+
function gfDiv(a: number, b: number): number {
|
|
57
|
+
if (b === 0) throw new Error('GF(2^8): division by zero');
|
|
58
|
+
if (a === 0) return 0;
|
|
59
|
+
return EXP_TABLE[(LOG_TABLE[a]! - LOG_TABLE[b]! + 255) % 255]!;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Share Type ──────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/** A single Shamir share */
|
|
65
|
+
export interface Share {
|
|
66
|
+
/** Share index (1..N, never 0) */
|
|
67
|
+
x: number;
|
|
68
|
+
/** Share data (same length as secret) */
|
|
69
|
+
y: Uint8Array;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Split ───────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Splits a secret into N shares with threshold K.
|
|
76
|
+
*
|
|
77
|
+
* @param secret - Secret bytes to split (any length)
|
|
78
|
+
* @param n - Total number of shares to create (2..255)
|
|
79
|
+
* @param k - Minimum shares needed for reconstruction (2..n)
|
|
80
|
+
* @returns Array of N shares
|
|
81
|
+
*/
|
|
82
|
+
export function split(secret: Uint8Array, n: number, k: number): Share[] {
|
|
83
|
+
if (k < 2) throw new Error('Threshold K must be at least 2');
|
|
84
|
+
if (n < k) throw new Error('Share count N must be >= threshold K');
|
|
85
|
+
if (n > 255) throw new Error('Maximum 255 shares (GF(2^8) limit)');
|
|
86
|
+
if (secret.length === 0) throw new Error('Secret must not be empty');
|
|
87
|
+
|
|
88
|
+
const coefficients = new Array<Uint8Array>(secret.length);
|
|
89
|
+
for (let i = 0; i < secret.length; i++) {
|
|
90
|
+
const poly = new Uint8Array(k);
|
|
91
|
+
poly[0] = secret[i]!;
|
|
92
|
+
const rand = randomBytes(k - 1);
|
|
93
|
+
poly.set(rand, 1);
|
|
94
|
+
coefficients[i] = poly;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const shares: Share[] = [];
|
|
98
|
+
for (let xi = 1; xi <= n; xi++) {
|
|
99
|
+
const y = new Uint8Array(secret.length);
|
|
100
|
+
for (let byteIdx = 0; byteIdx < secret.length; byteIdx++) {
|
|
101
|
+
y[byteIdx] = evaluatePolynomial(coefficients[byteIdx]!, xi);
|
|
102
|
+
}
|
|
103
|
+
shares.push({ x: xi, y });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
for (const poly of coefficients) {
|
|
107
|
+
poly.fill(0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return shares;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function evaluatePolynomial(coeffs: Uint8Array, x: number): number {
|
|
114
|
+
let result = 0;
|
|
115
|
+
for (let i = coeffs.length - 1; i >= 0; i--) {
|
|
116
|
+
result = gfAdd(gfMul(result, x), coeffs[i]!);
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── Combine ─────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Reconstructs a secret from K or more shares using Lagrange interpolation.
|
|
125
|
+
*
|
|
126
|
+
* @param shares - Array of K or more shares (must all have same y.length)
|
|
127
|
+
* @returns Reconstructed secret
|
|
128
|
+
*/
|
|
129
|
+
export function combine(shares: Share[]): Uint8Array {
|
|
130
|
+
if (shares.length < 2) throw new Error('Need at least 2 shares');
|
|
131
|
+
|
|
132
|
+
const secretLength = shares[0]!.y.length;
|
|
133
|
+
for (const share of shares) {
|
|
134
|
+
if (share.y.length !== secretLength) {
|
|
135
|
+
throw new Error('All shares must have the same data length');
|
|
136
|
+
}
|
|
137
|
+
if (share.x === 0) {
|
|
138
|
+
throw new Error('Share index must not be 0');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const xValues = new Set(shares.map(s => s.x));
|
|
143
|
+
if (xValues.size !== shares.length) {
|
|
144
|
+
throw new Error('Duplicate share indices');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const secret = new Uint8Array(secretLength);
|
|
148
|
+
|
|
149
|
+
for (let byteIdx = 0; byteIdx < secretLength; byteIdx++) {
|
|
150
|
+
let value = 0;
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < shares.length; i++) {
|
|
153
|
+
const xi = shares[i]!.x;
|
|
154
|
+
const yi = shares[i]!.y[byteIdx]!;
|
|
155
|
+
|
|
156
|
+
let basis = 1;
|
|
157
|
+
for (let j = 0; j < shares.length; j++) {
|
|
158
|
+
if (i === j) continue;
|
|
159
|
+
const xj = shares[j]!.x;
|
|
160
|
+
basis = gfMul(basis, gfDiv(xj, gfAdd(xi, xj)));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
value = gfAdd(value, gfMul(yi, basis));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
secret[byteIdx] = value;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return secret;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** @internal Exported for cross-platform test vector validation */
|
|
173
|
+
export const _gf = { add: gfAdd, mul: gfMul, inv: gfInv, div: gfDiv, EXP_TABLE, LOG_TABLE } as const;
|