@byearlybird/crypto 0.0.2 → 0.2.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/README.md CHANGED
@@ -10,208 +10,263 @@ npm install @byearlybird/crypto
10
10
 
11
11
  ## Quick Start
12
12
 
13
- ### Registration
13
+ ### Encrypt & Decrypt Data
14
14
 
15
15
  ```typescript
16
- import { generateKeys, encryptData } from '@byearlybird/crypto';
16
+ import { generateEncryptionKey, encrypt, decrypt } from '@byearlybird/crypto';
17
17
 
18
- // Generate all keys at once
19
- const { vaultKey, masterKey, encryptedMasterKey } = await generateKeys();
18
+ const key = await generateEncryptionKey();
20
19
 
21
- // Show vault key to user (they must save it!)
22
- console.log('Save this vault key:', vaultKey);
23
-
24
- // Encrypt user data
25
- const encrypted = await encryptData(JSON.stringify({ secret: 'data' }), masterKey);
26
-
27
- // Store on server: encryptedMasterKey + encrypted data
28
- await saveToServer({ encryptedMasterKey, data: encrypted });
20
+ const ciphertext = await encrypt('secret data', key);
21
+ const plaintext = await decrypt(ciphertext, key);
29
22
  ```
30
23
 
31
- ### Login
24
+ ### Sign & Verify Messages
32
25
 
33
26
  ```typescript
34
- import { decryptMasterKey, decryptData } from '@byearlybird/crypto';
27
+ import { generateSigningKeyPair, sign, verify, exportPublicKey } from '@byearlybird/crypto';
35
28
 
36
- // User provides their vault key
37
- const vaultKey = getUserInput();
29
+ const { publicKey, privateKey } = await generateSigningKeyPair(true);
38
30
 
39
- // Fetch from server
40
- const { encryptedMasterKey, data } = await fetchFromServer();
31
+ const signature = await sign('hello', privateKey);
32
+ const valid = await verify('hello', signature, publicKey);
33
+ ```
41
34
 
42
- // Decrypt master key, then decrypt data
43
- const masterKey = await decryptMasterKey(encryptedMasterKey, vaultKey);
44
- const decrypted = await decryptData(data, masterKey);
35
+ ### Signature-Based Auth (eba1)
36
+
37
+ ```typescript
38
+ import {
39
+ generateSigningKeyPair,
40
+ exportPublicKey,
41
+ deriveVaultId,
42
+ createAuthHeader,
43
+ verifyAuthHeader,
44
+ } from '@byearlybird/crypto';
45
+
46
+ const { publicKey, privateKey } = await generateSigningKeyPair(true);
47
+ const pubB64 = await exportPublicKey(publicKey);
48
+ const vaultId = await deriveVaultId(pubB64);
49
+
50
+ // Client: create a signed auth header
51
+ const header = await createAuthHeader({
52
+ vaultId,
53
+ method: 'POST',
54
+ pathWithQuery: '/api/data?page=1',
55
+ serializedBody: JSON.stringify({ hello: 'world' }),
56
+ privateKey,
57
+ });
58
+ // header: "eba1 vid=...;n=...;m=POST;p=/api/data?page=1;t=...;sig=...;bh=..."
59
+
60
+ // Server: verify the header
61
+ const valid = await verifyAuthHeader(header, publicKey);
45
62
  ```
46
63
 
47
64
  ## API Reference
48
65
 
49
- ### `generateKeys()`
66
+ ### Encryption Keys
67
+
68
+ #### `generateEncryptionKey(extractable?)`
50
69
  ```typescript
51
- function generateKeys(): Promise<{
52
- vaultKey: string;
53
- masterKey: CryptoKey;
54
- encryptedMasterKey: string;
55
- }>
70
+ function generateEncryptionKey(extractable?: boolean): Promise<CryptoKey>
56
71
  ```
72
+ Generates a random AES-256-GCM encryption key. `extractable` defaults to `false`.
57
73
 
58
- Generates vault key, master key, and encrypted master key in one call. Convenience method for new users.
74
+ ---
75
+
76
+ #### `exportEncryptionKey(key)`
77
+ ```typescript
78
+ function exportEncryptionKey(key: CryptoKey): Promise<string>
79
+ ```
80
+ Exports an encryption key to a base64 string (raw format).
59
81
 
60
82
  ---
61
83
 
