@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 +26 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.cjs +11 -0
- package/dist/_virtual/_@oxc-project_runtime@0.108.0/helpers/decorate.mjs +10 -0
- package/dist/encrypted-file.cjs +157 -0
- package/dist/encrypted-file.d.cts +44 -0
- package/dist/encrypted-file.d.cts.map +1 -0
- package/dist/encrypted-file.d.mts +44 -0
- package/dist/encrypted-file.d.mts.map +1 -0
- package/dist/encrypted-file.mjs +153 -0
- package/dist/encrypted-file.mjs.map +1 -0
- package/dist/index.cjs +408 -0
- package/dist/index.d.cts +77 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +77 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +400 -0
- package/dist/index.mjs.map +1 -0
- package/dist/key-resolver.cjs +219 -0
- package/dist/key-resolver.d.cts +35 -0
- package/dist/key-resolver.d.cts.map +1 -0
- package/dist/key-resolver.d.mts +35 -0
- package/dist/key-resolver.d.mts.map +1 -0
- package/dist/key-resolver.mjs +217 -0
- package/dist/key-resolver.mjs.map +1 -0
- package/package.json +57 -0
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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"}
|