@sovereignfs/sdk 1.25.0 → 2.0.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.
Files changed (55) hide show
  1. package/LICENSE +373 -661
  2. package/README.md +97 -0
  3. package/dist/browser.d.ts +2 -0
  4. package/dist/browser.js +4 -0
  5. package/dist/chunk-NYL4LLXF.js +65 -0
  6. package/dist/index.d.ts +57 -529
  7. package/dist/index.js +1283 -572
  8. package/dist/next.d.ts +37 -0
  9. package/dist/next.js +68 -0
  10. package/dist/session-DJl2XYMc.d.ts +44 -0
  11. package/dist/sovereign-CazGKEVc.d.ts +517 -0
  12. package/dist/testing.d.ts +181 -0
  13. package/dist/testing.js +560 -0
  14. package/package.json +53 -27
  15. package/dist/chunk-MLKGABMK.js +0 -9
  16. package/dist/chunk-NQLGPAL6.js +0 -62
  17. package/dist/e2ee-crypto.d.ts +0 -84
  18. package/dist/e2ee-crypto.js +0 -162
  19. package/dist/e2ee-device.d.ts +0 -21
  20. package/dist/e2ee-device.js +0 -13
  21. package/dist/e2ee-object.d.ts +0 -50
  22. package/dist/e2ee-object.js +0 -75
  23. package/dist/e2ee-state-U-GEJ7oL.d.ts +0 -411
  24. package/dist/e2ee-state.d.ts +0 -1
  25. package/dist/e2ee-state.js +0 -36
  26. package/src/__tests__/e2ee-crypto.test.ts +0 -156
  27. package/src/__tests__/e2ee-object.test.ts +0 -90
  28. package/src/__tests__/e2ee-state.test.ts +0 -83
  29. package/src/__tests__/sdk.test.ts +0 -427
  30. package/src/__tests__/sessions.test.ts +0 -63
  31. package/src/activity.ts +0 -19
  32. package/src/auth.ts +0 -134
  33. package/src/connections.ts +0 -106
  34. package/src/data.ts +0 -88
  35. package/src/db.ts +0 -34
  36. package/src/directory.ts +0 -31
  37. package/src/e2ee-crypto.ts +0 -260
  38. package/src/e2ee-device.ts +0 -75
  39. package/src/e2ee-object.ts +0 -118
  40. package/src/e2ee-state.ts +0 -72
  41. package/src/e2ee.ts +0 -65
  42. package/src/env.ts +0 -37
  43. package/src/errors.ts +0 -46
  44. package/src/host.ts +0 -251
  45. package/src/index.ts +0 -160
  46. package/src/mailer.ts +0 -7
  47. package/src/notifications.ts +0 -36
  48. package/src/platform.ts +0 -10
  49. package/src/plugins.ts +0 -146
  50. package/src/portability.ts +0 -176
  51. package/src/secrets.ts +0 -68
  52. package/src/sessions.ts +0 -36
  53. package/src/storage.ts +0 -49
  54. package/src/types.ts +0 -418
  55. package/src/unimplemented.ts +0 -48
@@ -1,62 +0,0 @@
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
- };
@@ -1,84 +0,0 @@
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 };
@@ -1,162 +0,0 @@
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
- };
@@ -1,21 +0,0 @@
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 };
@@ -1,13 +0,0 @@
1
- import {
2
- forgetDeviceKey,
3
- getDeviceKey,
4
- getOrCreateDeviceId,
5
- storeDeviceKey
6
- } from "./chunk-NQLGPAL6.js";
7
- import "./chunk-MLKGABMK.js";
8
- export {
9
- forgetDeviceKey,
10
- getDeviceKey,
11
- getOrCreateDeviceId,
12
- storeDeviceKey
13
- };
@@ -1,50 +0,0 @@
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 };
@@ -1,75 +0,0 @@
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
- };