62
- ### `generateVaultKey()`
84
+ #### `importEncryptionKey(base64, extractable?)`
63
85
  ```typescript
64
- function generateVaultKey(): string
86
+ function importEncryptionKey(base64: string, extractable?: boolean): Promise<CryptoKey>
65
87
  ```
88
+ Imports a base64-encoded encryption key. `extractable` defaults to `false`.
89
+
90
+ ---
66
91
 
67
- Generates a random 256-bit vault key (64-char hex string). User must save this.
92
+ #### `encrypt(plaintext, key)`
93
+ ```typescript
94
+ function encrypt(plaintext: string, key: CryptoKey): Promise<string>
95
+ ```
96
+ Encrypts a string with AES-256-GCM. Returns base64 payload (12-byte IV + ciphertext). Fresh random IV per call.
68
97
 
69
98
  ---
70
99
 
71
- ### `generateMasterKey()`
100
+ #### `decrypt(encoded, key)`
72
101
  ```typescript
73
- function generateMasterKey(): Promise<CryptoKey>
102
+ function decrypt(encoded: string, key: CryptoKey): Promise<string>
74
103
  ```
104
+ Decrypts an AES-256-GCM encrypted payload. Throws if the key is wrong or data is tampered with.
105
+
106
+ ---
75
107
 
76
- Generates a random AES-256-GCM master key for encrypting data.
108
+ ### Signing Keys
109
+
110
+ #### `generateSigningKeyPair(extractable?)`
111
+ ```typescript
112
+ function generateSigningKeyPair(extractable?: boolean): Promise<CryptoKeyPair>
113
+ ```
114
+ Generates an Ed25519 signing key pair. `extractable` defaults to `false`.
77
115
 
78
116
  ---
79
117
 
80
- ### `encryptMasterKey(masterKey, vaultKey)`
118
+ #### `exportPublicKey(key)`
81
119
  ```typescript
82
- function encryptMasterKey(
83
- masterKey: CryptoKey,
84
- vaultKey: string
85
- ): Promise<string>
120
+ function exportPublicKey(key: CryptoKey): Promise<string>
86
121
  ```
122
+ Exports a public key to base64 (SPKI format).
123
+
124
+ ---
87
125
 
88
- Encrypts master key with vault key using PBKDF2 (600k iterations). Returns base64 payload (salt + IV + ciphertext).
126
+ #### `importPublicKey(spkiB64, extractable?)`
127
+ ```typescript
128
+ function importPublicKey(spkiB64: string, extractable?: boolean): Promise<CryptoKey>
129
+ ```
130
+ Imports a base64-encoded SPKI public key. `extractable` defaults to `false`.
89
131
 
90
132
  ---
91
133
 
92
- ### `decryptMasterKey(encryptedMasterKey, vaultKey)`
134
+ #### `exportPrivateKey(key)`
93
135
  ```typescript
94
- function decryptMasterKey(
95
- encryptedMasterKey: string,
96
- vaultKey: string
97
- ): Promise<CryptoKey>
136
+ function exportPrivateKey(key: CryptoKey): Promise<string>
98
137
  ```
138
+ Exports a private key to base64 (PKCS8 format).
139
+
140
+ ---
99
141
 
100
- Decrypts encrypted master key. Throws if vault key is wrong or data is corrupted.
142
+ #### `importPrivateKey(pkcs8B64, extractable?)`
143
+ ```typescript
144
+ function importPrivateKey(pkcs8B64: string, extractable?: boolean): Promise<CryptoKey>
145
+ ```
146
+ Imports a base64-encoded PKCS8 private key. `extractable` defaults to `false`.
101
147
 
102
148
  ---
103
149
 
104
- ### `encryptData(data, masterKey)`
150
+ #### `sign(message, privateKey)`
105
151
  ```typescript
106
- function encryptData(
107
- data: string,
108
- masterKey: CryptoKey
109
- ): Promise<string>
152
+ function sign(message: string, privateKey: CryptoKey): Promise<string>
110
153
  ```
154
+ Signs a message using Ed25519. Returns a base64-encoded signature.
155
+
156
+ ---
111
157
 
112
- Encrypts data with master key. Returns base64 payload (IV + ciphertext). Fresh random IV per call.
158
+ #### `verify(message, signatureB64, publicKey)`
159
+ ```typescript
160
+ function verify(message: string, signatureB64: string, publicKey: CryptoKey): Promise<boolean>
161
+ ```
162
+ Verifies an Ed25519 signature. Returns `true` if valid, `false` otherwise.
113
163
 
