@sovereignfs/sdk 1.25.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/LICENSE +661 -0
- package/dist/chunk-MLKGABMK.js +9 -0
- package/dist/chunk-NQLGPAL6.js +62 -0
- package/dist/e2ee-crypto.d.ts +84 -0
- package/dist/e2ee-crypto.js +162 -0
- package/dist/e2ee-device.d.ts +21 -0
- package/dist/e2ee-device.js +13 -0
- package/dist/e2ee-object.d.ts +50 -0
- package/dist/e2ee-object.js +75 -0
- package/dist/e2ee-state-U-GEJ7oL.d.ts +411 -0
- package/dist/e2ee-state.d.ts +1 -0
- package/dist/e2ee-state.js +36 -0
- package/dist/index.d.ts +543 -0
- package/dist/index.js +652 -0
- package/package.json +50 -0
- package/src/__tests__/e2ee-crypto.test.ts +156 -0
- package/src/__tests__/e2ee-object.test.ts +90 -0
- package/src/__tests__/e2ee-state.test.ts +83 -0
- package/src/__tests__/sdk.test.ts +427 -0
- package/src/__tests__/sessions.test.ts +63 -0
- package/src/activity.ts +19 -0
- package/src/auth.ts +134 -0
- package/src/connections.ts +106 -0
- package/src/data.ts +88 -0
- package/src/db.ts +34 -0
- package/src/directory.ts +31 -0
- package/src/e2ee-crypto.ts +260 -0
- package/src/e2ee-device.ts +75 -0
- package/src/e2ee-object.ts +118 -0
- package/src/e2ee-state.ts +72 -0
- package/src/e2ee.ts +65 -0
- package/src/env.ts +37 -0
- package/src/errors.ts +46 -0
- package/src/host.ts +251 -0
- package/src/index.ts +160 -0
- package/src/mailer.ts +7 -0
- package/src/notifications.ts +36 -0
- package/src/platform.ts +10 -0
- package/src/plugins.ts +146 -0
- package/src/portability.ts +176 -0
- package/src/secrets.ts +68 -0
- package/src/sessions.ts +36 -0
- package/src/storage.ts +49 -0
- package/src/types.ts +418 -0
- package/src/unimplemented.ts +48 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// src/e2ee-device.ts
|
|
2
|
+
var DB_NAME = "sovereign-e2ee";
|
|
3
|
+
var DB_VERSION = 1;
|
|
4
|
+
var STORE_NAME = "device-keys";
|
|
5
|
+
var DEVICE_ID_STORAGE_KEY = "sovereign:e2ee:deviceId";
|
|
6
|
+
function openDb() {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
9
|
+
request.onupgradeneeded = () => {
|
|
10
|
+
if (!request.result.objectStoreNames.contains(STORE_NAME)) {
|
|
11
|
+
request.result.createObjectStore(STORE_NAME);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
request.onsuccess = () => resolve(request.result);
|
|
15
|
+
request.onerror = () => reject(request.error ?? new Error("Failed to open IndexedDB."));
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
function getOrCreateDeviceId() {
|
|
19
|
+
const existing = localStorage.getItem(DEVICE_ID_STORAGE_KEY);
|
|
20
|
+
if (existing) return existing;
|
|
21
|
+
const id = crypto.randomUUID();
|
|
22
|
+
localStorage.setItem(DEVICE_ID_STORAGE_KEY, id);
|
|
23
|
+
return id;
|
|
24
|
+
}
|
|
25
|
+
async function storeDeviceKey(deviceId, key) {
|
|
26
|
+
const db = await openDb();
|
|
27
|
+
await new Promise((resolve, reject) => {
|
|
28
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
29
|
+
tx.objectStore(STORE_NAME).put(key, deviceId);
|
|
30
|
+
tx.oncomplete = () => resolve();
|
|
31
|
+
tx.onerror = () => reject(tx.error ?? new Error("Failed to store device key."));
|
|
32
|
+
});
|
|
33
|
+
db.close();
|
|
34
|
+
}
|
|
35
|
+
async function getDeviceKey(deviceId) {
|
|
36
|
+
const db = await openDb();
|
|
37
|
+
const result = await new Promise((resolve, reject) => {
|
|
38
|
+
const tx = db.transaction(STORE_NAME, "readonly");
|
|
39
|
+
const request = tx.objectStore(STORE_NAME).get(deviceId);
|
|
40
|
+
request.onsuccess = () => resolve(request.result ?? null);
|
|
41
|
+
request.onerror = () => reject(request.error ?? new Error("Failed to read device key."));
|
|
42
|
+
});
|
|
43
|
+
db.close();
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
async function forgetDeviceKey(deviceId) {
|
|
47
|
+
const db = await openDb();
|
|
48
|
+
await new Promise((resolve, reject) => {
|
|
49
|
+
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
50
|
+
tx.objectStore(STORE_NAME).delete(deviceId);
|
|
51
|
+
tx.oncomplete = () => resolve();
|
|
52
|
+
tx.onerror = () => reject(tx.error ?? new Error("Failed to remove device key."));
|
|
53
|
+
});
|
|
54
|
+
db.close();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export {
|
|
58
|
+
getOrCreateDeviceId,
|
|
59
|
+
storeDeviceKey,
|
|
60
|
+
getDeviceKey,
|
|
61
|
+
forgetDeviceKey
|
|
62
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side Client Master Key (CMK) crypto (RFC 0060). Pure WebCrypto —
|
|
3
|
+
* runs in the browser (or any environment with a global `crypto.subtle`,
|
|
4
|
+
* which includes Node for testing). No plaintext key material or wrapped
|
|
5
|
+
* bytes ever need to leave this module's caller before being sent to the
|
|
6
|
+
* server as opaque ciphertext.
|
|
7
|
+
*
|
|
8
|
+
* Resolves RFC 0060 open question 2 (browser KDF choice): PBKDF2-HMAC-SHA256
|
|
9
|
+
* via native WebCrypto, not Argon2id — Argon2id has no native WebCrypto
|
|
10
|
+
* primitive, and pulling in a WASM implementation is out of scope for this
|
|
11
|
+
* pass. `kdfAlgorithm`/`kdfParams` are stored per-wrapper specifically so a
|
|
12
|
+
* future Argon2id upgrade doesn't require a schema change, only a new
|
|
13
|
+
* `kdfAlgorithm` value and a migration path for existing wrappers.
|
|
14
|
+
*
|
|
15
|
+
* Scope: CMK generation, master-key wrap/unwrap (recovery secret, device
|
|
16
|
+
* key), and per-object Data Encryption Key (DEK) generation/wrap/unwrap.
|
|
17
|
+
* Generic Blob/JSON encrypt/decrypt helpers that use a DEK live in
|
|
18
|
+
* `e2ee-object.ts`; this module only handles key material.
|
|
19
|
+
*/
|
|
20
|
+
declare const CMK_ALGORITHM = "AES-GCM-256";
|
|
21
|
+
declare const KDF_ALGORITHM = "PBKDF2-SHA256";
|
|
22
|
+
declare const WRAP_ALGORITHM_VERSION = "v1";
|
|
23
|
+
/** Ciphertext + metadata needed to unwrap a CMK later. Safe to store server-side as-is. */
|
|
24
|
+
interface WrappedCmk {
|
|
25
|
+
/** Base64url `iv || ciphertext`. */
|
|
26
|
+
wrappedCmk: string;
|
|
27
|
+
algorithmVersion: string;
|
|
28
|
+
}
|
|
29
|
+
/** A `WrappedCmk` plus the KDF metadata needed to re-derive the wrapping key from a secret. */
|
|
30
|
+
interface RecoveryWrappedCmk extends WrappedCmk {
|
|
31
|
+
kdfAlgorithm: string;
|
|
32
|
+
/** JSON-encoded KDF parameters. */
|
|
33
|
+
kdfParams: string;
|
|
34
|
+
kdfSalt: string;
|
|
35
|
+
}
|
|
36
|
+
/** Ciphertext + metadata needed to unwrap a per-object DEK later. Safe to store server-side as-is. */
|
|
37
|
+
interface WrappedDek {
|
|
38
|
+
/** Base64url `iv || ciphertext`. */
|
|
39
|
+
wrappedDek: string;
|
|
40
|
+
algorithmVersion: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A high-entropy, human-recordable recovery secret. Grouped base32
|
|
44
|
+
* (Crockford-ish alphabet, ambiguous characters removed) rather than a
|
|
45
|
+
* BIP39 word list — no wordlist dependency, ~160 bits of entropy in 32
|
|
46
|
+
* displayed characters across 6 groups (e.g. `AB3DE-FGH2J-...`).
|
|
47
|
+
*/
|
|
48
|
+
declare function generateRecoverySecret(): string;
|
|
49
|
+
/**
|
|
50
|
+
* Generate a new, extractable Client Master Key. Includes `wrapKey`/
|
|
51
|
+
* `unwrapKey` usages (not just `encrypt`/`decrypt`) since the CMK wraps
|
|
52
|
+
* per-object DEKs (`wrapDekWithCmk`/`unwrapDekWithCmk` below) in addition to
|
|
53
|
+
* being itself wrapped by a recovery secret or device key.
|
|
54
|
+
*/
|
|
55
|
+
declare function generateCmk(): Promise<CryptoKey>;
|
|
56
|
+
/** Wrap the CMK with a key derived from the user's recovery secret. */
|
|
57
|
+
declare function wrapCmkWithRecoverySecret(cmk: CryptoKey, recoverySecret: string): Promise<RecoveryWrappedCmk>;
|
|
58
|
+
/** Unwrap the CMK given the recovery secret. Throws if the secret is wrong. */
|
|
59
|
+
declare function unwrapCmkWithRecoverySecret(wrapped: RecoveryWrappedCmk, recoverySecret: string): Promise<CryptoKey>;
|
|
60
|
+
/**
|
|
61
|
+
* Generate a device-local wrapping key. Non-extractable — once created, no
|
|
62
|
+
* script (including a compromised page on this origin) can ever read its raw
|
|
63
|
+
* bytes back out via `exportKey`; it can only be used, via its `CryptoKey`
|
|
64
|
+
* handle, for wrap/unwrap. Must be persisted with IndexedDB (the only Web
|
|
65
|
+
* storage API that can hold a live `CryptoKey`) — see `e2ee-device.ts`.
|
|
66
|
+
*/
|
|
67
|
+
declare function generateDeviceKey(): Promise<CryptoKey>;
|
|
68
|
+
/** Wrap the CMK with this device's own key. */
|
|
69
|
+
declare function wrapCmkWithDeviceKey(cmk: CryptoKey, deviceKey: CryptoKey): Promise<WrappedCmk>;
|
|
70
|
+
/** Unwrap the CMK using this device's own key. */
|
|
71
|
+
declare function unwrapCmkWithDeviceKey(wrapped: WrappedCmk, deviceKey: CryptoKey): Promise<CryptoKey>;
|
|
72
|
+
/**
|
|
73
|
+
* Generate a new, extractable per-object Data Encryption Key (DEK). One DEK
|
|
74
|
+
* per encrypted object/document — never reused across objects — so
|
|
75
|
+
* compromising a single object's key never exposes any other object (RFC
|
|
76
|
+
* 0060 key hierarchy: CMK wraps DEKs, DEKs encrypt object content).
|
|
77
|
+
*/
|
|
78
|
+
declare function generateDek(): Promise<CryptoKey>;
|
|
79
|
+
/** Wrap a per-object DEK with the (unlocked) CMK. Safe to store server-side as-is. */
|
|
80
|
+
declare function wrapDekWithCmk(dek: CryptoKey, cmk: CryptoKey): Promise<WrappedDek>;
|
|
81
|
+
/** Unwrap a per-object DEK using the (unlocked) CMK. Throws if the CMK doesn't match. */
|
|
82
|
+
declare function unwrapDekWithCmk(wrapped: WrappedDek, cmk: CryptoKey): Promise<CryptoKey>;
|
|
83
|
+
|
|
84
|
+
export { CMK_ALGORITHM, KDF_ALGORITHM, type RecoveryWrappedCmk, WRAP_ALGORITHM_VERSION, type WrappedCmk, type WrappedDek, generateCmk, generateDek, generateDeviceKey, generateRecoverySecret, unwrapCmkWithDeviceKey, unwrapCmkWithRecoverySecret, unwrapDekWithCmk, wrapCmkWithDeviceKey, wrapCmkWithRecoverySecret, wrapDekWithCmk };
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import "./chunk-MLKGABMK.js";
|
|
2
|
+
|
|
3
|
+
// src/e2ee-crypto.ts
|
|
4
|
+
var AES_GCM_KEY_LENGTH = 256;
|
|
5
|
+
var AES_GCM_IV_BYTES = 12;
|
|
6
|
+
var PBKDF2_SALT_BYTES = 16;
|
|
7
|
+
var PBKDF2_ITERATIONS = 6e5;
|
|
8
|
+
var RECOVERY_SECRET_ENTROPY_BYTES = 20;
|
|
9
|
+
var CMK_ALGORITHM = "AES-GCM-256";
|
|
10
|
+
var KDF_ALGORITHM = "PBKDF2-SHA256";
|
|
11
|
+
var WRAP_ALGORITHM_VERSION = "v1";
|
|
12
|
+
var CMK_KEY_USAGES = ["encrypt", "decrypt", "wrapKey", "unwrapKey"];
|
|
13
|
+
var DEK_KEY_USAGES = ["encrypt", "decrypt"];
|
|
14
|
+
function toBase64Url(bytes) {
|
|
15
|
+
const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
16
|
+
let binary = "";
|
|
17
|
+
for (const byte of arr) binary += String.fromCharCode(byte);
|
|
18
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
19
|
+
}
|
|
20
|
+
function fromBase64Url(value) {
|
|
21
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
22
|
+
const withPadding = padded + "=".repeat((4 - padded.length % 4) % 4);
|
|
23
|
+
const binary = atob(withPadding);
|
|
24
|
+
const bytes = new Uint8Array(binary.length);
|
|
25
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
26
|
+
return bytes;
|
|
27
|
+
}
|
|
28
|
+
function concatBytes(a, b) {
|
|
29
|
+
const out = new Uint8Array(a.length + b.length);
|
|
30
|
+
out.set(a, 0);
|
|
31
|
+
out.set(b, a.length);
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
function generateRecoverySecret() {
|
|
35
|
+
const alphabet = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
|
|
36
|
+
const bytes = crypto.getRandomValues(new Uint8Array(RECOVERY_SECRET_ENTROPY_BYTES));
|
|
37
|
+
let out = "";
|
|
38
|
+
for (const byte of bytes) out += alphabet[byte % alphabet.length];
|
|
39
|
+
return out.match(/.{1,5}/g)?.join("-") ?? out;
|
|
40
|
+
}
|
|
41
|
+
function generateCmk() {
|
|
42
|
+
return crypto.subtle.generateKey(
|
|
43
|
+
{ name: "AES-GCM", length: AES_GCM_KEY_LENGTH },
|
|
44
|
+
true,
|
|
45
|
+
CMK_KEY_USAGES
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
async function deriveWrappingKeyFromSecret(secret, salt) {
|
|
49
|
+
const keyMaterial = await crypto.subtle.importKey(
|
|
50
|
+
"raw",
|
|
51
|
+
new TextEncoder().encode(secret.normalize("NFKC")),
|
|
52
|
+
"PBKDF2",
|
|
53
|
+
false,
|
|
54
|
+
["deriveKey"]
|
|
55
|
+
);
|
|
56
|
+
return crypto.subtle.deriveKey(
|
|
57
|
+
{ name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
|
|
58
|
+
keyMaterial,
|
|
59
|
+
{ name: "AES-GCM", length: AES_GCM_KEY_LENGTH },
|
|
60
|
+
false,
|
|
61
|
+
["wrapKey", "unwrapKey"]
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
async function wrapCmkWithRecoverySecret(cmk, recoverySecret) {
|
|
65
|
+
const salt = crypto.getRandomValues(new Uint8Array(PBKDF2_SALT_BYTES));
|
|
66
|
+
const wrappingKey = await deriveWrappingKeyFromSecret(recoverySecret, salt);
|
|
67
|
+
const iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES));
|
|
68
|
+
const wrapped = await crypto.subtle.wrapKey("raw", cmk, wrappingKey, {
|
|
69
|
+
name: "AES-GCM",
|
|
70
|
+
iv
|
|
71
|
+
});
|
|
72
|
+
return {
|
|
73
|
+
wrappedCmk: toBase64Url(concatBytes(iv, new Uint8Array(wrapped))),
|
|
74
|
+
kdfAlgorithm: KDF_ALGORITHM,
|
|
75
|
+
kdfParams: JSON.stringify({ iterations: PBKDF2_ITERATIONS, hash: "SHA-256" }),
|
|
76
|
+
kdfSalt: toBase64Url(salt),
|
|
77
|
+
algorithmVersion: WRAP_ALGORITHM_VERSION
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function unwrapCmkWithRecoverySecret(wrapped, recoverySecret) {
|
|
81
|
+
const salt = fromBase64Url(wrapped.kdfSalt);
|
|
82
|
+
const wrappingKey = await deriveWrappingKeyFromSecret(recoverySecret, salt);
|
|
83
|
+
const combined = fromBase64Url(wrapped.wrappedCmk);
|
|
84
|
+
const iv = combined.slice(0, AES_GCM_IV_BYTES);
|
|
85
|
+
const ciphertext = combined.slice(AES_GCM_IV_BYTES);
|
|
86
|
+
return crypto.subtle.unwrapKey(
|
|
87
|
+
"raw",
|
|
88
|
+
ciphertext,
|
|
89
|
+
wrappingKey,
|
|
90
|
+
{ name: "AES-GCM", iv },
|
|
91
|
+
{ name: "AES-GCM", length: AES_GCM_KEY_LENGTH },
|
|
92
|
+
true,
|
|
93
|
+
CMK_KEY_USAGES
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
function generateDeviceKey() {
|
|
97
|
+
return crypto.subtle.generateKey({ name: "AES-GCM", length: AES_GCM_KEY_LENGTH }, false, [
|
|
98
|
+
"wrapKey",
|
|
99
|
+
"unwrapKey"
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
async function wrapKeyWith(key, wrappingKey) {
|
|
103
|
+
const iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES));
|
|
104
|
+
const wrapped = await crypto.subtle.wrapKey("raw", key, wrappingKey, {
|
|
105
|
+
name: "AES-GCM",
|
|
106
|
+
iv
|
|
107
|
+
});
|
|
108
|
+
return {
|
|
109
|
+
value: toBase64Url(concatBytes(iv, new Uint8Array(wrapped))),
|
|
110
|
+
algorithmVersion: WRAP_ALGORITHM_VERSION
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async function unwrapKeyWith(wrappedValue, wrappingKey, extractable, usages) {
|
|
114
|
+
const combined = fromBase64Url(wrappedValue);
|
|
115
|
+
const iv = combined.slice(0, AES_GCM_IV_BYTES);
|
|
116
|
+
const ciphertext = combined.slice(AES_GCM_IV_BYTES);
|
|
117
|
+
return crypto.subtle.unwrapKey(
|
|
118
|
+
"raw",
|
|
119
|
+
ciphertext,
|
|
120
|
+
wrappingKey,
|
|
121
|
+
{ name: "AES-GCM", iv },
|
|
122
|
+
{ name: "AES-GCM", length: AES_GCM_KEY_LENGTH },
|
|
123
|
+
extractable,
|
|
124
|
+
usages
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
async function wrapCmkWithDeviceKey(cmk, deviceKey) {
|
|
128
|
+
const { value, algorithmVersion } = await wrapKeyWith(cmk, deviceKey);
|
|
129
|
+
return { wrappedCmk: value, algorithmVersion };
|
|
130
|
+
}
|
|
131
|
+
function unwrapCmkWithDeviceKey(wrapped, deviceKey) {
|
|
132
|
+
return unwrapKeyWith(wrapped.wrappedCmk, deviceKey, true, CMK_KEY_USAGES);
|
|
133
|
+
}
|
|
134
|
+
function generateDek() {
|
|
135
|
+
return crypto.subtle.generateKey(
|
|
136
|
+
{ name: "AES-GCM", length: AES_GCM_KEY_LENGTH },
|
|
137
|
+
true,
|
|
138
|
+
DEK_KEY_USAGES
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
async function wrapDekWithCmk(dek, cmk) {
|
|
142
|
+
const { value, algorithmVersion } = await wrapKeyWith(dek, cmk);
|
|
143
|
+
return { wrappedDek: value, algorithmVersion };
|
|
144
|
+
}
|
|
145
|
+
function unwrapDekWithCmk(wrapped, cmk) {
|
|
146
|
+
return unwrapKeyWith(wrapped.wrappedDek, cmk, true, DEK_KEY_USAGES);
|
|
147
|
+
}
|
|
148
|
+
export {
|
|
149
|
+
CMK_ALGORITHM,
|
|
150
|
+
KDF_ALGORITHM,
|
|
151
|
+
WRAP_ALGORITHM_VERSION,
|
|
152
|
+
generateCmk,
|
|
153
|
+
generateDek,
|
|
154
|
+
generateDeviceKey,
|
|
155
|
+
generateRecoverySecret,
|
|
156
|
+
unwrapCmkWithDeviceKey,
|
|
157
|
+
unwrapCmkWithRecoverySecret,
|
|
158
|
+
unwrapDekWithCmk,
|
|
159
|
+
wrapCmkWithDeviceKey,
|
|
160
|
+
wrapCmkWithRecoverySecret,
|
|
161
|
+
wrapDekWithCmk
|
|
162
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser-only persistence for this device's local E2EE wrapping key
|
|
3
|
+
* (RFC 0060). The key itself is generated non-extractable
|
|
4
|
+
* (`e2ee-crypto.ts#generateDeviceKey`) so it must be persisted via
|
|
5
|
+
* IndexedDB — the only Web storage API that can hold a live `CryptoKey`
|
|
6
|
+
* across page loads (`localStorage`/cookies cannot serialize one).
|
|
7
|
+
*
|
|
8
|
+
* The device id is a random UUID, not sensitive on its own (it is just a
|
|
9
|
+
* label distinguishing this browser/device's enrollment row from others) and
|
|
10
|
+
* is stored in `localStorage` alongside the IndexedDB key entry.
|
|
11
|
+
*/
|
|
12
|
+
/** This browser/device's stable id — generated once, persisted in `localStorage`. */
|
|
13
|
+
declare function getOrCreateDeviceId(): string;
|
|
14
|
+
/** Persist this device's local wrapping key, keyed by device id. */
|
|
15
|
+
declare function storeDeviceKey(deviceId: string, key: CryptoKey): Promise<void>;
|
|
16
|
+
/** Read this device's local wrapping key, or `null` if never enrolled on this device/browser. */
|
|
17
|
+
declare function getDeviceKey(deviceId: string): Promise<CryptoKey | null>;
|
|
18
|
+
/** Remove this device's local wrapping key (e.g. after the user revokes it from another device). */
|
|
19
|
+
declare function forgetDeviceKey(deviceId: string): Promise<void>;
|
|
20
|
+
|
|
21
|
+
export { forgetDeviceKey, getDeviceKey, getOrCreateDeviceId, storeDeviceKey };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encrypt/decrypt helpers for plugin object content under a per-object Data
|
|
3
|
+
* Encryption Key (DEK) (RFC 0060 step 4). Pure WebCrypto, browser-only in
|
|
4
|
+
* practice (also runs in Node for testing). Callers are responsible for
|
|
5
|
+
* generating the DEK (`generateDek` in `e2ee-crypto.ts`) and wrapping it
|
|
6
|
+
* under the unlocked CMK (`wrapDekWithCmk`) before storing the wrapped DEK
|
|
7
|
+
* alongside the ciphertext this module produces — this module only ever
|
|
8
|
+
* touches the unwrapped DEK, never the CMK.
|
|
9
|
+
*
|
|
10
|
+
* Two shapes are covered, matching RFC 0060's plaintext/encrypted metadata
|
|
11
|
+
* split:
|
|
12
|
+
* - `encryptBlob`/`decryptBlob` — binary payloads (document images, etc.)
|
|
13
|
+
* meant to travel through `sdk.storage` as opaque ciphertext.
|
|
14
|
+
* - `encryptJson`/`decryptJson` — the "encrypted metadata" half of an
|
|
15
|
+
* object's fields (human-readable title, notes, document number, etc.),
|
|
16
|
+
* as opposed to the plaintext routing metadata (object id, plugin id,
|
|
17
|
+
* storage key) plugins keep server-visible.
|
|
18
|
+
*
|
|
19
|
+
* Each result carries its own IV and `algorithmVersion` so a future cipher
|
|
20
|
+
* change is versioned per-object, not global — mirrors the wrapped-key shapes
|
|
21
|
+
* in `e2ee-crypto.ts`.
|
|
22
|
+
*/
|
|
23
|
+
declare const OBJECT_ALGORITHM_VERSION = "v1";
|
|
24
|
+
/** Encrypted binary payload — the `ciphertext` Blob is safe to upload as opaque bytes. */
|
|
25
|
+
interface EncryptedBlob {
|
|
26
|
+
ciphertext: Blob;
|
|
27
|
+
/** Base64url IV used for this encryption. */
|
|
28
|
+
iv: string;
|
|
29
|
+
algorithmVersion: string;
|
|
30
|
+
/** The original `Blob.type`, needed to reconstruct it on decrypt. */
|
|
31
|
+
contentType: string;
|
|
32
|
+
}
|
|
33
|
+
/** Encrypted JSON metadata — safe to store server-side as opaque ciphertext. */
|
|
34
|
+
interface EncryptedJson {
|
|
35
|
+
/** Base64url ciphertext of the UTF-8 JSON-serialized value. */
|
|
36
|
+
ciphertext: string;
|
|
37
|
+
/** Base64url IV used for this encryption. */
|
|
38
|
+
iv: string;
|
|
39
|
+
algorithmVersion: string;
|
|
40
|
+
}
|
|
41
|
+
/** Encrypt a `Blob` (e.g. a document image) under the given DEK. */
|
|
42
|
+
declare function encryptBlob(dek: CryptoKey, blob: Blob): Promise<EncryptedBlob>;
|
|
43
|
+
/** Decrypt a `Blob` previously produced by `encryptBlob` with the matching DEK. */
|
|
44
|
+
declare function decryptBlob(dek: CryptoKey, encrypted: EncryptedBlob): Promise<Blob>;
|
|
45
|
+
/** Encrypt a JSON-serializable value (e.g. an object's human-readable metadata) under the given DEK. */
|
|
46
|
+
declare function encryptJson<T>(dek: CryptoKey, value: T): Promise<EncryptedJson>;
|
|
47
|
+
/** Decrypt a JSON value previously produced by `encryptJson` with the matching DEK. */
|
|
48
|
+
declare function decryptJson<T>(dek: CryptoKey, encrypted: EncryptedJson): Promise<T>;
|
|
49
|
+
|
|
50
|
+
export { type EncryptedBlob, type EncryptedJson, OBJECT_ALGORITHM_VERSION, decryptBlob, decryptJson, encryptBlob, encryptJson };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import "./chunk-MLKGABMK.js";
|
|
2
|
+
|
|
3
|
+
// src/e2ee-object.ts
|
|
4
|
+
var AES_GCM_IV_BYTES = 12;
|
|
5
|
+
var OBJECT_ALGORITHM_VERSION = "v1";
|
|
6
|
+
function toBase64Url(bytes) {
|
|
7
|
+
const arr = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
8
|
+
let binary = "";
|
|
9
|
+
for (const byte of arr) binary += String.fromCharCode(byte);
|
|
10
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
11
|
+
}
|
|
12
|
+
function fromBase64Url(value) {
|
|
13
|
+
const padded = value.replace(/-/g, "+").replace(/_/g, "/");
|
|
14
|
+
const withPadding = padded + "=".repeat((4 - padded.length % 4) % 4);
|
|
15
|
+
const binary = atob(withPadding);
|
|
16
|
+
const bytes = new Uint8Array(binary.length);
|
|
17
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
18
|
+
return bytes;
|
|
19
|
+
}
|
|
20
|
+
async function encryptBlob(dek, blob) {
|
|
21
|
+
const iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES));
|
|
22
|
+
const plaintext = await blob.arrayBuffer();
|
|
23
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
24
|
+
{ name: "AES-GCM", iv },
|
|
25
|
+
dek,
|
|
26
|
+
plaintext
|
|
27
|
+
);
|
|
28
|
+
return {
|
|
29
|
+
ciphertext: new Blob([ciphertext], { type: "application/octet-stream" }),
|
|
30
|
+
iv: toBase64Url(iv),
|
|
31
|
+
algorithmVersion: OBJECT_ALGORITHM_VERSION,
|
|
32
|
+
contentType: blob.type
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function decryptBlob(dek, encrypted) {
|
|
36
|
+
const iv = fromBase64Url(encrypted.iv);
|
|
37
|
+
const ciphertext = await encrypted.ciphertext.arrayBuffer();
|
|
38
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
39
|
+
{ name: "AES-GCM", iv },
|
|
40
|
+
dek,
|
|
41
|
+
ciphertext
|
|
42
|
+
);
|
|
43
|
+
return new Blob([plaintext], { type: encrypted.contentType });
|
|
44
|
+
}
|
|
45
|
+
async function encryptJson(dek, value) {
|
|
46
|
+
const iv = crypto.getRandomValues(new Uint8Array(AES_GCM_IV_BYTES));
|
|
47
|
+
const plaintext = new TextEncoder().encode(JSON.stringify(value));
|
|
48
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
49
|
+
{ name: "AES-GCM", iv },
|
|
50
|
+
dek,
|
|
51
|
+
plaintext
|
|
52
|
+
);
|
|
53
|
+
return {
|
|
54
|
+
ciphertext: toBase64Url(ciphertext),
|
|
55
|
+
iv: toBase64Url(iv),
|
|
56
|
+
algorithmVersion: OBJECT_ALGORITHM_VERSION
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async function decryptJson(dek, encrypted) {
|
|
60
|
+
const iv = fromBase64Url(encrypted.iv);
|
|
61
|
+
const ciphertext = fromBase64Url(encrypted.ciphertext);
|
|
62
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
63
|
+
{ name: "AES-GCM", iv },
|
|
64
|
+
dek,
|
|
65
|
+
ciphertext
|
|
66
|
+
);
|
|
67
|
+
return JSON.parse(new TextDecoder().decode(plaintext));
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
OBJECT_ALGORITHM_VERSION,
|
|
71
|
+
decryptBlob,
|
|
72
|
+
decryptJson,
|
|
73
|
+
encryptBlob,
|
|
74
|
+
encryptJson
|
|
75
|
+
};
|