@byearlybird/crypto 0.0.1

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,218 @@
1
+ # @byearlybird/crypto
2
+
3
+ Lightweight E2EE toolkit for web applications. Zero dependencies, Web Crypto API only.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @byearlybird/crypto
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ### Registration
14
+
15
+ ```typescript
16
+ import { generateKeys, encryptData } from '@byearlybird/crypto';
17
+
18
+ // Generate all keys at once
19
+ const { vaultKey, masterKey, encryptedMasterKey } = await generateKeys();
20
+
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 });
29
+ ```
30
+
31
+ ### Login
32
+
33
+ ```typescript
34
+ import { decryptMasterKey, decryptData } from '@byearlybird/crypto';
35
+
36
+ // User provides their vault key
37
+ const vaultKey = getUserInput();
38
+
39
+ // Fetch from server
40
+ const { encryptedMasterKey, data } = await fetchFromServer();
41
+
42
+ // Decrypt master key, then decrypt data
43
+ const masterKey = await decryptMasterKey(encryptedMasterKey, vaultKey);
44
+ const decrypted = await decryptData(data, masterKey);
45
+ ```
46
+
47
+ ## API Reference
48
+
49
+ ### `generateKeys()`
50
+ ```typescript
51
+ function generateKeys(): Promise<{
52
+ vaultKey: string;
53
+ masterKey: CryptoKey;
54
+ encryptedMasterKey: string;
55
+ }>
56
+ ```
57
+
58
+ Generates vault key, master key, and encrypted master key in one call. Convenience method for new users.
59
+
60
+ ---
61
+
62
+ ### `generateVaultKey()`
63
+ ```typescript
64
+ function generateVaultKey(): string
65
+ ```
66
+
67
+ Generates a random 256-bit vault key (64-char hex string). User must save this.
68
+
69
+ ---
70
+
71
+ ### `generateMasterKey()`
72
+ ```typescript
73
+ function generateMasterKey(): Promise<CryptoKey>
74
+ ```
75
+
76
+ Generates a random AES-256-GCM master key for encrypting data.
77
+
78
+ ---
79
+
80
+ ### `encryptMasterKey(masterKey, vaultKey)`
81
+ ```typescript
82
+ function encryptMasterKey(
83
+ masterKey: CryptoKey,
84
+ vaultKey: string
85
+ ): Promise<string>
86
+ ```
87
+
88
+ Encrypts master key with vault key using PBKDF2 (600k iterations). Returns base64 payload (salt + IV + ciphertext).
89
+
90
+ ---
91
+
92
+ ### `decryptMasterKey(encryptedMasterKey, vaultKey)`
93
+ ```typescript
94
+ function decryptMasterKey(
95
+ encryptedMasterKey: string,
96
+ vaultKey: string
97
+ ): Promise<CryptoKey>
98
+ ```
99
+
100
+ Decrypts encrypted master key. Throws if vault key is wrong or data is corrupted.
101
+
102
+ ---
103
+
104
+ ### `encryptData(data, masterKey)`
105
+ ```typescript
106
+ function encryptData(
107
+ data: string,
108
+ masterKey: CryptoKey
109
+ ): Promise<string>
110
+ ```
111
+
112
+ Encrypts data with master key. Returns base64 payload (IV + ciphertext). Fresh random IV per call.
113
+
114
+ ---
115
+
116
+ ### `decryptData(encryptedData, masterKey)`
117
+ ```typescript
118
+ function decryptData(
119
+ encryptedData: string,
120
+ masterKey: CryptoKey
121
+ ): Promise<string>
122
+ ```
123
+
124
+ Decrypts data. Throws if key is wrong or data is tampered with (GCM authentication).
125
+
126
+ ---
127
+
128
+ ### `deriveEncryptionKey(vaultKey, salt?)`
129
+ ```typescript
130
+ function deriveEncryptionKey(
131
+ vaultKey: string,
132
+ salt?: Uint8Array
133
+ ): Promise<{ key: CryptoKey; salt: Uint8Array }>
134
+ ```
135
+
136
+ Low-level PBKDF2 key derivation. Used internally by `encryptMasterKey`/`decryptMasterKey`.
137
+
138
+ ---
139
+
140
+ ### `hash(data)`
141
+ ```typescript
142
+ function hash(data: string): Promise<string>
143
+ ```
144
+
145
+ Computes SHA-256 hash of a string. Returns hex-encoded digest (64 chars).
146
+
147
+ ---
148
+
149
+ ### `hashObject(obj)`
150
+ ```typescript
151
+ function hashObject(obj: unknown): Promise<string>
152
+ ```
153
+
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.
155
+
156
+ ## Security
157
+
158
+ **Cryptographic primitives:**
159
+ - AES-256-GCM (authenticated encryption)
160
+ - PBKDF2-SHA256 (600k iterations, OWASP 2023)
161
+ - SHA-256 (content hashing)
162
+ - 128-bit salts, 96-bit IVs
163
+ - `crypto.getRandomValues()` for all randomness
164
+
165
+ **E2EE model:**
166
+ - All encryption happens client-side
167
+ - Server only sees ciphertext
168
+ - Vault key never leaves client
169
+ - 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
175
+ - 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
+
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
+ ```
211
+
212
+ ## Browser Compatibility
213
+
214
+ Requires Web Crypto API (Chrome 37+, Firefox 34+, Safari 11+, all modern mobile browsers).
215
+
216
+ ## License
217
+
218
+ MIT
@@ -0,0 +1,25 @@
1
+ //#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>;
4
+ //#endregion
5
+ //#region src/hash.d.ts
6
+ declare function hash(data: string): Promise<string>;
7
+ declare function hashObject(obj: unknown): Promise<string>;
8
+ //#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
+ }>;
24
+ //#endregion
25
+ export { decryptData, decryptMasterKey, deriveEncryptionKey, encryptData, encryptMasterKey, generateKeys, generateMasterKey, generateVaultKey, hash, hashObject };
package/dist/index.mjs ADDED
@@ -0,0 +1,140 @@
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;
16
+ }
17
+ function toBase64(bytes) {
18
+ return btoa(String.fromCharCode(...bytes));
19
+ }
20
+ function fromBase64(value) {
21
+ return Uint8Array.from(atob(value), (c) => c.charCodeAt(0));
22
+ }
23
+
24
+ //#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);
45
+ }
46
+
47
+ //#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
+ }
69
+ }
70
+ return value;
71
+ }
72
+
73
+ //#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() {
83
+ return crypto.subtle.generateKey({
84
+ name: "AES-GCM",
85
+ 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
+ }, true, ["encrypt", "decrypt"]),
103
+ salt: saltBytes
104
+ };
105
+ }
106
+ async function encryptMasterKey(masterKey, vaultKey) {
107
+ const masterKeyBytes = await crypto.subtle.exportKey("raw", masterKey);
108
+ const { key: encryptionKey, salt } = await deriveEncryptionKey(vaultKey);
109
+ const iv = randomBytes(IV_LENGTH);
110
+ const encrypted = await crypto.subtle.encrypt({
111
+ name: "AES-GCM",
112
+ iv
113
+ }, encryptionKey, masterKeyBytes);
114
+ return toBase64(concatBytes(salt, iv, new Uint8Array(encrypted)));
115
+ }
116
+ async function decryptMasterKey(encryptedMasterKey, vaultKey) {
117
+ const combined = fromBase64(encryptedMasterKey);
118
+ if (combined.length <= PBKDF2_SALT_LENGTH + IV_LENGTH) throw new Error("Invalid encrypted master key payload");
119
+ const salt = combined.slice(0, PBKDF2_SALT_LENGTH);
120
+ const iv = combined.slice(PBKDF2_SALT_LENGTH, PBKDF2_SALT_LENGTH + IV_LENGTH);
121
+ const encrypted = combined.slice(PBKDF2_SALT_LENGTH + IV_LENGTH);
122
+ const { key: encryptionKey } = await deriveEncryptionKey(vaultKey, salt);
123
+ const decrypted = await crypto.subtle.decrypt({
124
+ name: "AES-GCM",
125
+ iv
126
+ }, encryptionKey, encrypted);
127
+ return crypto.subtle.importKey("raw", decrypted, { name: "AES-GCM" }, true, ["encrypt", "decrypt"]);
128
+ }
129
+ async function generateKeys() {
130
+ const vaultKey = generateVaultKey();
131
+ const masterKey = await generateMasterKey();
132
+ return {
133
+ vaultKey,
134
+ masterKey,
135
+ encryptedMasterKey: await encryptMasterKey(masterKey, vaultKey)
136
+ };
137
+ }
138
+
139
+ //#endregion
140
+ export { decryptData, decryptMasterKey, deriveEncryptionKey, encryptData, encryptMasterKey, generateKeys, generateMasterKey, generateVaultKey, hash, hashObject };
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@byearlybird/crypto",
3
+ "version": "0.0.1",
4
+ "description": "Lightweight E2EE toolkit for web apps - zero dependencies + vault key security",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "main": "./dist/index.mjs",
8
+ "types": "./dist/index.d.mts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.mts",
12
+ "import": "./dist/index.mjs",
13
+ "default": "./dist/index.mjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "bun run build.ts",
21
+ "prepublishOnly": "bun run build.ts",
22
+ "patch": "bun pm patch",
23
+ "minor": "bun pm minor",
24
+ "publish": "bun pm publish"
25
+ },
26
+ "devDependencies": {
27
+ "@biomejs/biome": "2.3.5",
28
+ "@types/bun": "latest",
29
+ "tsdown": "^0.16.1"
30
+ },
31
+ "peerDependencies": {
32
+ "typescript": "^5"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ }
37
+ }