114
164
  ---
115
165
 
116
- ### `decryptData(encryptedData, masterKey)`
166
+ ### Authentication (eba1)
167
+
168
+ #### `AuthPayload`
117
169
  ```typescript
118
- function decryptData(
119
- encryptedData: string,
120
- masterKey: CryptoKey
121
- ): Promise<string>
170
+ type AuthPayload = {
171
+ scheme: string;
172
+ nonce: string;
173
+ vaultId: string;
174
+ method: string;
175
+ pathWithQuery: string;
176
+ timestamp: number;
177
+ bodyHash?: string;
178
+ }
122
179
  ```
123
180
 
124
- Decrypts data. Throws if key is wrong or data is tampered with (GCM authentication).
181
+ ---
182
+
183
+ #### `deriveVaultId(publicKeyB64)`
184
+ ```typescript
185
+ function deriveVaultId(publicKeyB64: string): Promise<string>
186
+ ```
187
+ Derives a 32-character hex vault ID from a base64-encoded public key (SHA-256 of SPKI bytes, truncated).
125
188
 
126
189
  ---
127
190
 
128
- ### `deriveEncryptionKey(vaultKey, salt?)`
191
+ #### `parseAuthHeader(header)`
129
192
  ```typescript
130
- function deriveEncryptionKey(
131
- vaultKey: string,
132
- salt?: Uint8Array
133
- ): Promise<{ key: CryptoKey; salt: Uint8Array }>
193
+ function parseAuthHeader(header: string): AuthPayload & { signature: string }
134
194
  ```
195
+ Parses an eba1 auth header string back into its components. Throws on malformed or unsupported headers.
135
196
 
136
- Low-level PBKDF2 key derivation. Used internally by `encryptMasterKey`/`decryptMasterKey`.
197
+ ---
198
+
199
+ #### `createAuthHeader(args)`
200
+ ```typescript
201
+ function createAuthHeader(args: {
202
+ vaultId: string;
203
+ method: 'GET' | 'PUT' | 'POST' | 'PATCH' | 'DELETE';
204
+ pathWithQuery: string;
205
+ serializedBody?: string;
206
+ privateKey: CryptoKey;
207
+ }): Promise<string>
208
+ ```
209
+ Creates a complete signed auth header. Generates a nonce/timestamp, hashes the body if provided, signs the canonical string, and returns the formatted header.
137
210
 
138
211
  ---
139
212
 
140
- ### `hash(data)`
213
+ #### `verifyAuthHeader(header, publicKey)`
141
214
  ```typescript
142
- function hash(data: string): Promise<string>
215
+ function verifyAuthHeader(header: string, publicKey: CryptoKey): Promise<boolean>
143
216
  ```
217
+ Verifies an eba1 auth header against a public key. Parses the header, reconstructs the canonical string, and verifies the signature. Returns `true` if valid.
144
218
 
145
- Computes SHA-256 hash of a string. Returns hex-encoded digest (64 chars).
219
+ ---
220
+
221
+ ### Utilities
222
+
223
+ #### `bytesToBase64(bytes)`
224
+ ```typescript
225
+ function bytesToBase64(bytes: Uint8Array): string
226
+ ```
227
+ Converts a byte array to a base64 string.
146
228
 
147
229
  ---
148
230
 
149
- ### `hashObject(obj)`
231
+ #### `base64ToBytes(base64)`
150
232
  ```typescript
151
- function hashObject(obj: unknown): Promise<string>
233
+ function base64ToBytes(base64: string): Uint8Array
152
234
  ```
235
+ Converts a base64 string to a byte array.
153
236
 
154
- Computes SHA-256 hash of a JSON-serializable object. Returns hex-encoded digest (64 chars). Objects must be JSON serializable. Property order is normalized for consistent hashing.
237
+ ---
238
+
239
+ #### `hashString(value)`
240
+ ```typescript
241
+ function hashString(value: string): Promise<string>
242
+ ```
243
+ Computes SHA-256 hash of a string. Returns hex-encoded digest (64 chars).
155
244
 
156
245
  ## Security
157
246
 
158
247
  **Cryptographic primitives:**
159
248
  - AES-256-GCM (authenticated encryption)
160
- - PBKDF2-SHA256 (600k iterations, OWASP 2023)
249
+ - Ed25519 (digital signatures)
161
250
  - SHA-256 (content hashing)
