@byearlybird/crypto 0.0.1 → 0.1.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/dist/index.d.mts +44 -22
- package/dist/index.mjs +140 -118
- package/package.json +3 -3
package/dist/index.d.mts
CHANGED
|
@@ -1,25 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import * as crypto0 from "crypto";
|
|
2
|
+
|
|
3
|
+
//#region src/eb-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 generateAuthPayload(args: {
|
|
15
|
+
vaultId: string;
|
|
16
|
+
method: "GET" | "PUT" | "POST" | "PATCH" | "DELETE";
|
|
17
|
+
pathWithQuery: string;
|
|
18
|
+
serializedBody?: string;
|
|
19
|
+
}): Promise<AuthPayload>;
|
|
20
|
+
declare function makeCanonicalString(payload: AuthPayload): string;
|
|
21
|
+
declare function makeAuthHeader(payload: AuthPayload, signature: string): string;
|
|
22
|
+
declare function parseAuthHeader(header: string): AuthPayload & {
|
|
23
|
+
signature: string;
|
|
24
|
+
};
|
|
4
25
|
//#endregion
|
|
5
|
-
//#region src/
|
|
6
|
-
declare function
|
|
7
|
-
declare function
|
|
26
|
+
//#region src/encryption-keys.d.ts
|
|
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>;
|
|
8
32
|
//#endregion
|
|
9
|
-
//#region src/keys.d.ts
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
declare function
|
|
15
|
-
declare function
|
|
16
|
-
declare function
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
declare function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
encryptedMasterKey: string;
|
|
23
|
-
}>;
|
|
33
|
+
//#region src/signing-keys.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>;
|
|
41
|
+
//#endregion
|
|
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 {
|
|
47
|
+
export { AuthPayload, base64ToBytes, bytesToBase64, decrypt, deriveVaultId, encrypt, exportEncryptionKey, exportPrivateKey, exportPublicKey, generateAuthPayload, generateEncryptionKey, generateSigningKeyPair, hashString, importEncryptionKey, importPrivateKey, importPublicKey, makeAuthHeader, makeCanonicalString, parseAuthHeader, sign, verify };
|
package/dist/index.mjs
CHANGED
|
@@ -1,140 +1,162 @@
|
|
|
1
|
-
//#region src/
|
|
2
|
-
function
|
|
3
|
-
|
|
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/utils.ts
|
|
2
|
+
function bytesToBase64(bytes) {
|
|
3
|
+
return Buffer.from(bytes).toString("base64");
|
|
16
4
|
}
|
|
17
|
-
function
|
|
18
|
-
return
|
|
5
|
+
function base64ToBytes(base64) {
|
|
6
|
+
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
19
7
|
}
|
|
20
|
-
function
|
|
21
|
-
|
|
8
|
+
async function hashString(value) {
|
|
9
|
+
const hash = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
10
|
+
return Buffer.from(hash).toString("hex");
|
|
22
11
|
}
|
|
23
12
|
|
|
24
13
|
//#endregion
|
|
25
|
-
//#region src/
|
|
26
|
-
const
|
|
27
|
-
async function
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
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);
|
|
14
|
+
//#region src/eb-auth.ts
|
|
15
|
+
const CURRENT_SCHEME = "eb1";
|
|
16
|
+
async function deriveVaultId(publicKeyB64) {
|
|
17
|
+
const spkiBytes = Buffer.from(publicKeyB64, "base64");
|
|
18
|
+
const hash = await crypto.subtle.digest("SHA-256", spkiBytes);
|
|
19
|
+
return Buffer.from(hash).toString("hex").slice(0, 32);
|
|
45
20
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
return
|
|
58
|
-
}
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
21
|
+
async function generateAuthPayload(args) {
|
|
22
|
+
const { serializedBody,...rest } = args;
|
|
23
|
+
const nonce = crypto.randomUUID();
|
|
24
|
+
const timestamp = Date.now();
|
|
25
|
+
const payload = {
|
|
26
|
+
...rest,
|
|
27
|
+
scheme: CURRENT_SCHEME,
|
|
28
|
+
nonce,
|
|
29
|
+
timestamp
|
|
30
|
+
};
|
|
31
|
+
if (serializedBody) payload.bodyHash = await hashString(serializedBody);
|
|
32
|
+
return payload;
|
|
33
|
+
}
|
|
34
|
+
function makeCanonicalString(payload) {
|
|
35
|
+
const parts = [
|
|
36
|
+
payload.scheme,
|
|
37
|
+
payload.vaultId,
|
|
38
|
+
payload.method,
|
|
39
|
+
payload.pathWithQuery,
|
|
40
|
+
payload.nonce,
|
|
41
|
+
payload.timestamp
|
|
42
|
+
];
|
|
43
|
+
if (payload.bodyHash) parts.push(payload.bodyHash);
|
|
44
|
+
return parts.join("\n");
|
|
45
|
+
}
|
|
46
|
+
function makeAuthHeader(payload, signature) {
|
|
47
|
+
const params = [
|
|
48
|
+
`vid=${payload.vaultId}`,
|
|
49
|
+
`n=${payload.nonce}`,
|
|
50
|
+
`m=${payload.method}`,
|
|
51
|
+
`p=${payload.pathWithQuery}`,
|
|
52
|
+
`t=${payload.timestamp}`,
|
|
53
|
+
`sig=${signature}`
|
|
54
|
+
];
|
|
55
|
+
if (payload.bodyHash) params.push(`bh=${payload.bodyHash}`);
|
|
56
|
+
return `${payload.scheme} ${params.join(";")}`;
|
|
57
|
+
}
|
|
58
|
+
function parseAuthHeader(header) {
|
|
59
|
+
const spaceIdx = header.indexOf(" ");
|
|
60
|
+
if (spaceIdx === -1) throw new Error("Malformed auth header: missing scheme separator");
|
|
61
|
+
const scheme = header.slice(0, spaceIdx);
|
|
62
|
+
if (scheme !== CURRENT_SCHEME) throw new Error(`Unsupported auth scheme: ${scheme}`);
|
|
63
|
+
const paramStr = header.slice(spaceIdx + 1);
|
|
64
|
+
const params = /* @__PURE__ */ new Map();
|
|
65
|
+
for (const part of paramStr.split(";")) {
|
|
66
|
+
const eqIdx = part.indexOf("=");
|
|
67
|
+
if (eqIdx === -1) throw new Error(`Malformed auth header param: ${part}`);
|
|
68
|
+
params.set(part.slice(0, eqIdx), part.slice(eqIdx + 1));
|
|
69
69
|
}
|
|
70
|
-
|
|
70
|
+
const vid = params.get("vid");
|
|
71
|
+
const n = params.get("n");
|
|
72
|
+
const m = params.get("m");
|
|
73
|
+
const p = params.get("p");
|
|
74
|
+
const t = params.get("t");
|
|
75
|
+
const sig = params.get("sig");
|
|
76
|
+
if (!vid || !n || !m || !p || !t || !sig) throw new Error("Malformed auth header: missing required params");
|
|
77
|
+
const timestamp = Number(t);
|
|
78
|
+
if (Number.isNaN(timestamp)) throw new Error("Malformed auth header: timestamp is not a number");
|
|
79
|
+
const result = {
|
|
80
|
+
scheme,
|
|
81
|
+
vaultId: vid,
|
|
82
|
+
nonce: n,
|
|
83
|
+
method: m,
|
|
84
|
+
pathWithQuery: p,
|
|
85
|
+
timestamp,
|
|
86
|
+
signature: sig
|
|
87
|
+
};
|
|
88
|
+
const bh = params.get("bh");
|
|
89
|
+
if (bh) result.bodyHash = bh;
|
|
90
|
+
return result;
|
|
71
91
|
}
|
|
72
92
|
|
|
73
93
|
//#endregion
|
|
74
|
-
//#region src/keys.ts
|
|
75
|
-
|
|
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() {
|
|
94
|
+
//#region src/encryption-keys.ts
|
|
95
|
+
async function generateEncryptionKey(extractable = false) {
|
|
83
96
|
return crypto.subtle.generateKey({
|
|
84
97
|
name: "AES-GCM",
|
|
85
98
|
length: 256
|
|
86
|
-
},
|
|
87
|
-
}
|
|
88
|
-
async function
|
|
89
|
-
|
|
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
|
-
};
|
|
99
|
+
}, extractable, ["encrypt", "decrypt"]);
|
|
100
|
+
}
|
|
101
|
+
async function exportEncryptionKey(key) {
|
|
102
|
+
return bytesToBase64(new Uint8Array(await crypto.subtle.exportKey("raw", key)));
|
|
105
103
|
}
|
|
106
|
-
async function
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
104
|
+
async function importEncryptionKey(base64, extractable = false) {
|
|
105
|
+
const raw = base64ToBytes(base64);
|
|
106
|
+
return crypto.subtle.importKey("raw", raw, { name: "AES-GCM" }, extractable, ["encrypt", "decrypt"]);
|
|
107
|
+
}
|
|
108
|
+
async function encrypt(plaintext, key) {
|
|
109
|
+
const iv = crypto.getRandomValues(new Uint8Array(12));
|
|
110
|
+
const ciphertext = await crypto.subtle.encrypt({
|
|
111
111
|
name: "AES-GCM",
|
|
112
112
|
iv
|
|
113
|
-
},
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
const
|
|
122
|
-
const
|
|
113
|
+
}, key, new TextEncoder().encode(plaintext));
|
|
114
|
+
const combined = new Uint8Array(iv.byteLength + ciphertext.byteLength);
|
|
115
|
+
combined.set(iv, 0);
|
|
116
|
+
combined.set(new Uint8Array(ciphertext), iv.byteLength);
|
|
117
|
+
return bytesToBase64(combined);
|
|
118
|
+
}
|
|
119
|
+
async function decrypt(encoded, key) {
|
|
120
|
+
const combined = base64ToBytes(encoded);
|
|
121
|
+
const iv = new Uint8Array(combined.subarray(0, 12));
|
|
122
|
+
const ciphertext = new Uint8Array(combined.subarray(12));
|
|
123
123
|
const decrypted = await crypto.subtle.decrypt({
|
|
124
124
|
name: "AES-GCM",
|
|
125
125
|
iv
|
|
126
|
-
},
|
|
127
|
-
return
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
126
|
+
}, key, ciphertext);
|
|
127
|
+
return new TextDecoder().decode(decrypted);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
//#endregion
|
|
131
|
+
//#region src/signing-keys.ts
|
|
132
|
+
async function generateSigningKeyPair(extractable = false) {
|
|
133
|
+
return crypto.subtle.generateKey("Ed25519", extractable, ["sign", "verify"]);
|
|
134
|
+
}
|
|
135
|
+
async function exportPublicKey(key) {
|
|
136
|
+
const spki = await crypto.subtle.exportKey("spki", key);
|
|
137
|
+
return Buffer.from(spki).toString("base64");
|
|
138
|
+
}
|
|
139
|
+
async function importPublicKey(spkiB64, extractable = false) {
|
|
140
|
+
const spkiDer = Buffer.from(spkiB64, "base64");
|
|
141
|
+
return await crypto.subtle.importKey("spki", spkiDer, "Ed25519", extractable, ["verify"]);
|
|
142
|
+
}
|
|
143
|
+
async function exportPrivateKey(key) {
|
|
144
|
+
const pkcs8 = await crypto.subtle.exportKey("pkcs8", key);
|
|
145
|
+
return Buffer.from(pkcs8).toString("base64");
|
|
146
|
+
}
|
|
147
|
+
async function importPrivateKey(pkcs8B64, extractable = false) {
|
|
148
|
+
return await crypto.subtle.importKey("pkcs8", Buffer.from(pkcs8B64, "base64"), "Ed25519", extractable, ["sign"]);
|
|
149
|
+
}
|
|
150
|
+
async function sign(message, privateKey) {
|
|
151
|
+
const data = new TextEncoder().encode(message);
|
|
152
|
+
const signature = await crypto.subtle.sign("Ed25519", privateKey, data);
|
|
153
|
+
return Buffer.from(signature).toString("base64");
|
|
154
|
+
}
|
|
155
|
+
async function verify(message, signatureB64, publicKey) {
|
|
156
|
+
const data = new TextEncoder().encode(message);
|
|
157
|
+
const signature = Buffer.from(signatureB64, "base64");
|
|
158
|
+
return crypto.subtle.verify("Ed25519", publicKey, signature, data);
|
|
137
159
|
}
|
|
138
160
|
|
|
139
161
|
//#endregion
|
|
140
|
-
export {
|
|
162
|
+
export { base64ToBytes, bytesToBase64, decrypt, deriveVaultId, encrypt, exportEncryptionKey, exportPrivateKey, exportPublicKey, generateAuthPayload, generateEncryptionKey, generateSigningKeyPair, hashString, importEncryptionKey, importPrivateKey, importPublicKey, makeAuthHeader, makeCanonicalString, parseAuthHeader, sign, verify };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byearlybird/crypto",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.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": {
|