@tinycloud/sdk-services 1.7.0 → 2.0.2-beta.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/dist/{types.d.ts → BaseService-D9BFm_rV.d.cts} +179 -27
- package/dist/BaseService-D9BFm_rV.d.ts +440 -0
- package/dist/index.cjs +3221 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1843 -0
- package/dist/index.d.ts +1826 -41
- package/dist/index.js +3136 -58
- package/dist/index.js.map +1 -1
- package/dist/kv/index.cjs +909 -0
- package/dist/kv/index.cjs.map +1 -0
- package/dist/kv/index.d.cts +748 -0
- package/dist/kv/index.d.ts +745 -7
- package/dist/kv/index.js +877 -9
- package/dist/kv/index.js.map +1 -1
- package/dist/sql/index.cjs +596 -0
- package/dist/sql/index.cjs.map +1 -0
- package/dist/sql/index.d.cts +228 -0
- package/dist/sql/index.d.ts +225 -7
- package/dist/sql/index.js +566 -8
- package/dist/sql/index.js.map +1 -1
- package/package.json +7 -6
- package/dist/base/BaseService.d.ts +0 -151
- package/dist/base/BaseService.d.ts.map +0 -1
- package/dist/base/BaseService.js +0 -221
- package/dist/base/BaseService.js.map +0 -1
- package/dist/base/index.d.ts +0 -6
- package/dist/base/index.d.ts.map +0 -1
- package/dist/base/index.js +0 -6
- package/dist/base/index.js.map +0 -1
- package/dist/base/types.d.ts +0 -36
- package/dist/base/types.d.ts.map +0 -1
- package/dist/base/types.js +0 -7
- package/dist/base/types.js.map +0 -1
- package/dist/context.d.ts +0 -142
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -218
- package/dist/context.js.map +0 -1
- package/dist/duckdb/DuckDbDatabaseHandle.d.ts +0 -23
- package/dist/duckdb/DuckDbDatabaseHandle.d.ts.map +0 -1
- package/dist/duckdb/DuckDbDatabaseHandle.js +0 -36
- package/dist/duckdb/DuckDbDatabaseHandle.js.map +0 -1
- package/dist/duckdb/DuckDbService.d.ts +0 -50
- package/dist/duckdb/DuckDbService.d.ts.map +0 -1
- package/dist/duckdb/DuckDbService.js +0 -285
- package/dist/duckdb/DuckDbService.js.map +0 -1
- package/dist/duckdb/IDuckDbService.d.ts +0 -84
- package/dist/duckdb/IDuckDbService.d.ts.map +0 -1
- package/dist/duckdb/IDuckDbService.js +0 -7
- package/dist/duckdb/IDuckDbService.js.map +0 -1
- package/dist/duckdb/index.d.ts +0 -10
- package/dist/duckdb/index.d.ts.map +0 -1
- package/dist/duckdb/index.js +0 -9
- package/dist/duckdb/index.js.map +0 -1
- package/dist/duckdb/types.d.ts +0 -148
- package/dist/duckdb/types.d.ts.map +0 -1
- package/dist/duckdb/types.js +0 -19
- package/dist/duckdb/types.js.map +0 -1
- package/dist/errors.d.ts +0 -62
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -149
- package/dist/errors.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/kv/IKVService.d.ts +0 -148
- package/dist/kv/IKVService.d.ts.map +0 -1
- package/dist/kv/IKVService.js +0 -8
- package/dist/kv/IKVService.js.map +0 -1
- package/dist/kv/KVService.d.ts +0 -155
- package/dist/kv/KVService.d.ts.map +0 -1
- package/dist/kv/KVService.js +0 -419
- package/dist/kv/KVService.js.map +0 -1
- package/dist/kv/PrefixedKVService.d.ts +0 -246
- package/dist/kv/PrefixedKVService.d.ts.map +0 -1
- package/dist/kv/PrefixedKVService.js +0 -145
- package/dist/kv/PrefixedKVService.js.map +0 -1
- package/dist/kv/index.d.ts.map +0 -1
- package/dist/kv/types.d.ts +0 -204
- package/dist/kv/types.d.ts.map +0 -1
- package/dist/kv/types.js +0 -16
- package/dist/kv/types.js.map +0 -1
- package/dist/quota/TinyCloudQuota.d.ts +0 -27
- package/dist/quota/TinyCloudQuota.d.ts.map +0 -1
- package/dist/quota/TinyCloudQuota.js +0 -31
- package/dist/quota/TinyCloudQuota.js.map +0 -1
- package/dist/quota/index.d.ts +0 -3
- package/dist/quota/index.d.ts.map +0 -1
- package/dist/quota/index.js +0 -2
- package/dist/quota/index.js.map +0 -1
- package/dist/sql/DatabaseHandle.d.ts +0 -20
- package/dist/sql/DatabaseHandle.d.ts.map +0 -1
- package/dist/sql/DatabaseHandle.js +0 -27
- package/dist/sql/DatabaseHandle.js.map +0 -1
- package/dist/sql/ISQLService.d.ts +0 -67
- package/dist/sql/ISQLService.d.ts.map +0 -1
- package/dist/sql/ISQLService.js +0 -7
- package/dist/sql/ISQLService.js.map +0 -1
- package/dist/sql/SQLService.d.ts +0 -44
- package/dist/sql/SQLService.d.ts.map +0 -1
- package/dist/sql/SQLService.js +0 -216
- package/dist/sql/SQLService.js.map +0 -1
- package/dist/sql/index.d.ts.map +0 -1
- package/dist/sql/types.d.ts +0 -102
- package/dist/sql/types.d.ts.map +0 -1
- package/dist/sql/types.js +0 -21
- package/dist/sql/types.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -94
- package/dist/types.js.map +0 -1
- package/dist/types.schema.d.ts +0 -712
- package/dist/types.schema.d.ts.map +0 -1
- package/dist/types.schema.js +0 -342
- package/dist/types.schema.js.map +0 -1
- package/dist/types.schema.test.d.ts +0 -5
- package/dist/types.schema.test.d.ts.map +0 -1
- package/dist/types.schema.test.js +0 -677
- package/dist/types.schema.test.js.map +0 -1
- package/dist/vault/DataVaultService.d.ts +0 -258
- package/dist/vault/DataVaultService.d.ts.map +0 -1
- package/dist/vault/DataVaultService.js +0 -977
- package/dist/vault/DataVaultService.js.map +0 -1
- package/dist/vault/IDataVaultService.d.ts +0 -150
- package/dist/vault/IDataVaultService.d.ts.map +0 -1
- package/dist/vault/IDataVaultService.js +0 -8
- package/dist/vault/IDataVaultService.js.map +0 -1
- package/dist/vault/createVaultCrypto.d.ts +0 -16
- package/dist/vault/createVaultCrypto.d.ts.map +0 -1
- package/dist/vault/createVaultCrypto.js +0 -12
- package/dist/vault/createVaultCrypto.js.map +0 -1
- package/dist/vault/index.d.ts +0 -10
- package/dist/vault/index.d.ts.map +0 -1
- package/dist/vault/index.js +0 -11
- package/dist/vault/index.js.map +0 -1
- package/dist/vault/types.d.ts +0 -133
- package/dist/vault/types.d.ts.map +0 -1
- package/dist/vault/types.js +0 -23
- package/dist/vault/types.js.map +0 -1
|
@@ -1,977 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* DataVaultService - Encrypted key-value storage service implementation.
|
|
3
|
-
*
|
|
4
|
-
* Platform-agnostic encrypted KV service that wraps KVService internally.
|
|
5
|
-
* Uses dependency injection via VaultCrypto for WASM crypto operations
|
|
6
|
-
* and DataVaultServiceConfig for platform dependencies.
|
|
7
|
-
*
|
|
8
|
-
* Architecture:
|
|
9
|
-
* - Extends BaseService (not KVService)
|
|
10
|
-
* - Wraps two KV instances: dataKV (prefix "vault/") and keyKV (prefix "keys/")
|
|
11
|
-
* - Master key and encryption identity live in memory only (cleared on lock)
|
|
12
|
-
*/
|
|
13
|
-
import { BaseService } from "../base/BaseService";
|
|
14
|
-
import { ok, } from "../types";
|
|
15
|
-
import { VaultHeaders, } from "./types";
|
|
16
|
-
// =============================================================================
|
|
17
|
-
// Helper Functions
|
|
18
|
-
// =============================================================================
|
|
19
|
-
/** Convert a caught value to an Error. WASM throws plain objects, not Error instances. */
|
|
20
|
-
function toError(error) {
|
|
21
|
-
if (error instanceof Error)
|
|
22
|
-
return error;
|
|
23
|
-
if (typeof error === "object" && error !== null) {
|
|
24
|
-
return new Error(JSON.stringify(error));
|
|
25
|
-
}
|
|
26
|
-
return new Error(String(error));
|
|
27
|
-
}
|
|
28
|
-
function toBytes(str) {
|
|
29
|
-
return new TextEncoder().encode(str);
|
|
30
|
-
}
|
|
31
|
-
function fromBytes(bytes) {
|
|
32
|
-
return new TextDecoder().decode(bytes);
|
|
33
|
-
}
|
|
34
|
-
function hexEncode(bytes) {
|
|
35
|
-
return Array.from(bytes)
|
|
36
|
-
.map((b) => b.toString(16).padStart(2, "0"))
|
|
37
|
-
.join("");
|
|
38
|
-
}
|
|
39
|
-
function concatBytes(...arrays) {
|
|
40
|
-
const total = arrays.reduce((acc, arr) => acc + arr.length, 0);
|
|
41
|
-
const result = new Uint8Array(total);
|
|
42
|
-
let offset = 0;
|
|
43
|
-
for (const arr of arrays) {
|
|
44
|
-
result.set(arr, offset);
|
|
45
|
-
offset += arr.length;
|
|
46
|
-
}
|
|
47
|
-
return result;
|
|
48
|
-
}
|
|
49
|
-
function base64Encode(bytes) {
|
|
50
|
-
let binary = "";
|
|
51
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
52
|
-
binary += String.fromCharCode(bytes[i]);
|
|
53
|
-
}
|
|
54
|
-
return btoa(binary);
|
|
55
|
-
}
|
|
56
|
-
function base64Decode(str) {
|
|
57
|
-
const binary = atob(str);
|
|
58
|
-
const bytes = new Uint8Array(binary.length);
|
|
59
|
-
for (let i = 0; i < binary.length; i++) {
|
|
60
|
-
bytes[i] = binary.charCodeAt(i);
|
|
61
|
-
}
|
|
62
|
-
return bytes;
|
|
63
|
-
}
|
|
64
|
-
function defaultVaultMessage(input) {
|
|
65
|
-
switch (input.code) {
|
|
66
|
-
case "DECRYPTION_FAILED": return input.message ?? "Decryption failed";
|
|
67
|
-
case "KEY_NOT_FOUND": return input.message ?? `Key not found: ${input.key}`;
|
|
68
|
-
case "INTEGRITY_ERROR": return input.message ?? "Integrity check failed";
|
|
69
|
-
case "GRANT_NOT_FOUND": return input.message ?? `Grant not found: ${input.grantor} / ${input.key}`;
|
|
70
|
-
case "VAULT_LOCKED": return input.message ?? "Vault is locked";
|
|
71
|
-
case "PUBLIC_KEY_NOT_FOUND": return input.message ?? `Public key not found for ${input.did}`;
|
|
72
|
-
case "STORAGE_ERROR": return input.message ?? input.cause.message;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
function vaultError(input) {
|
|
76
|
-
const error = {
|
|
77
|
-
...input,
|
|
78
|
-
service: "vault",
|
|
79
|
-
message: defaultVaultMessage(input),
|
|
80
|
-
};
|
|
81
|
-
return { ok: false, error };
|
|
82
|
-
}
|
|
83
|
-
// =============================================================================
|
|
84
|
-
// DataVaultService
|
|
85
|
-
// =============================================================================
|
|
86
|
-
/**
|
|
87
|
-
* Data Vault service implementation.
|
|
88
|
-
*
|
|
89
|
-
* Provides encrypted key-value storage with client-side encryption,
|
|
90
|
-
* key management, and sharing via X25519 grants.
|
|
91
|
-
*
|
|
92
|
-
* @example
|
|
93
|
-
* ```typescript
|
|
94
|
-
* // Unlock the vault
|
|
95
|
-
* await vault.unlock(signer);
|
|
96
|
-
*
|
|
97
|
-
* // Store encrypted data
|
|
98
|
-
* await vault.put('secret/notes', { content: 'Hello' });
|
|
99
|
-
*
|
|
100
|
-
* // Retrieve and decrypt
|
|
101
|
-
* const entry = await vault.get<{ content: string }>('secret/notes');
|
|
102
|
-
* if (entry.ok) {
|
|
103
|
-
* console.log(entry.data.value.content); // 'Hello'
|
|
104
|
-
* }
|
|
105
|
-
*
|
|
106
|
-
* // Share with another user
|
|
107
|
-
* await vault.grant('secret/notes', recipientDID);
|
|
108
|
-
* ```
|
|
109
|
-
*/
|
|
110
|
-
export class DataVaultService extends BaseService {
|
|
111
|
-
/**
|
|
112
|
-
* Create a new DataVaultService instance.
|
|
113
|
-
*
|
|
114
|
-
* @param config - Service configuration including crypto and tc references
|
|
115
|
-
*/
|
|
116
|
-
constructor(config) {
|
|
117
|
-
super();
|
|
118
|
-
this.masterKey = null;
|
|
119
|
-
this.encryptionIdentity = null;
|
|
120
|
-
this._isUnlocked = false;
|
|
121
|
-
this.vaultConfig = config;
|
|
122
|
-
this._config = config;
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Get the service configuration.
|
|
126
|
-
*/
|
|
127
|
-
get config() {
|
|
128
|
-
return this._config;
|
|
129
|
-
}
|
|
130
|
-
/**
|
|
131
|
-
* Whether the vault is currently unlocked.
|
|
132
|
-
*/
|
|
133
|
-
get isUnlocked() {
|
|
134
|
-
return this._isUnlocked;
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* The vault's public encryption key (X25519).
|
|
138
|
-
* Throws if vault is locked.
|
|
139
|
-
*/
|
|
140
|
-
get publicKey() {
|
|
141
|
-
if (!this.encryptionIdentity) {
|
|
142
|
-
throw new Error("Vault is locked");
|
|
143
|
-
}
|
|
144
|
-
return this.encryptionIdentity.publicKey;
|
|
145
|
-
}
|
|
146
|
-
/**
|
|
147
|
-
* Convenience accessor for crypto operations.
|
|
148
|
-
*/
|
|
149
|
-
get crypto() {
|
|
150
|
-
return this.vaultConfig.crypto;
|
|
151
|
-
}
|
|
152
|
-
/**
|
|
153
|
-
* Convenience accessor for TinyCloud instance.
|
|
154
|
-
*/
|
|
155
|
-
get tc() {
|
|
156
|
-
return this.vaultConfig.tc;
|
|
157
|
-
}
|
|
158
|
-
/**
|
|
159
|
-
* Get the host URL.
|
|
160
|
-
*/
|
|
161
|
-
get host() {
|
|
162
|
-
return this.tc.hosts[0];
|
|
163
|
-
}
|
|
164
|
-
// =========================================================================
|
|
165
|
-
// Phase 1: Core Operations
|
|
166
|
-
// =========================================================================
|
|
167
|
-
/**
|
|
168
|
-
* Unlock the vault. Triggers wallet signatures to derive keys.
|
|
169
|
-
*
|
|
170
|
-
* 1. Signs a deterministic message to derive the master key
|
|
171
|
-
* 2. Signs a second message to derive the X25519 encryption identity
|
|
172
|
-
* 3. Ensures public space exists and publishes the public key
|
|
173
|
-
*
|
|
174
|
-
* @param signer - Object with signMessage method (wallet/key signer)
|
|
175
|
-
*/
|
|
176
|
-
async unlock(signer) {
|
|
177
|
-
return this.withTelemetry("unlock", undefined, async () => {
|
|
178
|
-
const s = signer;
|
|
179
|
-
const spaceId = this.vaultConfig.spaceId;
|
|
180
|
-
try {
|
|
181
|
-
// Step 1: Derive master key from deterministic signature
|
|
182
|
-
const masterSig = await s.signMessage(`tinycloud-vault-master-v1:${spaceId}`);
|
|
183
|
-
const masterSigBytes = toBytes(masterSig);
|
|
184
|
-
this.masterKey = this.crypto.deriveKey(masterSigBytes, this.crypto.sha256(toBytes(spaceId)), toBytes("vault-master"));
|
|
185
|
-
// Step 2: Derive encryption identity from second signature
|
|
186
|
-
const identitySig = await s.signMessage("tinycloud-encryption-identity-v1");
|
|
187
|
-
const identitySigBytes = toBytes(identitySig);
|
|
188
|
-
const seed = this.crypto.deriveKey(identitySigBytes, toBytes("tinycloud-x25519"), toBytes("encryption-identity"));
|
|
189
|
-
this.encryptionIdentity = this.crypto.x25519FromSeed(seed);
|
|
190
|
-
// Step 3: Publish vault metadata to public space (best-effort)
|
|
191
|
-
// This enables other users to discover our public key and vault location.
|
|
192
|
-
// Non-fatal: key derivation succeeded, publishing is optional.
|
|
193
|
-
try {
|
|
194
|
-
const pubKeyB64 = base64Encode(this.encryptionIdentity.publicKey);
|
|
195
|
-
const publicSpaceId = this.tc.makePublicSpaceId(this.tc.address, this.tc.chainId);
|
|
196
|
-
// Unauthenticated read — no delegation needed
|
|
197
|
-
const existing = await this.tc.readPublicSpace(this.host, publicSpaceId, ".well-known/vault-pubkey");
|
|
198
|
-
if (!existing.ok || existing.data !== pubKeyB64) {
|
|
199
|
-
await this.tc.ensurePublicSpace();
|
|
200
|
-
await this.tc.publicKV.put(".well-known/vault-pubkey", pubKeyB64);
|
|
201
|
-
await this.tc.publicKV.put(".well-known/vault-version", "1");
|
|
202
|
-
await this.tc.publicKV.put(".well-known/vault-space", this.vaultConfig.spaceId);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
catch {
|
|
206
|
-
// Public key publishing failed — vault still usable
|
|
207
|
-
}
|
|
208
|
-
this._isUnlocked = true;
|
|
209
|
-
return ok(undefined);
|
|
210
|
-
}
|
|
211
|
-
catch (error) {
|
|
212
|
-
// Clear key material on failure
|
|
213
|
-
this.masterKey = null;
|
|
214
|
-
this.encryptionIdentity = null;
|
|
215
|
-
return vaultError({
|
|
216
|
-
code: "STORAGE_ERROR",
|
|
217
|
-
cause: toError(error),
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
});
|
|
221
|
-
}
|
|
222
|
-
/**
|
|
223
|
-
* Lock the vault, clearing all key material from memory.
|
|
224
|
-
*/
|
|
225
|
-
lock() {
|
|
226
|
-
this.masterKey = null;
|
|
227
|
-
this.encryptionIdentity = null;
|
|
228
|
-
this._isUnlocked = false;
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Called when SDK signs out. Locks the vault and aborts operations.
|
|
232
|
-
*/
|
|
233
|
-
onSignOut() {
|
|
234
|
-
this.lock();
|
|
235
|
-
super.onSignOut();
|
|
236
|
-
}
|
|
237
|
-
/**
|
|
238
|
-
* Encrypt and store a value at the given key.
|
|
239
|
-
*
|
|
240
|
-
* @param key - The key to store under
|
|
241
|
-
* @param value - The value to encrypt and store
|
|
242
|
-
* @param options - Optional put configuration
|
|
243
|
-
*/
|
|
244
|
-
async put(key, value, options) {
|
|
245
|
-
return this.withTelemetry("put", key, async () => {
|
|
246
|
-
if (!this._isUnlocked || !this.masterKey) {
|
|
247
|
-
return vaultError({
|
|
248
|
-
code: "VAULT_LOCKED",
|
|
249
|
-
message: "Vault must be unlocked before storing data",
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
if (!this.requireAuth()) {
|
|
253
|
-
return vaultError({
|
|
254
|
-
code: "VAULT_LOCKED",
|
|
255
|
-
message: "Authentication required",
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
try {
|
|
259
|
-
// Serialize value
|
|
260
|
-
let plaintext;
|
|
261
|
-
if (value instanceof Uint8Array) {
|
|
262
|
-
plaintext = value;
|
|
263
|
-
}
|
|
264
|
-
else if (options?.serialize) {
|
|
265
|
-
plaintext = options.serialize(value);
|
|
266
|
-
}
|
|
267
|
-
else if (typeof value === "string") {
|
|
268
|
-
plaintext = toBytes(value);
|
|
269
|
-
}
|
|
270
|
-
else {
|
|
271
|
-
plaintext = toBytes(JSON.stringify(value));
|
|
272
|
-
}
|
|
273
|
-
const contentType = options?.contentType ??
|
|
274
|
-
(value instanceof Uint8Array
|
|
275
|
-
? "application/octet-stream"
|
|
276
|
-
: "application/json");
|
|
277
|
-
// Generate per-entry key
|
|
278
|
-
const entryKey = this.crypto.randomBytes(32);
|
|
279
|
-
const keyId = hexEncode(this.crypto.sha256(entryKey)).slice(0, 16);
|
|
280
|
-
// Encrypt value with entry key
|
|
281
|
-
const encrypted = this.crypto.encrypt(entryKey, plaintext);
|
|
282
|
-
// Encrypt entry key with master key
|
|
283
|
-
const keyBlob = this.crypto.encrypt(this.masterKey, entryKey);
|
|
284
|
-
// Build metadata
|
|
285
|
-
const metadata = {
|
|
286
|
-
[VaultHeaders.VERSION]: "1",
|
|
287
|
-
[VaultHeaders.CIPHER]: "aes-256-gcm",
|
|
288
|
-
[VaultHeaders.KEY_ID]: keyId,
|
|
289
|
-
[VaultHeaders.CONTENT_TYPE]: contentType,
|
|
290
|
-
[VaultHeaders.KDF]: "hkdf-sha256",
|
|
291
|
-
[VaultHeaders.KEY_ROTATION]: this.vaultConfig.keyRotation ?? "per-write",
|
|
292
|
-
...(options?.metadata ?? {}),
|
|
293
|
-
};
|
|
294
|
-
// Store encrypted entry key in key space
|
|
295
|
-
const keyMetadata = JSON.stringify({
|
|
296
|
-
keyId,
|
|
297
|
-
contentType,
|
|
298
|
-
...metadata,
|
|
299
|
-
});
|
|
300
|
-
const keyPayload = JSON.stringify({
|
|
301
|
-
key: base64Encode(keyBlob),
|
|
302
|
-
metadata: keyMetadata,
|
|
303
|
-
});
|
|
304
|
-
const keyPutResult = await this.tc.kv.put(`keys/${key}`, keyPayload);
|
|
305
|
-
if (!keyPutResult.ok) {
|
|
306
|
-
return vaultError({
|
|
307
|
-
code: "STORAGE_ERROR",
|
|
308
|
-
cause: new Error(`Failed to store key blob: ${keyPutResult.error.message}`),
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
// Store encrypted value in data space
|
|
312
|
-
const valuePayload = JSON.stringify({
|
|
313
|
-
data: base64Encode(encrypted),
|
|
314
|
-
metadata,
|
|
315
|
-
});
|
|
316
|
-
const valuePutResult = await this.tc.kv.put(`vault/${key}`, valuePayload);
|
|
317
|
-
if (!valuePutResult.ok) {
|
|
318
|
-
return vaultError({
|
|
319
|
-
code: "STORAGE_ERROR",
|
|
320
|
-
cause: new Error(`Failed to store encrypted value: ${valuePutResult.error.message}`),
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
return ok(undefined);
|
|
324
|
-
}
|
|
325
|
-
catch (error) {
|
|
326
|
-
return vaultError({
|
|
327
|
-
code: "STORAGE_ERROR",
|
|
328
|
-
cause: toError(error),
|
|
329
|
-
});
|
|
330
|
-
}
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
/**
|
|
334
|
-
* Retrieve and decrypt a value by key.
|
|
335
|
-
*
|
|
336
|
-
* @param key - The key to retrieve
|
|
337
|
-
* @param options - Optional get configuration
|
|
338
|
-
* @returns Result with the decrypted entry
|
|
339
|
-
*/
|
|
340
|
-
async get(key, options) {
|
|
341
|
-
return this.withTelemetry("get", key, async () => {
|
|
342
|
-
if (!this._isUnlocked || !this.masterKey) {
|
|
343
|
-
return vaultError({
|
|
344
|
-
code: "VAULT_LOCKED",
|
|
345
|
-
message: "Vault must be unlocked before reading data",
|
|
346
|
-
});
|
|
347
|
-
}
|
|
348
|
-
if (!this.requireAuth()) {
|
|
349
|
-
return vaultError({
|
|
350
|
-
code: "VAULT_LOCKED",
|
|
351
|
-
message: "Authentication required",
|
|
352
|
-
});
|
|
353
|
-
}
|
|
354
|
-
try {
|
|
355
|
-
// Fetch encrypted entry key from key space
|
|
356
|
-
const keyResult = await this.tc.kv.get(`keys/${key}`, {
|
|
357
|
-
raw: true,
|
|
358
|
-
});
|
|
359
|
-
if (!keyResult.ok) {
|
|
360
|
-
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
361
|
-
}
|
|
362
|
-
const keyEnvelope = JSON.parse(keyResult.data.data);
|
|
363
|
-
const keyBlobBytes = base64Decode(keyEnvelope.key);
|
|
364
|
-
const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
|
|
365
|
-
// Fetch encrypted value from data space
|
|
366
|
-
const valueResult = await this.tc.kv.get(`vault/${key}`, {
|
|
367
|
-
raw: true,
|
|
368
|
-
});
|
|
369
|
-
if (!valueResult.ok) {
|
|
370
|
-
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
371
|
-
}
|
|
372
|
-
const valueEnvelope = JSON.parse(valueResult.data.data);
|
|
373
|
-
const encryptedBytes = base64Decode(valueEnvelope.data);
|
|
374
|
-
const plaintext = this.crypto.decrypt(entryKey, encryptedBytes);
|
|
375
|
-
// Read metadata
|
|
376
|
-
const metadata = valueEnvelope.metadata ?? {};
|
|
377
|
-
const contentType = metadata[VaultHeaders.CONTENT_TYPE] ?? "application/json";
|
|
378
|
-
const keyId = metadata[VaultHeaders.KEY_ID] ?? "";
|
|
379
|
-
// Deserialize
|
|
380
|
-
let value;
|
|
381
|
-
if (options?.raw) {
|
|
382
|
-
value = plaintext;
|
|
383
|
-
}
|
|
384
|
-
else if (options?.deserialize) {
|
|
385
|
-
value = options.deserialize(plaintext);
|
|
386
|
-
}
|
|
387
|
-
else if (contentType === "application/json") {
|
|
388
|
-
value = JSON.parse(fromBytes(plaintext));
|
|
389
|
-
}
|
|
390
|
-
else {
|
|
391
|
-
value = plaintext;
|
|
392
|
-
}
|
|
393
|
-
return ok({ value, metadata, keyId });
|
|
394
|
-
}
|
|
395
|
-
catch (error) {
|
|
396
|
-
if (error instanceof Error &&
|
|
397
|
-
error.message.includes("decryption")) {
|
|
398
|
-
return vaultError({
|
|
399
|
-
code: "DECRYPTION_FAILED",
|
|
400
|
-
message: error.message,
|
|
401
|
-
});
|
|
402
|
-
}
|
|
403
|
-
return vaultError({
|
|
404
|
-
code: "STORAGE_ERROR",
|
|
405
|
-
cause: toError(error),
|
|
406
|
-
});
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Delete an encrypted key.
|
|
412
|
-
* Removes both the encrypted value and the key blob.
|
|
413
|
-
*
|
|
414
|
-
* @param key - The key to delete
|
|
415
|
-
*/
|
|
416
|
-
async delete(key) {
|
|
417
|
-
return this.withTelemetry("delete", key, async () => {
|
|
418
|
-
if (!this._isUnlocked) {
|
|
419
|
-
return vaultError({
|
|
420
|
-
code: "VAULT_LOCKED",
|
|
421
|
-
message: "Vault must be unlocked before deleting data",
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
if (!this.requireAuth()) {
|
|
425
|
-
return vaultError({
|
|
426
|
-
code: "VAULT_LOCKED",
|
|
427
|
-
message: "Authentication required",
|
|
428
|
-
});
|
|
429
|
-
}
|
|
430
|
-
try {
|
|
431
|
-
// Delete from both key space and data space
|
|
432
|
-
const [keyDelResult, valueDelResult] = await Promise.all([
|
|
433
|
-
this.tc.kv.delete(`keys/${key}`),
|
|
434
|
-
this.tc.kv.delete(`vault/${key}`),
|
|
435
|
-
]);
|
|
436
|
-
if (!keyDelResult.ok && !valueDelResult.ok) {
|
|
437
|
-
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
438
|
-
}
|
|
439
|
-
return ok(undefined);
|
|
440
|
-
}
|
|
441
|
-
catch (error) {
|
|
442
|
-
return vaultError({
|
|
443
|
-
code: "STORAGE_ERROR",
|
|
444
|
-
cause: toError(error),
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
/**
|
|
450
|
-
* List vault keys with optional prefix filtering.
|
|
451
|
-
*
|
|
452
|
-
* @param options - Optional list configuration
|
|
453
|
-
* @returns Result with array of key names (vault/ prefix stripped)
|
|
454
|
-
*/
|
|
455
|
-
async list(options) {
|
|
456
|
-
return this.withTelemetry("list", options?.prefix, async () => {
|
|
457
|
-
if (!this._isUnlocked) {
|
|
458
|
-
return vaultError({
|
|
459
|
-
code: "VAULT_LOCKED",
|
|
460
|
-
message: "Vault must be unlocked before listing data",
|
|
461
|
-
});
|
|
462
|
-
}
|
|
463
|
-
if (!this.requireAuth()) {
|
|
464
|
-
return vaultError({
|
|
465
|
-
code: "VAULT_LOCKED",
|
|
466
|
-
message: "Authentication required",
|
|
467
|
-
});
|
|
468
|
-
}
|
|
469
|
-
try {
|
|
470
|
-
const listPrefix = options?.prefix
|
|
471
|
-
? `vault/${options.prefix}`
|
|
472
|
-
: "vault/";
|
|
473
|
-
const listResult = await this.tc.kv.list({
|
|
474
|
-
prefix: listPrefix,
|
|
475
|
-
removePrefix: true,
|
|
476
|
-
});
|
|
477
|
-
if (!listResult.ok) {
|
|
478
|
-
return vaultError({
|
|
479
|
-
code: "STORAGE_ERROR",
|
|
480
|
-
cause: new Error(`Failed to list vault keys: ${listResult.error.message}`),
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
// Keys are already stripped of the "vault/" prefix by removePrefix
|
|
484
|
-
let keys = listResult.data.keys;
|
|
485
|
-
// If a user prefix was provided, strip it too if requested
|
|
486
|
-
if (options?.removePrefix && options.prefix) {
|
|
487
|
-
const userPrefix = options.prefix.endsWith("/")
|
|
488
|
-
? options.prefix
|
|
489
|
-
: `${options.prefix}/`;
|
|
490
|
-
keys = keys.map((k) => k.startsWith(userPrefix) ? k.slice(userPrefix.length) : k);
|
|
491
|
-
}
|
|
492
|
-
return ok(keys);
|
|
493
|
-
}
|
|
494
|
-
catch (error) {
|
|
495
|
-
return vaultError({
|
|
496
|
-
code: "STORAGE_ERROR",
|
|
497
|
-
cause: toError(error),
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
});
|
|
501
|
-
}
|
|
502
|
-
/**
|
|
503
|
-
* Get envelope metadata for a key without decrypting the value.
|
|
504
|
-
*
|
|
505
|
-
* @param key - The key to inspect
|
|
506
|
-
* @returns Result with metadata headers
|
|
507
|
-
*/
|
|
508
|
-
async head(key) {
|
|
509
|
-
return this.withTelemetry("head", key, async () => {
|
|
510
|
-
if (!this._isUnlocked) {
|
|
511
|
-
return vaultError({
|
|
512
|
-
code: "VAULT_LOCKED",
|
|
513
|
-
message: "Vault must be unlocked before reading metadata",
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
if (!this.requireAuth()) {
|
|
517
|
-
return vaultError({
|
|
518
|
-
code: "VAULT_LOCKED",
|
|
519
|
-
message: "Authentication required",
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
try {
|
|
523
|
-
// Fetch envelope without decrypting
|
|
524
|
-
const valueResult = await this.tc.kv.get(`vault/${key}`, {
|
|
525
|
-
raw: true,
|
|
526
|
-
});
|
|
527
|
-
if (!valueResult.ok) {
|
|
528
|
-
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
529
|
-
}
|
|
530
|
-
const valueEnvelope = JSON.parse(valueResult.data.data);
|
|
531
|
-
const metadata = valueEnvelope.metadata ?? {};
|
|
532
|
-
return ok(metadata);
|
|
533
|
-
}
|
|
534
|
-
catch (error) {
|
|
535
|
-
return vaultError({
|
|
536
|
-
code: "STORAGE_ERROR",
|
|
537
|
-
cause: toError(error),
|
|
538
|
-
});
|
|
539
|
-
}
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
// =========================================================================
|
|
543
|
-
// Batch Operations
|
|
544
|
-
// =========================================================================
|
|
545
|
-
/**
|
|
546
|
-
* Encrypt and store multiple entries.
|
|
547
|
-
*
|
|
548
|
-
* @param entries - Array of key/value pairs with optional per-entry options
|
|
549
|
-
* @returns Array of results, one per entry
|
|
550
|
-
*/
|
|
551
|
-
async putMany(entries) {
|
|
552
|
-
return Promise.all(entries.map((entry) => this.put(entry.key, entry.value, entry.options)));
|
|
553
|
-
}
|
|
554
|
-
/**
|
|
555
|
-
* Retrieve and decrypt multiple keys.
|
|
556
|
-
*
|
|
557
|
-
* @param keys - Array of keys to retrieve
|
|
558
|
-
* @param options - Optional get configuration applied to all entries
|
|
559
|
-
* @returns Array of results, one per key
|
|
560
|
-
*/
|
|
561
|
-
async getMany(keys, options) {
|
|
562
|
-
return Promise.all(keys.map((key) => this.get(key, options)));
|
|
563
|
-
}
|
|
564
|
-
// =========================================================================
|
|
565
|
-
// Phase 2: Sharing
|
|
566
|
-
// =========================================================================
|
|
567
|
-
/**
|
|
568
|
-
* Re-encrypt a vault key for another user (renamed from grant).
|
|
569
|
-
* Re-encrypts the data key to the recipient's public key via X25519 DH.
|
|
570
|
-
*
|
|
571
|
-
* @param key - The key to share
|
|
572
|
-
* @param recipientDID - The recipient's primary DID (did:pkh:...)
|
|
573
|
-
* @param options - Optional grant configuration
|
|
574
|
-
*/
|
|
575
|
-
async reencrypt(key, recipientDID, options) {
|
|
576
|
-
return this.withTelemetry("reencrypt", key, async () => {
|
|
577
|
-
if (!this._isUnlocked || !this.masterKey) {
|
|
578
|
-
return vaultError({
|
|
579
|
-
code: "VAULT_LOCKED",
|
|
580
|
-
message: "Vault must be unlocked before granting access",
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
if (!this.requireAuth()) {
|
|
584
|
-
return vaultError({
|
|
585
|
-
code: "VAULT_LOCKED",
|
|
586
|
-
message: "Authentication required",
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
try {
|
|
590
|
-
// Step 1: Resolve recipient's public key
|
|
591
|
-
const pubKeyResult = await this.resolvePublicKey(recipientDID);
|
|
592
|
-
if (!pubKeyResult.ok) {
|
|
593
|
-
return pubKeyResult;
|
|
594
|
-
}
|
|
595
|
-
const bobPubKey = pubKeyResult.data;
|
|
596
|
-
// Step 2: Fetch and decrypt entry key from key space
|
|
597
|
-
const keyResult = await this.tc.kv.get(`keys/${key}`, {
|
|
598
|
-
raw: true,
|
|
599
|
-
});
|
|
600
|
-
if (!keyResult.ok) {
|
|
601
|
-
return vaultError({ code: "KEY_NOT_FOUND", key });
|
|
602
|
-
}
|
|
603
|
-
const keyEnvelope = JSON.parse(keyResult.data.data);
|
|
604
|
-
const keyBlobBytes = base64Decode(keyEnvelope.key);
|
|
605
|
-
const entryKey = this.crypto.decrypt(this.masterKey, keyBlobBytes);
|
|
606
|
-
// Step 3: Create ephemeral X25519 key pair
|
|
607
|
-
const ephemeralSeed = this.crypto.randomBytes(32);
|
|
608
|
-
const ephemeralKeyPair = this.crypto.x25519FromSeed(ephemeralSeed);
|
|
609
|
-
// Step 4: Compute shared secret via DH
|
|
610
|
-
const sharedSecret = this.crypto.x25519Dh(ephemeralKeyPair.privateKey, bobPubKey);
|
|
611
|
-
// Step 5: Derive encryption key from shared secret
|
|
612
|
-
const encryptionKey = this.crypto.deriveKey(sharedSecret, toBytes("tinycloud-x25519"), toBytes("vault-grant"));
|
|
613
|
-
// Step 6: Encrypt entry key with derived key
|
|
614
|
-
const encryptedGrant = this.crypto.encrypt(encryptionKey, entryKey);
|
|
615
|
-
// Step 7: Concatenate ephemeral public key + encrypted grant
|
|
616
|
-
const grantBlob = concatBytes(ephemeralKeyPair.publicKey, encryptedGrant);
|
|
617
|
-
// Step 8: Store grant in key space
|
|
618
|
-
const grantPayload = JSON.stringify({
|
|
619
|
-
grant: base64Encode(grantBlob),
|
|
620
|
-
spaceId: this.vaultConfig.spaceId,
|
|
621
|
-
metadata: {
|
|
622
|
-
[VaultHeaders.GRANT_VERSION]: "1",
|
|
623
|
-
[VaultHeaders.GRANTOR]: this.tc.did,
|
|
624
|
-
...(options?.metadata ?? {}),
|
|
625
|
-
},
|
|
626
|
-
});
|
|
627
|
-
// Store grant in the vault's space (main space)
|
|
628
|
-
const grantPutResult = await this.tc.kv.put(`grants/${recipientDID}/${key}`, grantPayload);
|
|
629
|
-
if (!grantPutResult.ok) {
|
|
630
|
-
return vaultError({
|
|
631
|
-
code: "STORAGE_ERROR",
|
|
632
|
-
cause: new Error(`Failed to store grant: ${grantPutResult.error.message}`),
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
return ok(undefined);
|
|
636
|
-
}
|
|
637
|
-
catch (error) {
|
|
638
|
-
return vaultError({
|
|
639
|
-
code: "STORAGE_ERROR",
|
|
640
|
-
cause: toError(error),
|
|
641
|
-
});
|
|
642
|
-
}
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
/**
|
|
646
|
-
* @deprecated Use reencrypt() instead.
|
|
647
|
-
*/
|
|
648
|
-
async grant(key, recipientDID, options) {
|
|
649
|
-
return this.reencrypt(key, recipientDID, options);
|
|
650
|
-
}
|
|
651
|
-
/**
|
|
652
|
-
* Retrieve and decrypt a value shared by another user.
|
|
653
|
-
*
|
|
654
|
-
* @param grantorDID - The DID of the user who shared the data
|
|
655
|
-
* @param key - The key that was shared
|
|
656
|
-
* @param options - Optional get configuration
|
|
657
|
-
* @returns Result with the decrypted entry
|
|
658
|
-
*/
|
|
659
|
-
async getShared(grantorDID, key, options) {
|
|
660
|
-
return this.withTelemetry("getShared", key, async () => {
|
|
661
|
-
if (!this._isUnlocked ||
|
|
662
|
-
!this.masterKey ||
|
|
663
|
-
!this.encryptionIdentity) {
|
|
664
|
-
return vaultError({
|
|
665
|
-
code: "VAULT_LOCKED",
|
|
666
|
-
message: "Vault must be unlocked before reading shared data",
|
|
667
|
-
});
|
|
668
|
-
}
|
|
669
|
-
if (!this.requireAuth()) {
|
|
670
|
-
return vaultError({
|
|
671
|
-
code: "VAULT_LOCKED",
|
|
672
|
-
message: "Authentication required",
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
try {
|
|
676
|
-
const myDID = this.tc.did;
|
|
677
|
-
const grantorKV = options?.kv;
|
|
678
|
-
if (!grantorKV) {
|
|
679
|
-
return vaultError({
|
|
680
|
-
code: "STORAGE_ERROR",
|
|
681
|
-
cause: new Error("getShared requires a delegated KV service via options.kv. " +
|
|
682
|
-
"Use useDelegation() to get delegated access, then pass { kv: access.kv }."),
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
// Step 1: Fetch grant from grantor's space via delegated KV
|
|
686
|
-
const grantResult = await grantorKV.get(`grants/${myDID}/${key}`, {
|
|
687
|
-
raw: true,
|
|
688
|
-
});
|
|
689
|
-
if (!grantResult.ok) {
|
|
690
|
-
return vaultError({
|
|
691
|
-
code: "GRANT_NOT_FOUND",
|
|
692
|
-
grantor: grantorDID,
|
|
693
|
-
key,
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
const grantEnvelope = typeof grantResult.data?.data === "string"
|
|
697
|
-
? JSON.parse(grantResult.data.data)
|
|
698
|
-
: grantResult.data?.data;
|
|
699
|
-
const grantBlobBytes = base64Decode(grantEnvelope.grant);
|
|
700
|
-
// Step 2: Extract ephemeral public key and encrypted grant
|
|
701
|
-
const ephemeralPubKey = grantBlobBytes.slice(0, 32);
|
|
702
|
-
const encryptedGrant = grantBlobBytes.slice(32);
|
|
703
|
-
// Step 3: Compute shared secret using our private key
|
|
704
|
-
const sharedSecret = this.crypto.x25519Dh(this.encryptionIdentity.privateKey, ephemeralPubKey);
|
|
705
|
-
// Step 4: Derive decryption key
|
|
706
|
-
const encryptionKey = this.crypto.deriveKey(sharedSecret, toBytes("tinycloud-x25519"), toBytes("vault-grant"));
|
|
707
|
-
// Step 5: Decrypt entry key
|
|
708
|
-
const entryKey = this.crypto.decrypt(encryptionKey, encryptedGrant);
|
|
709
|
-
// Step 6: Fetch encrypted value from grantor's space via delegated KV
|
|
710
|
-
const valueResult = await grantorKV.get(`vault/${key}`, {
|
|
711
|
-
raw: true,
|
|
712
|
-
});
|
|
713
|
-
if (!valueResult.ok) {
|
|
714
|
-
return vaultError({
|
|
715
|
-
code: "KEY_NOT_FOUND",
|
|
716
|
-
key,
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
const valueEnvelope = typeof valueResult.data?.data === "string"
|
|
720
|
-
? JSON.parse(valueResult.data.data)
|
|
721
|
-
: valueResult.data?.data;
|
|
722
|
-
const encryptedBytes = base64Decode(valueEnvelope.data);
|
|
723
|
-
const plaintext = this.crypto.decrypt(entryKey, encryptedBytes);
|
|
724
|
-
// Read metadata
|
|
725
|
-
const metadata = valueEnvelope.metadata ?? {};
|
|
726
|
-
const contentType = metadata[VaultHeaders.CONTENT_TYPE] ?? "application/json";
|
|
727
|
-
const keyId = metadata[VaultHeaders.KEY_ID] ?? "";
|
|
728
|
-
// Deserialize
|
|
729
|
-
let value;
|
|
730
|
-
if (options?.raw) {
|
|
731
|
-
value = plaintext;
|
|
732
|
-
}
|
|
733
|
-
else if (options?.deserialize) {
|
|
734
|
-
value = options.deserialize(plaintext);
|
|
735
|
-
}
|
|
736
|
-
else if (contentType === "application/json") {
|
|
737
|
-
value = JSON.parse(fromBytes(plaintext));
|
|
738
|
-
}
|
|
739
|
-
else {
|
|
740
|
-
value = plaintext;
|
|
741
|
-
}
|
|
742
|
-
return ok({ value, metadata, keyId });
|
|
743
|
-
}
|
|
744
|
-
catch (error) {
|
|
745
|
-
if (error instanceof Error &&
|
|
746
|
-
error.message.includes("decryption")) {
|
|
747
|
-
return vaultError({
|
|
748
|
-
code: "DECRYPTION_FAILED",
|
|
749
|
-
message: error.message,
|
|
750
|
-
});
|
|
751
|
-
}
|
|
752
|
-
return vaultError({
|
|
753
|
-
code: "STORAGE_ERROR",
|
|
754
|
-
cause: toError(error),
|
|
755
|
-
});
|
|
756
|
-
}
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
/**
|
|
760
|
-
* Resolve another user's public encryption key from their DID.
|
|
761
|
-
*
|
|
762
|
-
* @param did - The DID to resolve (did:pkh:eip155:{chainId}:{address})
|
|
763
|
-
* @returns Result with the public key bytes
|
|
764
|
-
*/
|
|
765
|
-
async resolvePublicKey(did) {
|
|
766
|
-
try {
|
|
767
|
-
const parts = this.parseDID(did);
|
|
768
|
-
if (!parts) {
|
|
769
|
-
return vaultError({ code: "PUBLIC_KEY_NOT_FOUND", did });
|
|
770
|
-
}
|
|
771
|
-
const spaceId = this.tc.makePublicSpaceId(parts.address, parts.chainId);
|
|
772
|
-
const result = await this.tc.readPublicSpace(this.host, spaceId, ".well-known/vault-pubkey");
|
|
773
|
-
if (!result.ok) {
|
|
774
|
-
return vaultError({ code: "PUBLIC_KEY_NOT_FOUND", did });
|
|
775
|
-
}
|
|
776
|
-
const pubKeyBytes = base64Decode(result.data);
|
|
777
|
-
return { ok: true, data: pubKeyBytes };
|
|
778
|
-
}
|
|
779
|
-
catch (error) {
|
|
780
|
-
return vaultError({ code: "PUBLIC_KEY_NOT_FOUND", did });
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
/**
|
|
784
|
-
* List DIDs that have been granted access to a key.
|
|
785
|
-
*
|
|
786
|
-
* @param key - The key to list grants for
|
|
787
|
-
* @returns Result with array of recipient DIDs
|
|
788
|
-
*/
|
|
789
|
-
async listGrants(key) {
|
|
790
|
-
return this.withTelemetry("listGrants", key, async () => {
|
|
791
|
-
if (!this._isUnlocked) {
|
|
792
|
-
return vaultError({
|
|
793
|
-
code: "VAULT_LOCKED",
|
|
794
|
-
message: "Vault must be unlocked before listing grants",
|
|
795
|
-
});
|
|
796
|
-
}
|
|
797
|
-
if (!this.requireAuth()) {
|
|
798
|
-
return vaultError({
|
|
799
|
-
code: "VAULT_LOCKED",
|
|
800
|
-
message: "Authentication required",
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
|
-
try {
|
|
804
|
-
const listResult = await this.tc.kv.list({
|
|
805
|
-
prefix: "grants/",
|
|
806
|
-
removePrefix: true,
|
|
807
|
-
});
|
|
808
|
-
if (!listResult.ok) {
|
|
809
|
-
return vaultError({
|
|
810
|
-
code: "STORAGE_ERROR",
|
|
811
|
-
cause: new Error(`Failed to list grants: ${listResult.error.message}`),
|
|
812
|
-
});
|
|
813
|
-
}
|
|
814
|
-
// Grant paths are: {recipientDID}/{key}
|
|
815
|
-
// Filter for the specific key and extract DIDs
|
|
816
|
-
const dids = [];
|
|
817
|
-
for (const grantPath of listResult.data.keys) {
|
|
818
|
-
// Path format: {recipientDID}/{key}
|
|
819
|
-
// The key may contain slashes, so we need to match the suffix
|
|
820
|
-
if (grantPath.endsWith(`/${key}`)) {
|
|
821
|
-
const did = grantPath.slice(0, grantPath.length - key.length - 1);
|
|
822
|
-
if (did) {
|
|
823
|
-
dids.push(did);
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
return ok(dids);
|
|
828
|
-
}
|
|
829
|
-
catch (error) {
|
|
830
|
-
return vaultError({
|
|
831
|
-
code: "STORAGE_ERROR",
|
|
832
|
-
cause: toError(error),
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
});
|
|
836
|
-
}
|
|
837
|
-
// =========================================================================
|
|
838
|
-
// Phase 3: Key Rotation / Revocation
|
|
839
|
-
// =========================================================================
|
|
840
|
-
/**
|
|
841
|
-
* Revoke a previously issued grant.
|
|
842
|
-
*
|
|
843
|
-
* This performs a full key rotation:
|
|
844
|
-
* 1. Lists current grantees
|
|
845
|
-
* 2. Removes the revoked recipient
|
|
846
|
-
* 3. Re-encrypts the value with a new entry key
|
|
847
|
-
* 4. Re-issues grants to remaining recipients
|
|
848
|
-
*
|
|
849
|
-
* @param key - The key to revoke access to
|
|
850
|
-
* @param recipientDID - The recipient whose access to revoke
|
|
851
|
-
*/
|
|
852
|
-
async revoke(key, recipientDID) {
|
|
853
|
-
return this.withTelemetry("revoke", key, async () => {
|
|
854
|
-
if (!this._isUnlocked || !this.masterKey) {
|
|
855
|
-
return vaultError({
|
|
856
|
-
code: "VAULT_LOCKED",
|
|
857
|
-
message: "Vault must be unlocked before revoking access",
|
|
858
|
-
});
|
|
859
|
-
}
|
|
860
|
-
if (!this.requireAuth()) {
|
|
861
|
-
return vaultError({
|
|
862
|
-
code: "VAULT_LOCKED",
|
|
863
|
-
message: "Authentication required",
|
|
864
|
-
});
|
|
865
|
-
}
|
|
866
|
-
try {
|
|
867
|
-
// Step 1: List all current grantees
|
|
868
|
-
const granteesResult = await this.listGrants(key);
|
|
869
|
-
if (!granteesResult.ok) {
|
|
870
|
-
return granteesResult;
|
|
871
|
-
}
|
|
872
|
-
const remainingGrantees = granteesResult.data.filter((did) => did !== recipientDID);
|
|
873
|
-
// Step 2: Delete the grant for the revoked recipient
|
|
874
|
-
const deleteGrantResult = await this.tc.kv.delete(`grants/${recipientDID}/${key}`);
|
|
875
|
-
// Grant may already be deleted, that's fine
|
|
876
|
-
// Step 3: Fetch and decrypt current value
|
|
877
|
-
const getResult = await this.get(key);
|
|
878
|
-
if (!getResult.ok) {
|
|
879
|
-
return getResult;
|
|
880
|
-
}
|
|
881
|
-
const currentEntry = getResult.data;
|
|
882
|
-
// Step 4: Generate new entry key
|
|
883
|
-
const newEntryKey = this.crypto.randomBytes(32);
|
|
884
|
-
const newKeyId = hexEncode(this.crypto.sha256(newEntryKey)).slice(0, 16);
|
|
885
|
-
// Step 5: Re-serialize and re-encrypt value with new key
|
|
886
|
-
let plaintext;
|
|
887
|
-
if (currentEntry.value instanceof Uint8Array) {
|
|
888
|
-
plaintext = currentEntry.value;
|
|
889
|
-
}
|
|
890
|
-
else {
|
|
891
|
-
plaintext = toBytes(JSON.stringify(currentEntry.value));
|
|
892
|
-
}
|
|
893
|
-
const encrypted = this.crypto.encrypt(newEntryKey, plaintext);
|
|
894
|
-
// Step 6: Encrypt new entry key with master key
|
|
895
|
-
const newKeyBlob = this.crypto.encrypt(this.masterKey, newEntryKey);
|
|
896
|
-
// Step 7: Update metadata with new key ID
|
|
897
|
-
const metadata = {
|
|
898
|
-
...currentEntry.metadata,
|
|
899
|
-
[VaultHeaders.KEY_ID]: newKeyId,
|
|
900
|
-
};
|
|
901
|
-
// Step 8: Store updated key blob
|
|
902
|
-
const keyPayload = JSON.stringify({
|
|
903
|
-
key: base64Encode(newKeyBlob),
|
|
904
|
-
metadata: JSON.stringify({
|
|
905
|
-
keyId: newKeyId,
|
|
906
|
-
...metadata,
|
|
907
|
-
}),
|
|
908
|
-
});
|
|
909
|
-
const keyPutResult = await this.tc.kv.put(`keys/${key}`, keyPayload);
|
|
910
|
-
if (!keyPutResult.ok) {
|
|
911
|
-
return vaultError({
|
|
912
|
-
code: "STORAGE_ERROR",
|
|
913
|
-
cause: new Error(`Failed to store rotated key blob: ${keyPutResult.error.message}`),
|
|
914
|
-
});
|
|
915
|
-
}
|
|
916
|
-
// Step 9: Store re-encrypted value
|
|
917
|
-
const valuePayload = JSON.stringify({
|
|
918
|
-
data: base64Encode(encrypted),
|
|
919
|
-
metadata,
|
|
920
|
-
});
|
|
921
|
-
const valuePutResult = await this.tc.kv.put(`vault/${key}`, valuePayload);
|
|
922
|
-
if (!valuePutResult.ok) {
|
|
923
|
-
return vaultError({
|
|
924
|
-
code: "STORAGE_ERROR",
|
|
925
|
-
cause: new Error(`Failed to store re-encrypted value: ${valuePutResult.error.message}`),
|
|
926
|
-
});
|
|
927
|
-
}
|
|
928
|
-
// Step 10: Re-issue grants to remaining recipients
|
|
929
|
-
for (const did of remainingGrantees) {
|
|
930
|
-
const grantResult = await this.reencrypt(key, did);
|
|
931
|
-
if (!grantResult.ok) {
|
|
932
|
-
// Continue re-issuing to other recipients even if one fails
|
|
933
|
-
// The failed grant can be re-issued manually
|
|
934
|
-
}
|
|
935
|
-
}
|
|
936
|
-
return ok(undefined);
|
|
937
|
-
}
|
|
938
|
-
catch (error) {
|
|
939
|
-
return vaultError({
|
|
940
|
-
code: "STORAGE_ERROR",
|
|
941
|
-
cause: toError(error),
|
|
942
|
-
});
|
|
943
|
-
}
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
// =========================================================================
|
|
947
|
-
// Internal Helpers
|
|
948
|
-
// =========================================================================
|
|
949
|
-
/**
|
|
950
|
-
* Parse a DID string to extract address and chainId.
|
|
951
|
-
* Expected format: did:pkh:eip155:{chainId}:{address}
|
|
952
|
-
*
|
|
953
|
-
* @param did - The DID to parse
|
|
954
|
-
* @returns Parsed address and chainId, or null if invalid
|
|
955
|
-
*/
|
|
956
|
-
parseDID(did) {
|
|
957
|
-
// did:pkh:eip155:{chainId}:{address}
|
|
958
|
-
const parts = did.split(":");
|
|
959
|
-
if (parts.length !== 5 ||
|
|
960
|
-
parts[0] !== "did" ||
|
|
961
|
-
parts[1] !== "pkh" ||
|
|
962
|
-
parts[2] !== "eip155") {
|
|
963
|
-
return null;
|
|
964
|
-
}
|
|
965
|
-
const chainId = parseInt(parts[3], 10);
|
|
966
|
-
const address = parts[4];
|
|
967
|
-
if (isNaN(chainId) || !address) {
|
|
968
|
-
return null;
|
|
969
|
-
}
|
|
970
|
-
return { address, chainId };
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
/**
|
|
974
|
-
* Service identifier for registration.
|
|
975
|
-
*/
|
|
976
|
-
DataVaultService.serviceName = "vault";
|
|
977
|
-
//# sourceMappingURL=DataVaultService.js.map
|