@aigne/afs-vault 1.11.0-beta.12

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/LICENSE.md ADDED
@@ -0,0 +1,26 @@
1
+ # Proprietary License
2
+
3
+ Copyright (c) 2024-2025 ArcBlock, Inc. All Rights Reserved.
4
+
5
+ This software and associated documentation files (the "Software") are proprietary
6
+ and confidential. Unauthorized copying, modification, distribution, or use of
7
+ this Software, via any medium, is strictly prohibited.
8
+
9
+ The Software is provided for internal use only within ArcBlock, Inc. and its
10
+ authorized affiliates.
11
+
12
+ ## No License Granted
13
+
14
+ No license, express or implied, is granted to any party for any purpose.
15
+ All rights are reserved by ArcBlock, Inc.
16
+
17
+ ## Public Artifact Distribution
18
+
19
+ Portions of this Software may be released publicly under separate open-source
20
+ licenses (such as MIT License) through designated public repositories. Such
21
+ public releases are governed by their respective licenses and do not affect
22
+ the proprietary nature of this repository.
23
+
24
+ ## Contact
25
+
26
+ For licensing inquiries, contact: legal@arcblock.io
@@ -0,0 +1,11 @@
1
+
2
+ //#region \0@oxc-project+runtime@0.108.0/helpers/decorate.js
3
+ function __decorate(decorators, target, key, desc) {
4
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
5
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
6
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
7
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
8
+ }
9
+
10
+ //#endregion
11
+ exports.__decorate = __decorate;
@@ -0,0 +1,10 @@
1
+ //#region \0@oxc-project+runtime@0.108.0/helpers/decorate.js
2
+ function __decorate(decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ }
8
+
9
+ //#endregion
10
+ export { __decorate };
@@ -0,0 +1,157 @@
1
+ let node_crypto = require("node:crypto");
2
+ let node_fs_promises = require("node:fs/promises");
3
+ let node_path = require("node:path");
4
+
5
+ //#region src/encrypted-file.ts
6
+ /**
7
+ * Encrypted file backend for vault storage.
8
+ *
9
+ * - AES-256-GCM encryption
10
+ * - Master key: 256-bit random or crypto.scrypt-derived from passphrase
11
+ * - File format: JSON header + encrypted blob
12
+ * - File permissions: 0600
13
+ */
14
+ /** Encryption parameters */
15
+ const KEY_LENGTH = 32;
16
+ const IV_LENGTH = 12;
17
+ const SALT_LENGTH = 32;
18
+ const AUTH_TAG_LENGTH = 16;
19
+ const SCRYPT_N = 2 ** 14;
20
+ const SCRYPT_R = 8;
21
+ const SCRYPT_P = 1;
22
+ const SCRYPT_KEYLEN = KEY_LENGTH;
23
+ const FILE_MODE = 384;
24
+ /** Magic bytes to identify vault files */
25
+ const MAGIC = "AFSVAULT";
26
+ const FORMAT_VERSION = 1;
27
+ /**
28
+ * Generate a random 256-bit master key.
29
+ */
30
+ function generateMasterKey() {
31
+ return (0, node_crypto.randomBytes)(KEY_LENGTH);
32
+ }
33
+ /**
34
+ * Derive a master key from a passphrase using crypto.scrypt.
35
+ */
36
+ function deriveKeyFromPassphrase(passphrase, salt) {
37
+ return (0, node_crypto.scryptSync)(passphrase, salt, SCRYPT_KEYLEN, {
38
+ N: SCRYPT_N,
39
+ r: SCRYPT_R,
40
+ p: SCRYPT_P
41
+ });
42
+ }
43
+ /**
44
+ * Encrypt vault data and write to file.
45
+ */
46
+ async function writeEncryptedVault(filePath, data, masterKey) {
47
+ const json = JSON.stringify(data);
48
+ const plaintext = Buffer.from(json, "utf-8");
49
+ const salt = (0, node_crypto.randomBytes)(SALT_LENGTH);
50
+ const iv = (0, node_crypto.randomBytes)(IV_LENGTH);
51
+ const cipher = (0, node_crypto.createCipheriv)("aes-256-gcm", masterKey, iv);
52
+ const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
53
+ const authTag = cipher.getAuthTag();
54
+ const magic = Buffer.from(MAGIC, "ascii");
55
+ const version = Buffer.alloc(1);
56
+ version[0] = FORMAT_VERSION;
57
+ const fileContent = Buffer.concat([
58
+ magic,
59
+ version,
60
+ salt,
61
+ iv,
62
+ authTag,
63
+ encrypted
64
+ ]);
65
+ await (0, node_fs_promises.mkdir)((0, node_path.dirname)(filePath), { recursive: true });
66
+ const tmpPath = `${filePath}.tmp.${process.pid}`;
67
+ try {
68
+ await (0, node_fs_promises.writeFile)(tmpPath, fileContent, { mode: FILE_MODE });
69
+ await (0, node_fs_promises.rename)(tmpPath, filePath);
70
+ } catch (err) {
71
+ try {
72
+ const { unlink } = await import("node:fs/promises");
73
+ await unlink(tmpPath);
74
+ } catch {}
75
+ throw err;
76
+ }
77
+ try {
78
+ if (((await (0, node_fs_promises.stat)(filePath)).mode & 511) !== FILE_MODE) await (0, node_fs_promises.chmod)(filePath, FILE_MODE);
79
+ } catch {}
80
+ }
81
+ /**
82
+ * Read and decrypt vault data from file.
83
+ * Returns null if file does not exist.
84
+ */
85
+ async function readEncryptedVault(filePath, masterKey) {
86
+ let fileContent;
87
+ try {
88
+ fileContent = await (0, node_fs_promises.readFile)(filePath);
89
+ } catch (err) {
90
+ if (err?.code === "ENOENT") return null;
91
+ throw err;
92
+ }
93
+ const magicLen = 8;
94
+ const minSize = magicLen + 1 + SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH;
95
+ if (fileContent.length < minSize) throw new Error("Vault file is corrupted: too small");
96
+ if (fileContent.subarray(0, magicLen).toString("ascii") !== MAGIC) throw new Error("Not a valid vault file");
97
+ const version = fileContent[magicLen];
98
+ if (version !== FORMAT_VERSION) throw new Error(`Unsupported vault format version: ${version}`);
99
+ let offset = magicLen + 1;
100
+ fileContent.subarray(offset, offset + SALT_LENGTH);
101
+ offset += SALT_LENGTH;
102
+ const iv = fileContent.subarray(offset, offset + IV_LENGTH);
103
+ offset += IV_LENGTH;
104
+ const authTag = fileContent.subarray(offset, offset + AUTH_TAG_LENGTH);
105
+ offset += AUTH_TAG_LENGTH;
106
+ const ciphertext = fileContent.subarray(offset);
107
+ const decipher = (0, node_crypto.createDecipheriv)("aes-256-gcm", masterKey, iv);
108
+ decipher.setAuthTag(authTag);
109
+ let plaintext;
110
+ try {
111
+ plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
112
+ } catch {
113
+ throw new Error("Vault decryption failed: wrong master key or corrupted data");
114
+ }
115
+ const json = plaintext.toString("utf-8");
116
+ try {
117
+ return JSON.parse(json);
118
+ } catch {
119
+ throw new Error("Vault data is corrupted: invalid JSON after decryption");
120
+ }
121
+ }
122
+ /**
123
+ * Read the per-file salt from a vault file header.
124
+ * Returns null if file does not exist or is not a valid vault file.
125
+ */
126
+ async function readVaultSalt(filePath) {
127
+ let fileContent;
128
+ try {
129
+ fileContent = await (0, node_fs_promises.readFile)(filePath);
130
+ } catch {
131
+ return null;
132
+ }
133
+ const magicLen = 8;
134
+ const minSize = magicLen + 1 + SALT_LENGTH;
135
+ if (fileContent.length < minSize) return null;
136
+ if (fileContent.subarray(0, magicLen).toString("ascii") !== MAGIC) return null;
137
+ return fileContent.subarray(magicLen + 1, magicLen + 1 + SALT_LENGTH);
138
+ }
139
+ /**
140
+ * Check if a vault file exists.
141
+ */
142
+ async function vaultFileExists(filePath) {
143
+ try {
144
+ await (0, node_fs_promises.stat)(filePath);
145
+ return true;
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+
151
+ //#endregion
152
+ exports.deriveKeyFromPassphrase = deriveKeyFromPassphrase;
153
+ exports.generateMasterKey = generateMasterKey;
154
+ exports.readEncryptedVault = readEncryptedVault;
155
+ exports.readVaultSalt = readVaultSalt;
156
+ exports.vaultFileExists = vaultFileExists;
157
+ exports.writeEncryptedVault = writeEncryptedVault;
@@ -0,0 +1,44 @@
1
+ //#region src/encrypted-file.d.ts
2
+ /**
3
+ * Encrypted file backend for vault storage.
4
+ *
5
+ * - AES-256-GCM encryption
6
+ * - Master key: 256-bit random or crypto.scrypt-derived from passphrase
7
+ * - File format: JSON header + encrypted blob
8
+ * - File permissions: 0600
9
+ */
10
+ /** File header layout (binary):
11
+ * 8 bytes — magic "AFSVAULT"
12
+ * 1 byte — format version
13
+ * 32 bytes — salt (for passphrase-derived keys; random for direct keys)
14
+ * 12 bytes — IV
15
+ * 16 bytes — auth tag
16
+ * rest — ciphertext
17
+ */
18
+ interface VaultData {
19
+ secrets: Record<string, Record<string, string>>;
20
+ }
21
+ /**
22
+ * Generate a random 256-bit master key.
23
+ */
24
+ declare function generateMasterKey(): Buffer;
25
+ /**
26
+ * Derive a master key from a passphrase using crypto.scrypt.
27
+ */
28
+ declare function deriveKeyFromPassphrase(passphrase: string, salt: Buffer): Buffer;
29
+ /**
30
+ * Encrypt vault data and write to file.
31
+ */
32
+ declare function writeEncryptedVault(filePath: string, data: VaultData, masterKey: Buffer): Promise<void>;
33
+ /**
34
+ * Read and decrypt vault data from file.
35
+ * Returns null if file does not exist.
36
+ */
37
+ declare function readEncryptedVault(filePath: string, masterKey: Buffer): Promise<VaultData | null>;
38
+ /**
39
+ * Check if a vault file exists.
40
+ */
41
+ declare function vaultFileExists(filePath: string): Promise<boolean>;
42
+ //#endregion
43
+ export { VaultData, deriveKeyFromPassphrase, generateMasterKey, readEncryptedVault, vaultFileExists, writeEncryptedVault };
44
+ //# sourceMappingURL=encrypted-file.d.cts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encrypted-file.d.cts","names":[],"sources":["../src/encrypted-file.ts"],"mappings":";;AAqCA;;;;;;;;;AAYA;;;;;AAOA;UAnBiB,SAAA;EACf,OAAA,EAAS,MAAA,SAAe,MAAA;AAAA;;;AA6B1B;iBAlBgB,iBAAA,CAAA,GAAqB,MAAA;;;;iBAOrB,uBAAA,CAAwB,UAAA,UAAoB,IAAA,EAAM,MAAA,GAAS,MAAA;;;;iBAWrD,mBAAA,CACpB,QAAA,UACA,IAAA,EAAM,SAAA,EACN,SAAA,EAAW,MAAA,GACV,OAAA;;;;;iBAmDmB,kBAAA,CACpB,QAAA,UACA,SAAA,EAAW,MAAA,GACV,OAAA,CAAQ,SAAA;;;;iBA+EW,eAAA,CAAgB,QAAA,WAAmB,OAAA"}
@@ -0,0 +1,44 @@
1
+ //#region src/encrypted-file.d.ts
2
+ /**
3
+ * Encrypted file backend for vault storage.
4
+ *
5
+ * - AES-256-GCM encryption
6
+ * - Master key: 256-bit random or crypto.scrypt-derived from passphrase
7
+ * - File format: JSON header + encrypted blob
8
+ * - File permissions: 0600
9
+ */
10
+ /** File header layout (binary):
11
+ * 8 bytes — magic "AFSVAULT"
12
+ * 1 byte — format version
13
+ * 32 bytes — salt (for passphrase-derived keys; random for direct keys)
14
+ * 12 bytes — IV
15
+ * 16 bytes — auth tag
16
+ * rest — ciphertext
17
+ */
18
+ interface VaultData {
19
+ secrets: Record<string, Record<string, string>>;
20
+ }
21
+ /**
22
+ * Generate a random 256-bit master key.
23
+ */
24
+ declare function generateMasterKey(): Buffer;
25
+ /**
26
+ * Derive a master key from a passphrase using crypto.scrypt.
27
+ */
28
+ declare function deriveKeyFromPassphrase(passphrase: string, salt: Buffer): Buffer;
29
+ /**
30
+ * Encrypt vault data and write to file.
31
+ */
32
+ declare function writeEncryptedVault(filePath: string, data: VaultData, masterKey: Buffer): Promise<void>;
33
+ /**
34
+ * Read and decrypt vault data from file.
35
+ * Returns null if file does not exist.
36
+ */
37
+ declare function readEncryptedVault(filePath: string, masterKey: Buffer): Promise<VaultData | null>;
38
+ /**
39
+ * Check if a vault file exists.
40
+ */
41
+ declare function vaultFileExists(filePath: string): Promise<boolean>;
42
+ //#endregion
43
+ export { VaultData, deriveKeyFromPassphrase, generateMasterKey, readEncryptedVault, vaultFileExists, writeEncryptedVault };
44
+ //# sourceMappingURL=encrypted-file.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encrypted-file.d.mts","names":[],"sources":["../src/encrypted-file.ts"],"mappings":";;AAqCA;;;;;;;;;AAYA;;;;;AAOA;UAnBiB,SAAA;EACf,OAAA,EAAS,MAAA,SAAe,MAAA;AAAA;;;AA6B1B;iBAlBgB,iBAAA,CAAA,GAAqB,MAAA;;;;iBAOrB,uBAAA,CAAwB,UAAA,UAAoB,IAAA,EAAM,MAAA,GAAS,MAAA;;;;iBAWrD,mBAAA,CACpB,QAAA,UACA,IAAA,EAAM,SAAA,EACN,SAAA,EAAW,MAAA,GACV,OAAA;;;;;iBAmDmB,kBAAA,CACpB,QAAA,UACA,SAAA,EAAW,MAAA,GACV,OAAA,CAAQ,SAAA;;;;iBA+EW,eAAA,CAAgB,QAAA,WAAmB,OAAA"}
@@ -0,0 +1,153 @@
1
+ import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
2
+ import { chmod, mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
3
+ import { dirname } from "node:path";
4
+
5
+ //#region src/encrypted-file.ts
6
+ /**
7
+ * Encrypted file backend for vault storage.
8
+ *
9
+ * - AES-256-GCM encryption
10
+ * - Master key: 256-bit random or crypto.scrypt-derived from passphrase
11
+ * - File format: JSON header + encrypted blob
12
+ * - File permissions: 0600
13
+ */
14
+ /** Encryption parameters */
15
+ const KEY_LENGTH = 32;
16
+ const IV_LENGTH = 12;
17
+ const SALT_LENGTH = 32;
18
+ const AUTH_TAG_LENGTH = 16;
19
+ const SCRYPT_N = 2 ** 14;
20
+ const SCRYPT_R = 8;
21
+ const SCRYPT_P = 1;
22
+ const SCRYPT_KEYLEN = KEY_LENGTH;
23
+ const FILE_MODE = 384;
24
+ /** Magic bytes to identify vault files */
25
+ const MAGIC = "AFSVAULT";
26
+ const FORMAT_VERSION = 1;
27
+ /**
28
+ * Generate a random 256-bit master key.
29
+ */
30
+ function generateMasterKey() {
31
+ return randomBytes(KEY_LENGTH);
32
+ }
33
+ /**
34
+ * Derive a master key from a passphrase using crypto.scrypt.
35
+ */
36
+ function deriveKeyFromPassphrase(passphrase, salt) {
37
+ return scryptSync(passphrase, salt, SCRYPT_KEYLEN, {
38
+ N: SCRYPT_N,
39
+ r: SCRYPT_R,
40
+ p: SCRYPT_P
41
+ });
42
+ }
43
+ /**
44
+ * Encrypt vault data and write to file.
45
+ */
46
+ async function writeEncryptedVault(filePath, data, masterKey) {
47
+ const json = JSON.stringify(data);
48
+ const plaintext = Buffer.from(json, "utf-8");
49
+ const salt = randomBytes(SALT_LENGTH);
50
+ const iv = randomBytes(IV_LENGTH);
51
+ const cipher = createCipheriv("aes-256-gcm", masterKey, iv);
52
+ const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);
53
+ const authTag = cipher.getAuthTag();
54
+ const magic = Buffer.from(MAGIC, "ascii");
55
+ const version = Buffer.alloc(1);
56
+ version[0] = FORMAT_VERSION;
57
+ const fileContent = Buffer.concat([
58
+ magic,
59
+ version,
60
+ salt,
61
+ iv,
62
+ authTag,
63
+ encrypted
64
+ ]);
65
+ await mkdir(dirname(filePath), { recursive: true });
66
+ const tmpPath = `${filePath}.tmp.${process.pid}`;
67
+ try {
68
+ await writeFile(tmpPath, fileContent, { mode: FILE_MODE });
69
+ await rename(tmpPath, filePath);
70
+ } catch (err) {
71
+ try {
72
+ const { unlink } = await import("node:fs/promises");
73
+ await unlink(tmpPath);
74
+ } catch {}
75
+ throw err;
76
+ }
77
+ try {
78
+ if (((await stat(filePath)).mode & 511) !== FILE_MODE) await chmod(filePath, FILE_MODE);
79
+ } catch {}
80
+ }
81
+ /**
82
+ * Read and decrypt vault data from file.
83
+ * Returns null if file does not exist.
84
+ */
85
+ async function readEncryptedVault(filePath, masterKey) {
86
+ let fileContent;
87
+ try {
88
+ fileContent = await readFile(filePath);
89
+ } catch (err) {
90
+ if (err?.code === "ENOENT") return null;
91
+ throw err;
92
+ }
93
+ const magicLen = 8;
94
+ const minSize = magicLen + 1 + SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH;
95
+ if (fileContent.length < minSize) throw new Error("Vault file is corrupted: too small");
96
+ if (fileContent.subarray(0, magicLen).toString("ascii") !== MAGIC) throw new Error("Not a valid vault file");
97
+ const version = fileContent[magicLen];
98
+ if (version !== FORMAT_VERSION) throw new Error(`Unsupported vault format version: ${version}`);
99
+ let offset = magicLen + 1;
100
+ fileContent.subarray(offset, offset + SALT_LENGTH);
101
+ offset += SALT_LENGTH;
102
+ const iv = fileContent.subarray(offset, offset + IV_LENGTH);
103
+ offset += IV_LENGTH;
104
+ const authTag = fileContent.subarray(offset, offset + AUTH_TAG_LENGTH);
105
+ offset += AUTH_TAG_LENGTH;
106
+ const ciphertext = fileContent.subarray(offset);
107
+ const decipher = createDecipheriv("aes-256-gcm", masterKey, iv);
108
+ decipher.setAuthTag(authTag);
109
+ let plaintext;
110
+ try {
111
+ plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
112
+ } catch {
113
+ throw new Error("Vault decryption failed: wrong master key or corrupted data");
114
+ }
115
+ const json = plaintext.toString("utf-8");
116
+ try {
117
+ return JSON.parse(json);
118
+ } catch {
119
+ throw new Error("Vault data is corrupted: invalid JSON after decryption");
120
+ }
121
+ }
122
+ /**
123
+ * Read the per-file salt from a vault file header.
124
+ * Returns null if file does not exist or is not a valid vault file.
125
+ */
126
+ async function readVaultSalt(filePath) {
127
+ let fileContent;
128
+ try {
129
+ fileContent = await readFile(filePath);
130
+ } catch {
131
+ return null;
132
+ }
133
+ const magicLen = 8;
134
+ const minSize = magicLen + 1 + SALT_LENGTH;
135
+ if (fileContent.length < minSize) return null;
136
+ if (fileContent.subarray(0, magicLen).toString("ascii") !== MAGIC) return null;
137
+ return fileContent.subarray(magicLen + 1, magicLen + 1 + SALT_LENGTH);
138
+ }
139
+ /**
140
+ * Check if a vault file exists.
141
+ */
142
+ async function vaultFileExists(filePath) {
143
+ try {
144
+ await stat(filePath);
145
+ return true;
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+
151
+ //#endregion
152
+ export { deriveKeyFromPassphrase, generateMasterKey, readEncryptedVault, readVaultSalt, vaultFileExists, writeEncryptedVault };
153
+ //# sourceMappingURL=encrypted-file.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encrypted-file.mjs","names":[],"sources":["../src/encrypted-file.ts"],"sourcesContent":["/**\n * Encrypted file backend for vault storage.\n *\n * - AES-256-GCM encryption\n * - Master key: 256-bit random or crypto.scrypt-derived from passphrase\n * - File format: JSON header + encrypted blob\n * - File permissions: 0600\n */\n\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync } from \"node:crypto\";\nimport { chmod, mkdir, readFile, rename, stat, writeFile } from \"node:fs/promises\";\nimport { dirname } from \"node:path\";\n\n/** Encryption parameters */\nconst KEY_LENGTH = 32; // 256 bits\nconst IV_LENGTH = 12; // 96 bits for GCM\nconst SALT_LENGTH = 32;\nconst AUTH_TAG_LENGTH = 16;\nconst SCRYPT_N = 2 ** 14; // 16384 — compatible with Bun's BoringSSL limits\nconst SCRYPT_R = 8;\nconst SCRYPT_P = 1;\nconst SCRYPT_KEYLEN = KEY_LENGTH;\nconst FILE_MODE = 0o600;\n\n/** Magic bytes to identify vault files */\nconst MAGIC = \"AFSVAULT\";\nconst FORMAT_VERSION = 1;\n\n/** File header layout (binary):\n * 8 bytes — magic \"AFSVAULT\"\n * 1 byte — format version\n * 32 bytes — salt (for passphrase-derived keys; random for direct keys)\n * 12 bytes — IV\n * 16 bytes — auth tag\n * rest — ciphertext\n */\n\nexport interface VaultData {\n secrets: Record<string, Record<string, string>>;\n}\n\nexport interface EncryptedFileOptions {\n /** Path to vault.enc file */\n path: string;\n}\n\n/**\n * Generate a random 256-bit master key.\n */\nexport function generateMasterKey(): Buffer {\n return randomBytes(KEY_LENGTH);\n}\n\n/**\n * Derive a master key from a passphrase using crypto.scrypt.\n */\nexport function deriveKeyFromPassphrase(passphrase: string, salt: Buffer): Buffer {\n return scryptSync(passphrase, salt, SCRYPT_KEYLEN, {\n N: SCRYPT_N,\n r: SCRYPT_R,\n p: SCRYPT_P,\n }) as Buffer;\n}\n\n/**\n * Encrypt vault data and write to file.\n */\nexport async function writeEncryptedVault(\n filePath: string,\n data: VaultData,\n masterKey: Buffer,\n): Promise<void> {\n const json = JSON.stringify(data);\n const plaintext = Buffer.from(json, \"utf-8\");\n\n const salt = randomBytes(SALT_LENGTH);\n const iv = randomBytes(IV_LENGTH);\n\n const cipher = createCipheriv(\"aes-256-gcm\", masterKey, iv);\n const encrypted = Buffer.concat([cipher.update(plaintext), cipher.final()]);\n const authTag = cipher.getAuthTag();\n\n // Build file content\n const magic = Buffer.from(MAGIC, \"ascii\");\n const version = Buffer.alloc(1);\n version[0] = FORMAT_VERSION;\n\n const fileContent = Buffer.concat([magic, version, salt, iv, authTag, encrypted]);\n\n // Atomic write\n const dir = dirname(filePath);\n await mkdir(dir, { recursive: true });\n\n const tmpPath = `${filePath}.tmp.${process.pid}`;\n try {\n await writeFile(tmpPath, fileContent, { mode: FILE_MODE });\n await rename(tmpPath, filePath);\n } catch (err) {\n try {\n const { unlink } = await import(\"node:fs/promises\");\n await unlink(tmpPath);\n } catch {\n // ignore cleanup errors\n }\n throw err;\n }\n\n // Ensure permissions on first creation\n try {\n const s = await stat(filePath);\n if ((s.mode & 0o777) !== FILE_MODE) {\n await chmod(filePath, FILE_MODE);\n }\n } catch {\n // ignore stat errors\n }\n}\n\n/**\n * Read and decrypt vault data from file.\n * Returns null if file does not exist.\n */\nexport async function readEncryptedVault(\n filePath: string,\n masterKey: Buffer,\n): Promise<VaultData | null> {\n let fileContent: Buffer;\n try {\n fileContent = await readFile(filePath);\n } catch (err: any) {\n if (err?.code === \"ENOENT\") return null;\n throw err;\n }\n\n // Parse header\n const magicLen = MAGIC.length;\n const minSize = magicLen + 1 + SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH;\n if (fileContent.length < minSize) {\n throw new Error(\"Vault file is corrupted: too small\");\n }\n\n const magic = fileContent.subarray(0, magicLen).toString(\"ascii\");\n if (magic !== MAGIC) {\n throw new Error(\"Not a valid vault file\");\n }\n\n const version = fileContent[magicLen];\n if (version !== FORMAT_VERSION) {\n throw new Error(`Unsupported vault format version: ${version}`);\n }\n\n let offset = magicLen + 1;\n const _salt = fileContent.subarray(offset, offset + SALT_LENGTH);\n offset += SALT_LENGTH;\n const iv = fileContent.subarray(offset, offset + IV_LENGTH);\n offset += IV_LENGTH;\n const authTag = fileContent.subarray(offset, offset + AUTH_TAG_LENGTH);\n offset += AUTH_TAG_LENGTH;\n const ciphertext = fileContent.subarray(offset);\n\n // Decrypt\n const decipher = createDecipheriv(\"aes-256-gcm\", masterKey, iv);\n decipher.setAuthTag(authTag);\n\n let plaintext: Buffer;\n try {\n plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n } catch {\n throw new Error(\"Vault decryption failed: wrong master key or corrupted data\");\n }\n\n const json = plaintext.toString(\"utf-8\");\n try {\n return JSON.parse(json) as VaultData;\n } catch {\n throw new Error(\"Vault data is corrupted: invalid JSON after decryption\");\n }\n}\n\n/**\n * Read the per-file salt from a vault file header.\n * Returns null if file does not exist or is not a valid vault file.\n */\nexport async function readVaultSalt(filePath: string): Promise<Buffer | null> {\n let fileContent: Buffer;\n try {\n fileContent = await readFile(filePath);\n } catch {\n return null;\n }\n\n const magicLen = MAGIC.length;\n const minSize = magicLen + 1 + SALT_LENGTH;\n if (fileContent.length < minSize) return null;\n\n const magic = fileContent.subarray(0, magicLen).toString(\"ascii\");\n if (magic !== MAGIC) return null;\n\n return fileContent.subarray(magicLen + 1, magicLen + 1 + SALT_LENGTH);\n}\n\n/**\n * Check if a vault file exists.\n */\nexport async function vaultFileExists(filePath: string): Promise<boolean> {\n try {\n await stat(filePath);\n return true;\n } catch {\n return false;\n }\n}\n"],"mappings":"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"}