162
- - 128-bit salts, 96-bit IVs
251
+ - 96-bit IVs
163
252
  - `crypto.getRandomValues()` for all randomness
164
253
 
165
254
  **E2EE model:**
166
255
  - All encryption happens client-side
167
256
  - Server only sees ciphertext
168
- - Vault key never leaves client
169
257
  - Fresh IV per encryption (no reuse)
170
- - Self-contained ciphertexts (IVs and salts embedded)
171
-
172
- **Important:**
173
- - Users must save vault keys securely (no recovery if lost)
174
- - Use HTTPS to prevent code injection
258
+ - Self-contained ciphertexts (IVs embedded)
175
259
  - GCM authentication detects tampering
176
- - Vault keys are case-insensitive (normalized to lowercase)
177
-
178
- ## Examples
179
-
180
- ### Password Change
181
-
182
- ```typescript
183
- // Re-encrypt master key with new vault key
184
- const masterKey = await decryptMasterKey(encrypted, oldVaultKey);
185
- const newEncrypted = await encryptMasterKey(masterKey, newVaultKey);
186
- // Update on server (user data doesn't need re-encryption)
187
- ```
188
-
189
- ### Multiple Items
190
260
 
191
- ```typescript
192
- const masterKey = await generateMasterKey();
193
- const items = ['item1', 'item2', 'item3'];
194
- const encrypted = await Promise.all(
195
- items.map(item => encryptData(item, masterKey))
196
- );
197
- // Each has different ciphertext (fresh IV)
198
- ```
199
-
200
- ### Hashing
201
-
202
- ```typescript
203
- import { hash, hashObject } from '@byearlybird/crypto';
204
-
205
- // Hash a string
206
- const digest = await hash('hello world');
207
-
208
- // Hash an object (must be JSON serializable)
209
- const objHash = await hashObject({ user: 'alice', id: 123 });
210
- ```
261
+ **Signature-based auth:**
262
+ - Ed25519 key pairs for request signing
263
+ - Nonce + timestamp prevent replay attacks
264
+ - Optional body hash ensures payload integrity
265
+ - Canonical string format for deterministic signing
211
266
 
212
267
  ## Browser Compatibility
213
268
 
214
- Requires Web Crypto API (Chrome 37+, Firefox 34+, Safari 11+, all modern mobile browsers).
269
+ Requires Web Crypto API and Ed25519 support (Chrome 113+, Firefox 130+, Safari 17+).
215
270
 
216
271
  ## License
217
272
 
package/dist/index.d.mts CHANGED
@@ -1,25 +1,47 @@
1
+ import * as crypto0 from "crypto";
2
+
3
+ //#region src/auth.d.ts
4
+ type AuthPayload = {
5
+ scheme: string;
6
+ nonce: string;
7
+ vaultId: string;
8
+ method: string;
9
+ pathWithQuery: string;
10
+ timestamp: number;
11
+ bodyHash?: string;
12
+ };
13
+ declare function deriveVaultId(publicKeyB64: string): Promise<string>;
14
+ declare function createAuthHeader(args: {
15
+ vaultId: string;
16
+ method: "GET" | "PUT" | "POST" | "PATCH" | "DELETE";
17
+ pathWithQuery: string;
18
+ serializedBody?: string;
19
+ privateKey: CryptoKey;
20
+ }): Promise<string>;
21
+ declare function parseAuthHeader(header: string): AuthPayload & {
22
+ signature: string;
23
+ };
24
+ declare function verifyAuthHeader(header: string, publicKey: CryptoKey): Promise<boolean>;
25
+ //#endregion
1
26
  //#region src/encryption.d.ts
2
- declare function encryptData(data: string, masterKey: CryptoKey): Promise<string>;
3
- declare function decryptData(encryptedData: string, masterKey: CryptoKey): Promise<string>;
27
+ declare function generateEncryptionKey(extractable?: boolean): Promise<crypto0.webcrypto.CryptoKey>;
28
+ declare function exportEncryptionKey(key: CryptoKey): Promise<string>;
29
+ declare function importEncryptionKey(base64: string, extractable?: boolean): Promise<crypto0.webcrypto.CryptoKey>;
30
+ declare function encrypt(plaintext: string, key: CryptoKey): Promise<string>;
31
+ declare function decrypt(encoded: string, key: CryptoKey): Promise<string>;
4
32
  //#endregion
