@codesense/conseal 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,144 @@
1
+ <p align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="docs/assets/logo-dark.svg">
4
+ <img src="docs/assets/logo-light.svg" width="64" height="64" alt="Conseal logo">
5
+ </picture>
6
+ </p>
7
+
8
+ <h1 align="center">Conseal</h1>
9
+
10
+ <p align="center">
11
+ Browser-side zero-knowledge cryptography library.<br>
12
+ All crypto runs in the browser via <a href="https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto">SubtleCrypto</a> — the server never sees plaintext or key material.
13
+ </p>
14
+
15
+ ---
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install conseal
21
+ ```
22
+
23
+ ## Quick start
24
+
25
+ ```ts
26
+ import { seal, unseal, generateAesKey } from 'conseal'
27
+
28
+ // Generate a key and encrypt
29
+ const key = await generateAesKey()
30
+ const plaintext = await file.arrayBuffer()
31
+ const { ciphertext, iv } = await seal(key, plaintext)
32
+
33
+ // Decrypt
34
+ const result = await unseal(key, ciphertext, iv)
35
+ ```
36
+
37
+ ## API
38
+
39
+ ### Symmetric encryption (AES-256-GCM)
40
+
41
+ | Function | Description |
42
+ |---|---|
43
+ | `seal(key, plaintext)` | Encrypts with a random 96-bit IV. Returns `{ ciphertext, iv }`. |
44
+ | `unseal(key, ciphertext, iv)` | Decrypts. Throws on tampered data. |
45
+ | `generateAesKey(extractable?)` | Generates a random AES-256 key. |
46
+ | `importAesKey(raw, extractable?)` | Imports raw key bytes as a CryptoKey. |
47
+
48
+ ### Passphrase key wrapping (PBKDF2 + AES-KW)
49
+
50
+ | Function | Description |
51
+ |---|---|
52
+ | `wrapKey(passphrase, key)` | Wraps a CryptoKey with a passphrase. Returns `{ wrappedKey, salt }`. |
53
+ | `unwrapKey(passphrase, wrappedKey, salt)` | Unwraps. Throws on wrong passphrase. |
54
+ | `rekey(oldPass, newPass, wrappedKey, salt)` | Changes passphrase without re-encrypting data. |
55
+
56
+ ### Asymmetric encryption (ECDH P-256)
57
+
58
+ | Function | Description |
59
+ |---|---|
60
+ | `sealMessage(recipientPublicKey, plaintext)` | Encrypts for a recipient using ephemeral ECDH. |
61
+ | `unsealMessage(privateKey, ciphertext, iv, ephemeralPublicKey)` | Decrypts with the recipient's private key. |
62
+ | `generateECDHKeyPair()` | Generates a P-256 ECDH key pair. |
63
+
64
+ ### Digital signatures (ECDSA P-256)
65
+
66
+ | Function | Description |
67
+ |---|---|
68
+ | `sign(privateKey, data)` | Signs data with ECDSA-SHA256. |
69
+ | `verify(publicKey, signature, data)` | Verifies a signature. Returns `true` or `false`. |
70
+ | `generateECDSAKeyPair()` | Generates a P-256 ECDSA key pair. |
71
+
72
+ ### Envelope encryption (passcode-protected)
73
+
74
+ | Function | Description |
75
+ |---|---|
76
+ | `sealEnvelope(plaintext, passcode)` | Encrypts for a recipient without a Conseal account. |
77
+ | `unsealEnvelope(envelope, passcode)` | Decrypts with the passcode. |
78
+ | `encodeEnvelope(envelope)` | Serialises a `SealedEnvelope` to JSON. |
79
+ | `decodeEnvelope(json)` | Deserialises JSON back to a `SealedEnvelope`. |
80
+
81
+ ### Device initialisation
82
+
83
+ | Function | Description |
84
+ |---|---|
85
+ | `init(wrappedKey, salt, passphrase)` | Unwraps the AEK and stores it in IndexedDB. |
86
+ | `AEK_KEY_ID` | The IndexedDB key id for the AEK (`'aek'`). |
87
+
88
+ ### Mnemonic recovery (BIP-39)
89
+
90
+ | Function | Description |
91
+ |---|---|
92
+ | `generateMnemonic()` | Generates a 24-word recovery phrase. |
93
+ | `recoverWithMnemonic(mnemonic)` | Derives the AEK from the mnemonic. |
94
+
95
+ ### Key serialisation (JWK)
96
+
97
+ | Function | Description |
98
+ |---|---|
99
+ | `exportPublicKeyAsJwk(key)` | Exports a public CryptoKey to JWK. |
100
+ | `importPublicKeyFromJwk(jwk, algorithm)` | Imports a JWK as a CryptoKey (`'ECDH'` or `'ECDSA'`). |
101
+
102
+ ### IndexedDB key storage
103
+
104
+ ```ts
105
+ import { save, load, remove } from 'conseal/storage'
106
+ ```
107
+
108
+ | Function | Description |
109
+ |---|---|
110
+ | `save(name, key)` | Persists a CryptoKey to IndexedDB. |
111
+ | `load(name)` | Loads a CryptoKey. Returns `null` if not found. |
112
+ | `remove(name)` | Deletes a CryptoKey. |
113
+
114
+ ### Utilities
115
+
116
+ | Function | Description |
117
+ |---|---|
118
+ | `toBase64(buf)` / `fromBase64(b64)` | Standard base64 encoding/decoding. |
119
+ | `toBase64Url(buf)` / `fromBase64Url(b64)` | URL-safe base64 (no padding). |
120
+ | `digest(data)` | SHA-256 hash. |
121
+
122
+ ## Design
123
+
124
+ - **Zero runtime secrets on the server.** All encryption and decryption happens in the browser. The server stores only wrapped keys and ciphertext.
125
+ - **SubtleCrypto everywhere.** No OpenSSL, no polyfills, no WASM. The only runtime dependency is [`@scure/bip39`](https://github.com/nicolo-ribaudo/noble-bip39) for mnemonic wordlists (bundled into the output).
126
+ - **Non-extractable keys.** Keys stored in IndexedDB have `extractable: false` — JavaScript cannot read the raw bytes, only use them for encrypt/decrypt.
127
+ - **PBKDF2 at 600,000 iterations.** Passphrase-derived keys use SHA-256 with a 128-bit random salt per wrap. Intentionally slow to resist offline brute-force.
128
+
129
+ ## Requirements
130
+
131
+ - Browser with [SubtleCrypto](https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto) support (all modern browsers)
132
+ - Node.js >= 18 (for testing / SSR with `globalThis.crypto`)
133
+
134
+ ## Development
135
+
136
+ ```bash
137
+ npm install
138
+ npm test # run tests
139
+ npm run build # build to dist/
140
+ ```
141
+
142
+ ## License
143
+
144
+ Dual-licensed under [AGPL-3.0](LICENSE) and a [commercial license](COMMERCIAL-LICENSE.md).
package/dist/.gitkeep ADDED
File without changes
@@ -0,0 +1,63 @@
1
+ // src/storage.ts
2
+ var DB_NAME = "conseal-keys";
3
+ var STORE = "keys";
4
+ var VERSION = 1;
5
+ function openDb() {
6
+ return new Promise((resolve, reject) => {
7
+ const req = indexedDB.open(DB_NAME, VERSION);
8
+ req.onupgradeneeded = () => {
9
+ req.result.createObjectStore(STORE);
10
+ };
11
+ req.onsuccess = () => resolve(req.result);
12
+ req.onerror = () => reject(req.error);
13
+ });
14
+ }
15
+ async function save(name, key) {
16
+ const db = await openDb();
17
+ try {
18
+ return await new Promise((resolve, reject) => {
19
+ const tx = db.transaction(STORE, "readwrite");
20
+ tx.objectStore(STORE).put(key, name);
21
+ tx.oncomplete = () => resolve();
22
+ tx.onerror = () => reject(tx.error);
23
+ });
24
+ } finally {
25
+ db.close();
26
+ }
27
+ }
28
+ async function load(name) {
29
+ const db = await openDb();
30
+ try {
31
+ return await new Promise((resolve, reject) => {
32
+ const tx = db.transaction(STORE, "readonly");
33
+ const req = tx.objectStore(STORE).get(name);
34
+ let result = null;
35
+ req.onsuccess = () => {
36
+ result = req.result ?? null;
37
+ };
38
+ tx.oncomplete = () => resolve(result);
39
+ tx.onerror = () => reject(tx.error);
40
+ });
41
+ } finally {
42
+ db.close();
43
+ }
44
+ }
45
+ async function remove(name) {
46
+ const db = await openDb();
47
+ try {
48
+ return await new Promise((resolve, reject) => {
49
+ const tx = db.transaction(STORE, "readwrite");
50
+ tx.objectStore(STORE).delete(name);
51
+ tx.oncomplete = () => resolve();
52
+ tx.onerror = () => reject(tx.error);
53
+ });
54
+ } finally {
55
+ db.close();
56
+ }
57
+ }
58
+
59
+ export {
60
+ save,
61
+ load,
62
+ remove
63
+ };
@@ -0,0 +1,171 @@
1
+ export { load, remove, save } from './storage.js';
2
+
3
+ /**
4
+ * AES-256-GCM symmetric encryption.
5
+ *
6
+ * seal() encrypts an ArrayBuffer with a CryptoKey, returning ciphertext + a
7
+ * random 96-bit IV. The 128-bit authentication tag is appended to the
8
+ * ciphertext by SubtleCrypto automatically.
9
+ *
10
+ * unseal() decrypts ciphertext given the same key and IV. Throws if the
11
+ * authentication tag fails — any tampering is detected.
12
+ *
13
+ * Callers convert File objects before passing in: await file.arrayBuffer()
14
+ */
15
+ /** Encrypts plaintext with AES-256-GCM. Returns ciphertext (with auth tag appended) and IV. */
16
+ declare function seal(key: CryptoKey, plaintext: ArrayBuffer): Promise<{
17
+ ciphertext: ArrayBuffer;
18
+ iv: Uint8Array;
19
+ }>;
20
+ /** Decrypts AES-256-GCM ciphertext. Throws if the auth tag check fails. */
21
+ declare function unseal(key: CryptoKey, ciphertext: ArrayBuffer, iv: Uint8Array): Promise<ArrayBuffer>;
22
+ /**
23
+ * Generates a random AES-256-GCM CryptoKey.
24
+ * Pass extractable: true when the key must be wrapped before storage (e.g. via wrapKey).
25
+ */
26
+ declare function generateAesKey(extractable?: boolean): Promise<CryptoKey>;
27
+ /**
28
+ * Imports raw key bytes as an AES-256-GCM CryptoKey.
29
+ * Pass extractable: true when the key must be wrapped before storage (e.g. via wrapKey).
30
+ */
31
+ declare function importAesKey(raw: ArrayBuffer | Uint8Array, extractable?: boolean): Promise<CryptoKey>;
32
+
33
+ /** Wraps a CryptoKey with a passphrase. The input key must have extractable: true. */
34
+ declare function wrapKey(passphrase: string, key: CryptoKey): Promise<{
35
+ wrappedKey: ArrayBuffer;
36
+ salt: Uint8Array;
37
+ }>;
38
+ /** Unwraps a CryptoKey. Always returns extractable: false. */
39
+ declare function unwrapKey(passphrase: string, wrappedKey: ArrayBuffer, salt: Uint8Array): Promise<CryptoKey>;
40
+ /**
41
+ * Changes the passphrase protecting the AEK without re-encrypting any content.
42
+ * Internally unwraps with extractable: true so the key can be immediately re-wrapped.
43
+ */
44
+ declare function rekey(oldPassphrase: string, newPassphrase: string, wrappedKey: ArrayBuffer, salt: Uint8Array): Promise<{
45
+ wrappedKey: ArrayBuffer;
46
+ salt: Uint8Array;
47
+ }>;
48
+
49
+ /** Generates a long-term ECDH P-256 key pair for an account identity. */
50
+ declare function generateECDHKeyPair(): Promise<CryptoKeyPair>;
51
+ /** Encrypts plaintext for a recipient. Only the recipient's private key can decrypt. */
52
+ declare function sealMessage(recipientPublicKey: CryptoKey, plaintext: ArrayBuffer): Promise<{
53
+ ciphertext: ArrayBuffer;
54
+ iv: Uint8Array;
55
+ ephemeralPublicKey: JsonWebKey;
56
+ }>;
57
+ /** Decrypts a message sealed with the recipient's public key. */
58
+ declare function unsealMessage(recipientPrivateKey: CryptoKey, ciphertext: ArrayBuffer, iv: Uint8Array, ephemeralPublicKey: JsonWebKey): Promise<ArrayBuffer>;
59
+
60
+ /**
61
+ * ECDSA P-256 signing for sender verification.
62
+ *
63
+ * Provides cryptographic proof that a file or message came from the claimed sender.
64
+ * The sender signs with their private key; any recipient who has the sender's public
65
+ * key can verify the signature.
66
+ *
67
+ * sign() produces a raw ECDSA signature over arbitrary data.
68
+ * verify() checks a signature against data using the signer's public key.
69
+ * Returns true if valid, false if not — never throws on invalid signatures.
70
+ *
71
+ * Hash: SHA-256.
72
+ */
73
+ /** Generates a long-term ECDSA P-256 key pair for an account identity. */
74
+ declare function generateECDSAKeyPair(): Promise<CryptoKeyPair>;
75
+ /** Signs data with the sender's ECDSA private key. */
76
+ declare function sign(privateKey: CryptoKey, data: ArrayBuffer): Promise<ArrayBuffer>;
77
+ /** Verifies a signature against data using the sender's ECDSA public key. */
78
+ declare function verify(publicKey: CryptoKey, signature: ArrayBuffer, data: ArrayBuffer): Promise<boolean>;
79
+
80
+ /**
81
+ * JWK serialisation for public keys.
82
+ *
83
+ * exportPublicKeyAsJwk() serialises a CryptoKey to a JSON Web Key — used when
84
+ * registering a public key with the Conseal identity registry.
85
+ *
86
+ * importPublicKeyFromJwk() deserialises a JWK back to a CryptoKey — used when
87
+ * loading a recipient's public key before encrypting a message.
88
+ *
89
+ * Only public keys are exported. Private keys are never extracted.
90
+ */
91
+ /** Serialises a public CryptoKey to a JSON Web Key object. */
92
+ declare function exportPublicKeyAsJwk(key: CryptoKey): Promise<JsonWebKey>;
93
+ /** Deserialises a JSON Web Key to a CryptoKey for the given algorithm. */
94
+ declare function importPublicKeyFromJwk(jwk: JsonWebKey, algorithm: 'ECDH' | 'ECDSA'): Promise<CryptoKey>;
95
+
96
+ /** The IndexedDB key id under which the AEK is stored after init(). */
97
+ declare const AEK_KEY_ID = "aek";
98
+ /**
99
+ * Unwraps the AEK with the given passphrase and stores it in IndexedDB.
100
+ * After this completes, the AEK is available via loadKey(AEK_KEY_ID).
101
+ */
102
+ declare function init(wrappedKey: ArrayBuffer, salt: Uint8Array, passphrase: string): Promise<void>;
103
+
104
+ /** Generates a fresh 24-word BIP-39 mnemonic (256 bits of entropy). */
105
+ declare function generateMnemonic(): string;
106
+ /**
107
+ * Derives a deterministic AES-256-GCM CryptoKey from a BIP-39 mnemonic.
108
+ * Throws if the mnemonic is invalid or not in the BIP-39 word list.
109
+ */
110
+ declare function recoverWithMnemonic(mnemonic: string): Promise<CryptoKey>;
111
+
112
+ /** Encrypts plaintext and wraps the key with a passcode, returning a SealedEnvelope. */
113
+ declare function sealEnvelope(plaintext: ArrayBuffer, passcode: string): Promise<SealedEnvelope>;
114
+ /** Decrypts a SealedEnvelope using the passcode. Throws if the passcode is wrong. */
115
+ declare function unsealEnvelope(envelope: SealedEnvelope, passcode: string): Promise<ArrayBuffer>;
116
+ /** The fields produced by sealEnvelope(), ready for JSON serialisation. */
117
+ interface SealedEnvelope {
118
+ ciphertext: ArrayBuffer;
119
+ iv: Uint8Array;
120
+ wrappedKey: ArrayBuffer;
121
+ salt: Uint8Array;
122
+ }
123
+ /**
124
+ * Serialises a SealedEnvelope to a JSON string.
125
+ * Each binary field is base64-encoded. Safe to store server-side or pass over text channels.
126
+ */
127
+ declare function encodeEnvelope(envelope: SealedEnvelope): string;
128
+ /**
129
+ * Deserialises a JSON string produced by encodeEnvelope() back to a SealedEnvelope.
130
+ * Throws SyntaxError if the string is not valid JSON.
131
+ * Throws TypeError if required fields are missing or not strings.
132
+ */
133
+ declare function decodeEnvelope(json: string): SealedEnvelope;
134
+
135
+ /**
136
+ * Base64 encoding and decoding utilities.
137
+ *
138
+ * Helper functions for converting between binary data (ArrayBuffer / Uint8Array)
139
+ * and standard base64 strings — used when serialising encrypted payloads for
140
+ * storage or transmission over text-based channels.
141
+ *
142
+ * Two variants are provided:
143
+ * toBase64 / fromBase64 — standard base64 (uses +, /, = padding)
144
+ * toBase64Url / fromBase64Url — base64url (uses -, _, no padding; safe in URLs and JWKs)
145
+ */
146
+ /**
147
+ * Encodes an ArrayBuffer or Uint8Array to a standard base64 string.
148
+ * Uses a loop instead of spread to avoid stack overflow on large buffers.
149
+ */
150
+ declare function toBase64(buf: ArrayBuffer | Uint8Array): string;
151
+ /** Decodes a standard base64 string to a Uint8Array. */
152
+ declare function fromBase64(b64: string): Uint8Array;
153
+ /**
154
+ * Encodes an ArrayBuffer or Uint8Array to a base64url string.
155
+ * Replaces + with -, / with _, and strips = padding.
156
+ * Used for JWK coordinates and URL-safe contexts.
157
+ */
158
+ declare function toBase64Url(buf: ArrayBuffer | Uint8Array): string;
159
+ /** Decodes a base64url string to a Uint8Array. */
160
+ declare function fromBase64Url(b64url: string): Uint8Array;
161
+
162
+ /**
163
+ * SHA-256 digest.
164
+ *
165
+ * Thin wrapper around SubtleCrypto.digest for the hash function used throughout
166
+ * this library — key fingerprinting, content hashing, and integrity checks.
167
+ */
168
+ /** Returns the SHA-256 hash of the input data as an ArrayBuffer. */
169
+ declare function digest(data: ArrayBuffer | Uint8Array): Promise<ArrayBuffer>;
170
+
171
+ export { AEK_KEY_ID, type SealedEnvelope, decodeEnvelope, digest, encodeEnvelope, exportPublicKeyAsJwk, fromBase64, fromBase64Url, generateAesKey, generateECDHKeyPair, generateECDSAKeyPair, generateMnemonic, importAesKey, importPublicKeyFromJwk, init, recoverWithMnemonic, rekey, seal, sealEnvelope, sealMessage, sign, toBase64, toBase64Url, unseal, unsealEnvelope, unsealMessage, unwrapKey, verify, wrapKey };