@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 +218 -0
- package/dist/index.d.mts +25 -0
- package/dist/index.mjs +140 -0
- package/package.json +37 -0
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
|
package/dist/index.d.mts
ADDED
|
@@ -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
|
+
}
|