5
- //#region src/hash.d.ts
6
- declare function hash(data: string): Promise<string>;
7
- declare function hashObject(obj: unknown): Promise<string>;
33
+ //#region src/signing.d.ts
34
+ declare function generateSigningKeyPair(extractable?: boolean): Promise<CryptoKeyPair>;
35
+ declare function exportPublicKey(key: CryptoKey): Promise<string>;
36
+ declare function importPublicKey(spkiB64: string, extractable?: boolean): Promise<CryptoKey>;
37
+ declare function exportPrivateKey(key: CryptoKey): Promise<string>;
38
+ declare function importPrivateKey(pkcs8B64: string, extractable?: boolean): Promise<CryptoKey>;
39
+ declare function sign(message: string, privateKey: CryptoKey): Promise<string>;
40
+ declare function verify(message: string, signatureB64: string, publicKey: CryptoKey): Promise<boolean>;
8
41
  //#endregion
9
- //#region src/keys.d.ts
10
- type DerivedKeyResult = {
11
- key: CryptoKey;
12
- salt: Uint8Array;
13
- };
14
- declare function generateVaultKey(): string;
15
- declare function generateMasterKey(): Promise<CryptoKey>;
16
- declare function deriveEncryptionKey(vaultKey: string, salt?: Uint8Array): Promise<DerivedKeyResult>;
17
- declare function encryptMasterKey(masterKey: CryptoKey, vaultKey: string): Promise<string>;
18
- declare function decryptMasterKey(encryptedMasterKey: string, vaultKey: string): Promise<CryptoKey>;
19
- declare function generateKeys(): Promise<{
20
- vaultKey: string;
21
- masterKey: CryptoKey;
22
- encryptedMasterKey: string;
23
- }>;
42
+ //#region src/utils.d.ts
43
+ declare function bytesToBase64(bytes: Uint8Array): string;
44
+ declare function base64ToBytes(base64: string): Uint8Array;
45
+ declare function hashString(value: string): Promise<string>;
24
46
  //#endregion
