apiro-db 1.0.3 → 1.0.5
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 +5 -5
- package/lib/crypto.js +11 -21
- package/lib/masterKey.js +52 -0
- package/lib/store.js +32 -24
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A lightweight, zero-dependency, encrypted data store for Node.js.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
No configuration. No secrets. Just import and use.
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -11,7 +11,8 @@ Secure Store provides a simple key-value database similar to Quick.db, while rem
|
|
|
11
11
|
* Zero dependencies
|
|
12
12
|
* Fully asynchronous API
|
|
13
13
|
* AES-256-GCM encrypted storage
|
|
14
|
-
*
|
|
14
|
+
* Automatic key generation
|
|
15
|
+
* Machine-bound encryption
|
|
15
16
|
* Human-unreadable data at rest
|
|
16
17
|
* Simple, familiar database methods
|
|
17
18
|
|
|
@@ -28,11 +29,10 @@ npm install apiro-db
|
|
|
28
29
|
## Usage
|
|
29
30
|
|
|
30
31
|
```js
|
|
31
|
-
|
|
32
|
+
import { SecureStore } from "apiro-db";
|
|
32
33
|
|
|
33
34
|
const db = new SecureStore({
|
|
34
|
-
file: "./data.db",
|
|
35
|
-
secret: process.env.DB_SECRET
|
|
35
|
+
file: "./data.db", // Optional
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
await db.set("coins", 100);
|
package/lib/crypto.js
CHANGED
|
@@ -2,40 +2,30 @@ const crypto = require("crypto");
|
|
|
2
2
|
|
|
3
3
|
const ALGO = "aes-256-gcm";
|
|
4
4
|
const IV_LENGTH = 12;
|
|
5
|
-
const SALT_LENGTH = 16;
|
|
6
|
-
const TAG_LENGTH = 16;
|
|
7
5
|
|
|
8
|
-
function
|
|
9
|
-
return crypto.scryptSync(secret, salt, 32);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function encrypt(text, secret) {
|
|
6
|
+
function encrypt(text, key) {
|
|
13
7
|
const iv = crypto.randomBytes(IV_LENGTH);
|
|
14
|
-
const salt = crypto.randomBytes(SALT_LENGTH);
|
|
15
|
-
const key = deriveKey(secret, salt);
|
|
16
|
-
|
|
17
8
|
const cipher = crypto.createCipheriv(ALGO, key, iv);
|
|
9
|
+
|
|
18
10
|
const encrypted = Buffer.concat([
|
|
19
11
|
cipher.update(text, "utf8"),
|
|
20
12
|
cipher.final()
|
|
21
13
|
]);
|
|
22
14
|
|
|
23
|
-
|
|
24
|
-
|
|
15
|
+
return Buffer.concat([
|
|
16
|
+
iv,
|
|
17
|
+
cipher.getAuthTag(),
|
|
18
|
+
encrypted
|
|
19
|
+
]).toString("base64");
|
|
25
20
|
}
|
|
26
21
|
|
|
27
|
-
function decrypt(payload,
|
|
22
|
+
function decrypt(payload, key) {
|
|
28
23
|
const buffer = Buffer.from(payload, "base64");
|
|
29
24
|
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
SALT_LENGTH + IV_LENGTH,
|
|
34
|
-
SALT_LENGTH + IV_LENGTH + TAG_LENGTH
|
|
35
|
-
);
|
|
36
|
-
const encrypted = buffer.subarray(SALT_LENGTH + IV_LENGTH + TAG_LENGTH);
|
|
25
|
+
const iv = buffer.subarray(0, 12);
|
|
26
|
+
const tag = buffer.subarray(12, 28);
|
|
27
|
+
const encrypted = buffer.subarray(28);
|
|
37
28
|
|
|
38
|
-
const key = deriveKey(secret, salt);
|
|
39
29
|
const decipher = crypto.createDecipheriv(ALGO, key, iv);
|
|
40
30
|
decipher.setAuthTag(tag);
|
|
41
31
|
|
package/lib/masterKey.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
|
|
4
|
+
const ALGO = "aes-256-gcm";
|
|
5
|
+
|
|
6
|
+
function getMachineKey() {
|
|
7
|
+
const fingerprint = [
|
|
8
|
+
os.hostname(),
|
|
9
|
+
os.platform(),
|
|
10
|
+
os.arch()
|
|
11
|
+
].join("|");
|
|
12
|
+
|
|
13
|
+
return crypto.scryptSync(fingerprint, "secure-store", 32);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function encryptMasterKey(masterKey) {
|
|
17
|
+
const iv = crypto.randomBytes(12);
|
|
18
|
+
const key = getMachineKey();
|
|
19
|
+
|
|
20
|
+
const cipher = crypto.createCipheriv(ALGO, key, iv);
|
|
21
|
+
const encrypted = Buffer.concat([
|
|
22
|
+
cipher.update(masterKey),
|
|
23
|
+
cipher.final()
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
iv: iv.toString("base64"),
|
|
28
|
+
tag: cipher.getAuthTag().toString("base64"),
|
|
29
|
+
data: encrypted.toString("base64")
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function decryptMasterKey(payload) {
|
|
34
|
+
const key = getMachineKey();
|
|
35
|
+
const iv = Buffer.from(payload.iv, "base64");
|
|
36
|
+
const tag = Buffer.from(payload.tag, "base64");
|
|
37
|
+
const encrypted = Buffer.from(payload.data, "base64");
|
|
38
|
+
|
|
39
|
+
const decipher = crypto.createDecipheriv(ALGO, key, iv);
|
|
40
|
+
decipher.setAuthTag(tag);
|
|
41
|
+
|
|
42
|
+
return Buffer.concat([
|
|
43
|
+
decipher.update(encrypted),
|
|
44
|
+
decipher.final()
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
generateMasterKey: () => crypto.randomBytes(32),
|
|
50
|
+
encryptMasterKey,
|
|
51
|
+
decryptMasterKey
|
|
52
|
+
};
|
package/lib/store.js
CHANGED
|
@@ -1,34 +1,46 @@
|
|
|
1
|
+
const crypto = require("crypto");
|
|
1
2
|
const { readFile, writeFile } = require("./file");
|
|
2
3
|
const { encrypt, decrypt } = require("./crypto");
|
|
4
|
+
const { generateMasterKey, encryptMasterKey, decryptMasterKey } = require("./masterKey");
|
|
3
5
|
|
|
4
6
|
class SecureStore {
|
|
5
7
|
constructor(options = {}) {
|
|
6
|
-
|
|
7
|
-
throw new Error("SecureStore requires a secret key");
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
this.file = options.file || "./apiro.db";
|
|
11
|
-
this.secret = options.secret;
|
|
8
|
+
this.file = options.file || "./secure.db";
|
|
12
9
|
this.data = {};
|
|
13
|
-
this.
|
|
10
|
+
this.masterKey = null;
|
|
11
|
+
this.ready = this._init();
|
|
14
12
|
}
|
|
15
13
|
|
|
16
|
-
async
|
|
17
|
-
const
|
|
18
|
-
|
|
14
|
+
async _init() {
|
|
15
|
+
const existing = await readFile(this.file);
|
|
16
|
+
|
|
17
|
+
if (!existing) {
|
|
18
|
+
this.masterKey = crypto.randomBytes(32);
|
|
19
|
+
const encryptedKey = encryptMasterKey(this.masterKey);
|
|
20
|
+
|
|
21
|
+
const payload = encrypt(JSON.stringify({}), this.masterKey);
|
|
22
|
+
|
|
23
|
+
await writeFile(this.file, JSON.stringify({
|
|
24
|
+
_meta: { key: encryptedKey },
|
|
25
|
+
payload
|
|
26
|
+
}));
|
|
19
27
|
|
|
20
|
-
|
|
21
|
-
const decrypted = decrypt(encrypted, this.secret);
|
|
22
|
-
this.data = JSON.parse(decrypted);
|
|
23
|
-
} catch {
|
|
24
|
-
throw new Error("Failed to decrypt data store. Invalid secret?");
|
|
28
|
+
return;
|
|
25
29
|
}
|
|
30
|
+
|
|
31
|
+
const parsed = JSON.parse(existing);
|
|
32
|
+
this.masterKey = decryptMasterKey(parsed._meta.key);
|
|
33
|
+
const decrypted = decrypt(parsed.payload, this.masterKey);
|
|
34
|
+
this.data = JSON.parse(decrypted);
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
async _save() {
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
await writeFile(this.file,
|
|
38
|
+
const payload = encrypt(JSON.stringify(this.data), this.masterKey);
|
|
39
|
+
|
|
40
|
+
await writeFile(this.file, JSON.stringify({
|
|
41
|
+
_meta: { key: encryptMasterKey(this.masterKey) },
|
|
42
|
+
payload
|
|
43
|
+
}));
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
async get(key) {
|
|
@@ -53,9 +65,7 @@ class SecureStore {
|
|
|
53
65
|
|
|
54
66
|
async add(key, amount) {
|
|
55
67
|
await this.ready;
|
|
56
|
-
if (typeof this.data[key] !== "number")
|
|
57
|
-
this.data[key] = 0;
|
|
58
|
-
}
|
|
68
|
+
if (typeof this.data[key] !== "number") this.data[key] = 0;
|
|
59
69
|
this.data[key] += amount;
|
|
60
70
|
await this._save();
|
|
61
71
|
return this.data[key];
|
|
@@ -67,9 +77,7 @@ class SecureStore {
|
|
|
67
77
|
|
|
68
78
|
async push(key, value) {
|
|
69
79
|
await this.ready;
|
|
70
|
-
if (!Array.isArray(this.data[key]))
|
|
71
|
-
this.data[key] = [];
|
|
72
|
-
}
|
|
80
|
+
if (!Array.isArray(this.data[key])) this.data[key] = [];
|
|
73
81
|
this.data[key].push(value);
|
|
74
82
|
await this._save();
|
|
75
83
|
return this.data[key];
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apiro-db",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "A lightweight, zero-dependency, encrypted data store for Node.js.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
8
|
},
|
|
9
|
-
"keywords": [],
|
|
9
|
+
"keywords": ["database", "storage", "encryption", "secure"],
|
|
10
10
|
"author": "krispowers",
|
|
11
11
|
"license": "ISC",
|
|
12
12
|
"files": [
|