25
- export { decryptData, decryptMasterKey, deriveEncryptionKey, encryptData, encryptMasterKey, generateKeys, generateMasterKey, generateVaultKey, hash, hashObject };
47
+ export { AuthPayload, base64ToBytes, bytesToBase64, createAuthHeader, decrypt, deriveVaultId, encrypt, exportEncryptionKey, exportPrivateKey, exportPublicKey, generateEncryptionKey, generateSigningKeyPair, hashString, importEncryptionKey, importPrivateKey, importPublicKey, parseAuthHeader, sign, verify, verifyAuthHeader };
package/dist/index.mjs CHANGED
@@ -1,138 +1,171 @@
1
- //#region src/crypto-utils.ts
2
- function randomBytes(length) {
3
- const bytes = new Uint8Array(length);
4
- crypto.getRandomValues(bytes);
5
- return bytes;
6
- }
7
- function concatBytes(...chunks) {
8
- const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
9
- const result = new Uint8Array(total);
10
- let offset = 0;
11
- for (const chunk of chunks) {
12
- result.set(chunk, offset);
13
- offset += chunk.length;
14
- }
15
- return result;
1
+ //#region src/signing.ts
2
+ async function generateSigningKeyPair(extractable = false) {
3
+ return crypto.subtle.generateKey("Ed25519", extractable, ["sign", "verify"]);
4
+ }
5
+ async function exportPublicKey(key) {
6
+ const spki = await crypto.subtle.exportKey("spki", key);
7
+ return Buffer.from(spki).toString("base64");
8
+ }
9
+ async function importPublicKey(spkiB64, extractable = false) {
10
+ const spkiDer = Buffer.from(spkiB64, "base64");
11
+ return await crypto.subtle.importKey("spki", spkiDer, "Ed25519", extractable, ["verify"]);
16
12
  }
17
- function toBase64(bytes) {
18
- return btoa(String.fromCharCode(...bytes));
13
+ async function exportPrivateKey(key) {
14
+ const pkcs8 = await crypto.subtle.exportKey("pkcs8", key);
15
+ return Buffer.from(pkcs8).toString("base64");
19
16
  }
20
- function fromBase64(value) {
21
- return Uint8Array.from(atob(value), (c) => c.charCodeAt(0));
17
+ async function importPrivateKey(pkcs8B64, extractable = false) {
18
+ return await crypto.subtle.importKey("pkcs8", Buffer.from(pkcs8B64, "base64"), "Ed25519", extractable, ["sign"]);
19
+ }
20
+ async function sign(message, privateKey) {
21
+ const data = new TextEncoder().encode(message);
22
+ const signature = await crypto.subtle.sign("Ed25519", privateKey, data);
23
+ return Buffer.from(signature).toString("base64");
24
+ }
25
+ async function verify(message, signatureB64, publicKey) {
26
+ const data = new TextEncoder().encode(message);
27
+ const signature = Buffer.from(signatureB64, "base64");
28
+ return crypto.subtle.verify("Ed25519", publicKey, signature, data);
22
29
  }
23
30
 
24
31
  //#endregion
25
- //#region src/encryption.ts
26
- const IV_LENGTH$1 = 12;
27
- async function encryptData(data, masterKey) {
28
- const iv = randomBytes(IV_LENGTH$1);
29
- const encrypted = await crypto.subtle.encrypt({
30
- name: "AES-GCM",
31
- iv
32
- }, masterKey, new TextEncoder().encode(data));
33
- return toBase64(concatBytes(iv, new Uint8Array(encrypted)));
34
- }
35
- async function decryptData(encryptedData, masterKey) {
36
- const combined = fromBase64(encryptedData);
37
- if (combined.length <= IV_LENGTH$1) throw new Error("Invalid encrypted data payload");
38
- const iv = combined.slice(0, IV_LENGTH$1);
39
- const encrypted = combined.slice(IV_LENGTH$1);
40
- const decrypted = await crypto.subtle.decrypt({
41
- name: "AES-GCM",
42
- iv
43
- }, masterKey, encrypted);
44
- return new TextDecoder().decode(decrypted);
32
+ //#region src/utils.ts
33
+ function bytesToBase64(bytes) {
34
+ return Buffer.from(bytes).toString("base64");
35
+ }
36
+ function base64ToBytes(base64) {
37
+ return new Uint8Array(Buffer.from(base64, "base64"));
38
+ }
39
+ async function hashString(value) {
40
+ const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
41
+ return Buffer.from(hash).toString("hex");
45
42
  }
46
43
 
47
44
  //#endregion
48
- //#region src/hash.ts
49
- async function hash(data) {
50
- const dataBytes = new TextEncoder().encode(data);
51
- const hashBuffer = await crypto.subtle.digest("SHA-256", dataBytes);
52
- return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
53
- }
54
- async function hashObject(obj) {
55
- const canonical = JSON.stringify(normalize(obj));
56
- if (canonical === void 0) throw new TypeError("hashObject only supports JSON-serializable values");
57
- return hash(canonical);
58
- }
59
- function normalize(value) {
60
- if (Array.isArray(value)) return value.map((item) => normalize(item));
61
- if (value && typeof value === "object") {
62
- const prototype = Object.getPrototypeOf(value);
63
- if (prototype === Object.prototype || prototype === null) {
64
- const sortedKeys = Object.keys(value).sort();
65
- const result = {};
66
- for (const key of sortedKeys) result[key] = normalize(value[key]);
67
- return result;
68
- }
45
+ //#region src/auth.ts
46
+ const CURRENT_SCHEME = "eba1";
47
+ async function deriveVaultId(publicKeyB64) {
48
+ const spkiBytes = Buffer.from(publicKeyB64, "base64");
49
+ const hash = await crypto.subtle.digest("SHA-256", spkiBytes);
50
+ return Buffer.from(hash).toString("hex").slice(0, 32);
51
+ }
52
+ async function createAuthHeader(args) {
53
+ const { privateKey,...payloadArgs } = args;
54
+ const payload = await generateAuthPayload(payloadArgs);
55
+ return makeAuthHeader(payload, await sign(makeCanonicalString(payload), privateKey));
56
+ }
57
+ function parseAuthHeader(header) {
58
+ const spaceIdx = header.indexOf(" ");
59
+ if (spaceIdx === -1) throw new Error("Malformed auth header: missing scheme separator");
60
+ const scheme = header.slice(0, spaceIdx);
61
+ if (scheme !== CURRENT_SCHEME) throw new Error(`Unsupported auth scheme: ${scheme}`);
62
+ const paramStr = header.slice(spaceIdx + 1);
63
+ const params = /* @__PURE__ */ new Map();
64
+ for (const part of paramStr.split(";")) {
65
+ const eqIdx = part.indexOf("=");
66
+ if (eqIdx === -1) throw new Error(`Malformed auth header param: ${part}`);
67
+ params.set(part.slice(0, eqIdx), part.slice(eqIdx + 1));
69
68
  }
70
- return value;
69
+ const vid = params.get("vid");
70
+ const n = params.get("n");
71
+ const m = params.get("m");
72
+ const p = params.get("p");
73
+ const t = params.get("t");
74
+ const sig = params.get("sig");
75
+ if (!vid || !n || !m || !p || !t || !sig) throw new Error("Malformed auth header: missing required params");
76
+ const timestamp = Number(t);
77
+ if (Number.isNaN(timestamp)) throw new Error("Malformed auth header: timestamp is not a number");
78
+ const result = {
79
+ scheme,
80
+ vaultId: vid,
81
+ nonce: n,
82
+ method: m,
83
+ pathWithQuery: p,
84
+ timestamp,
85
+ signature: sig
86
+ };
87
+ const bh = params.get("bh");
88
+ if (bh) result.bodyHash = bh;
89
+ return result;
90
+ }
91
+ async function verifyAuthHeader(header, publicKey) {
92
+ const { signature,...payload } = parseAuthHeader(header);
93
+ return verify(makeCanonicalString(payload), signature, publicKey);
94
+ }
95
+ async function generateAuthPayload(args) {
96
+ const { serializedBody,...rest } = args;
97
+ const nonce = crypto.randomUUID();
98
+ const timestamp = Date.now();
99
+ const payload = {
100
+ ...rest,
101
+ scheme: CURRENT_SCHEME,
102
+ nonce,
103
+ timestamp
104
+ };
105
+ if (serializedBody) payload.bodyHash = await hashString(serializedBody);
106
+ return payload;
107
+ }
108
+ function makeCanonicalString(payload) {
109
+ const parts = [
110
+ payload.scheme,
111
+ payload.vaultId,
112
+ payload.method,
113
+ payload.pathWithQuery,
114
+ payload.nonce,
115
+ payload.timestamp
116
+ ];
117
+ if (payload.bodyHash) parts.push(payload.bodyHash);
118
+ return parts.join("\n");
119
+ }
120
+ function makeAuthHeader(payload, signature) {
121
+ const params = [
122
+ `vid=${payload.vaultId}`,
123
+ `n=${payload.nonce}`,
124
+ `m=${payload.method}`,
125
+ `p=${payload.pathWithQuery}`,
126
+ `t=${payload.timestamp}`,
127
+ `sig=${signature}`
128
+ ];
129
+ if (payload.bodyHash) params.push(`bh=${payload.bodyHash}`);
130
+ return `${payload.scheme} ${params.join(";")}`;
71
131
  }
72
132
 
73
133
  //#endregion
74
- //#region src/keys.ts
75
- const PBKDF2_ITERATIONS = 6e5;
76
- const PBKDF2_SALT_LENGTH = 16;
77
- const VAULT_KEY_LENGTH = 32;
78
- const IV_LENGTH = 12;
79
- function generateVaultKey() {
80
- return [...randomBytes(VAULT_KEY_LENGTH)].map((b) => b.toString(16).padStart(2, "0")).join("");
81
- }
82
- async function generateMasterKey() {
134
+ //#region src/encryption.ts
135
+ async function generateEncryptionKey(extractable = false) {
83
136
  return crypto.subtle.generateKey({
84
137
  name: "AES-GCM",
85
138
  length: 256
86
- }, true, ["encrypt", "decrypt"]);
87
- }
88
- async function deriveEncryptionKey(vaultKey, salt) {
89
- const normalizedKey = vaultKey.toLowerCase();
90
- const encoder = new TextEncoder();
91
- const keyMaterial = await crypto.subtle.importKey("raw", encoder.encode(normalizedKey), { name: "PBKDF2" }, false, ["deriveKey"]);
92
- const saltBytes = salt ? new Uint8Array(salt) : randomBytes(PBKDF2_SALT_LENGTH);
93
- return {
94
- key: await crypto.subtle.deriveKey({
95
- name: "PBKDF2",
96
- salt: saltBytes,
97
- iterations: PBKDF2_ITERATIONS,
98
- hash: "SHA-256"
99
- }, keyMaterial, {
100
- name: "AES-GCM",
101
- length: 256
102
- }, false, ["wrapKey", "unwrapKey"]),
103
- salt: saltBytes
104
- };
139
+ }, extractable, ["encrypt", "decrypt"]);
140
+ }
141
+ async function exportEncryptionKey(key) {
142
+ return bytesToBase64(new Uint8Array(await crypto.subtle.exportKey("raw", key)));
143
+ }
144
+ async function importEncryptionKey(base64, extractable = false) {
145
+ const raw = base64ToBytes(base64);
146
+ return crypto.subtle.importKey("raw", raw, { name: "AES-GCM" }, extractable, ["encrypt", "decrypt"]);
105
147
  }
106
- async function encryptMasterKey(masterKey, vaultKey) {
107
- const { key: wrappingKey, salt } = await deriveEncryptionKey(vaultKey);
108
- const iv = randomBytes(IV_LENGTH);
109
- const wrapped = await crypto.subtle.wrapKey("raw", masterKey, wrappingKey, {
148
+ async function encrypt(plaintext, key) {
149
+ const iv = crypto.getRandomValues(new Uint8Array(12));
150
+ const ciphertext = await crypto.subtle.encrypt({
110
151
  name: "AES-GCM",
111
152
  iv
112
- });
113
- return toBase64(concatBytes(salt, iv, new Uint8Array(wrapped)));
114
- }
115
- async function decryptMasterKey(encryptedMasterKey, vaultKey) {
116
- const combined = fromBase64(encryptedMasterKey);
117
- if (combined.length <= PBKDF2_SALT_LENGTH + IV_LENGTH) throw new Error("Invalid encrypted master key payload");
118
- const salt = combined.slice(0, PBKDF2_SALT_LENGTH);
119
- const iv = combined.slice(PBKDF2_SALT_LENGTH, PBKDF2_SALT_LENGTH + IV_LENGTH);
120
- const wrapped = combined.slice(PBKDF2_SALT_LENGTH + IV_LENGTH);
121
- const { key: unwrappingKey } = await deriveEncryptionKey(vaultKey, salt);
122
- return crypto.subtle.unwrapKey("raw", wrapped, unwrappingKey, {
153
+ }, key, new TextEncoder().encode(plaintext));
154
+ const combined = new Uint8Array(iv.byteLength + ciphertext.byteLength);
155
+ combined.set(iv, 0);
156
+ combined.set(new Uint8Array(ciphertext), iv.byteLength);
157
+ return bytesToBase64(combined);
158
+ }
159
+ async function decrypt(encoded, key) {
160
+ const combined = base64ToBytes(encoded);
161
+ const iv = new Uint8Array(combined.subarray(0, 12));
162
+ const ciphertext = new Uint8Array(combined.subarray(12));
163
+ const decrypted = await crypto.subtle.decrypt({
123
164
  name: "AES-GCM",
124
165
  iv
125
- }, { name: "AES-GCM" }, false, ["encrypt", "decrypt"]);
126
- }
127
- async function generateKeys() {
128
- const vaultKey = generateVaultKey();
129
- const encryptedMasterKey = await encryptMasterKey(await generateMasterKey(), vaultKey);
130
- return {
131
- vaultKey,
132
- masterKey: await decryptMasterKey(encryptedMasterKey, vaultKey),
133
- encryptedMasterKey
134
- };
166
+ }, key, ciphertext);
167
+ return new TextDecoder().decode(decrypted);
135
168
  }
136
169
 
137
170
  //#endregion
138
- export { decryptData, decryptMasterKey, deriveEncryptionKey, encryptData, encryptMasterKey, generateKeys, generateMasterKey, generateVaultKey, hash, hashObject };
171
+ export { base64ToBytes, bytesToBase64, createAuthHeader, decrypt, deriveVaultId, encrypt, exportEncryptionKey, exportPrivateKey, exportPublicKey, generateEncryptionKey, generateSigningKeyPair, hashString, importEncryptionKey, importPrivateKey, importPublicKey, parseAuthHeader, sign, verify, verifyAuthHeader };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byearlybird/crypto",
3
- "version": "0.0.2",
3
+ "version": "0.2.0",
4
4
  "description": "Lightweight E2EE toolkit for web apps - zero dependencies + vault key security",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -19,8 +19,8 @@
19
19
  "scripts": {
20
20
  "build": "bun run build.ts",
21
21
  "prepublishOnly": "bun run build.ts",
22
- "patch": "bun pm patch",
23
- "minor": "bun pm minor",
22
+ "patch": "bun pm version patch",
23
+ "minor": "bun pm version minor",
24
24
  "publish": "bun pm publish"
25
25
  },
26
26
  "devDependencies": {