@hackthedev/dsync-sign 1.0.10 → 1.0.11
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/.gitattributes +2 -0
- package/.github/workflows/publish.yml +43 -0
- package/README.md +1 -103
- package/index.mjs +341 -310
- package/js/README.md +112 -0
- package/js/index.mjs +341 -0
- package/package.json +6 -2
package/.gitattributes
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
publish:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
with:
|
|
18
|
+
persist-credentials: true
|
|
19
|
+
|
|
20
|
+
- name: Skip version bump commits
|
|
21
|
+
run: |
|
|
22
|
+
if git log -1 --pretty=%B | grep -q "chore: bump version"; then
|
|
23
|
+
echo "Version bump commit detected, skipping."
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
- uses: actions/setup-node@v4
|
|
28
|
+
with:
|
|
29
|
+
node-version: 20
|
|
30
|
+
registry-url: https://registry.npmjs.org/
|
|
31
|
+
|
|
32
|
+
- run: npm ci
|
|
33
|
+
|
|
34
|
+
- run: |
|
|
35
|
+
git config user.name "github-actions"
|
|
36
|
+
git config user.email "actions@github.com"
|
|
37
|
+
npm version patch -m "chore: bump version %s"
|
|
38
|
+
git push
|
|
39
|
+
|
|
40
|
+
- run: npm publish --access public
|
|
41
|
+
env:
|
|
42
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
43
|
+
|
package/README.md
CHANGED
|
@@ -7,106 +7,4 @@ dSyncSign is an additional package that comes with a few helper functions that c
|
|
|
7
7
|
- Ability to verify signed strings/objects using a known public key
|
|
8
8
|
- Ability to encrypt and decrypt data with a private key or password
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
## Importing
|
|
13
|
-
|
|
14
|
-
You can import the package with the following line into your code and install it like below
|
|
15
|
-
|
|
16
|
-
```js
|
|
17
|
-
import { dSyncSign } from "@hackthedev/dsync-sign";
|
|
18
|
-
|
|
19
|
-
const signer = new dSyncSign("./mykeys.json"); // optional path for private key file
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
```sh
|
|
23
|
-
npm i @hackthedev/dsync-sign
|
|
24
|
-
```
|
|
25
|
-
|
|
26
|
-
------
|
|
27
|
-
|
|
28
|
-
## Signing & Verifying JSON Objects
|
|
29
|
-
|
|
30
|
-
You can also sign and very JSON objects pretty easily.
|
|
31
|
-
|
|
32
|
-
```js
|
|
33
|
-
const obj = { hello: "world" };
|
|
34
|
-
await signer.signJson(obj);
|
|
35
|
-
console.log("Object with sig", obj);
|
|
36
|
-
|
|
37
|
-
const verified = await signer.verifyJson(obj, await signer.getPublicKey());
|
|
38
|
-
console.log("JSON valid?", verified);
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Output:
|
|
42
|
-
|
|
43
|
-
```sh
|
|
44
|
-
Object with sig {
|
|
45
|
-
hello: 'world',
|
|
46
|
-
sig: 'W3tGrkWdCT62Zc7eJKM2Pr13CgsQc65diH4N5d0pGasyKEpWQVZG5wz6WhlKoJmYqE8O4OSIcm/WVCBtnZM66zpic0PAtuGaTKt224AO/zDWrQhuCDflvR29OHzeKcnHXNVS924PXK24dA2MiILTYlSbGLguIw0bfIWN1hDeVHYWu3VeDmOSBFUlkaviJzxV/lALRSBySIDd5SFFQQWfk0hLv0Hy8MMHzGQetrs9/l5mBLGU8iSrA85alXFN+OKz0Qo57zgPV5cBCl19LB/ZL0oR+GsQv171Jn04UO8hFUsyJJqI2VnPAw11LgPqwXqHUDuQwdCS7zvTyDmlM7+rvA=='
|
|
47
|
-
}
|
|
48
|
-
JSON valid? true
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
------
|
|
52
|
-
|
|
53
|
-
## Verifying & Signing nested JSON Objects
|
|
54
|
-
|
|
55
|
-
Its also possible to sign nested or specific objects.
|
|
56
|
-
|
|
57
|
-
```js
|
|
58
|
-
const obj = {
|
|
59
|
-
hello: {
|
|
60
|
-
world: "hi"
|
|
61
|
-
},
|
|
62
|
-
bye: {
|
|
63
|
-
crazy: "indeed"
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
await signer.signJson(obj.hello);
|
|
67
|
-
console.log("Object with sig", obj);
|
|
68
|
-
|
|
69
|
-
const verified = await signer.verifyJson(obj.hello, await signer.getPublicKey());
|
|
70
|
-
console.log("JSON valid?", verified);
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Output:
|
|
74
|
-
|
|
75
|
-
```sh
|
|
76
|
-
Object with sig {
|
|
77
|
-
hello: {
|
|
78
|
-
world: 'hi',
|
|
79
|
-
sig: 'qEsJ8O7HVohexEFpvjVljfvSXdPp93DHAcg1PiLxNA0TjE48FdHd11cS5vYJlLPmpPEG/80cqETsHwlCjTiOZI6xC90IxdGTKGttjv1gFYM5bOgQlgcLW83BtlWdC0PES3xU5nEUCiNfXNKSeUT8HJTEsggQ6c17WjMcunZENEWiRqCQNY3ZXzvrqGKrJ/mm9BrRsgaFZMRh5j0eUhT1eJ4pVp6fleTAYIumuagpwG41MR3CG57dImxCoeFAcCDMikJEQKBknmhaDsEa9UFHzl8+hTsroI30ktTK7kOPf4XKbkuNGX+lZZwZPlWkfh/sQLSD59psvJDVvEQTrX1/KQ=='
|
|
80
|
-
},
|
|
81
|
-
bye: { crazy: 'indeed' }
|
|
82
|
-
}
|
|
83
|
-
JSON valid? true
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
------
|
|
87
|
-
|
|
88
|
-
## Encryption & Decryption using password
|
|
89
|
-
|
|
90
|
-
```js
|
|
91
|
-
const secretMsg = "This is some secret text";
|
|
92
|
-
|
|
93
|
-
const envPwd = await signer.encrypt(secretMsg, "somepass1234");
|
|
94
|
-
console.log("Envelope (Password):", envPwd);
|
|
95
|
-
|
|
96
|
-
const decPwd = await signer.decrypt(envPwd, "somepass1234");
|
|
97
|
-
console.log("Decrypted (Password):", decPwd);
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
Output:
|
|
101
|
-
|
|
102
|
-
```sh
|
|
103
|
-
Envelope (Password): {
|
|
104
|
-
method: 'password',
|
|
105
|
-
salt: 'QxGLufg1lXn6boTVHme8+Q==',
|
|
106
|
-
iv: 'yum57MIAZc0hUBLXPd7hxQ==',
|
|
107
|
-
tag: 'jok95AmgKTj0okoLrFeL0A==',
|
|
108
|
-
ciphertext: 'cDz+F+z8i9emqTO5COKOYfnYWxTB4spC'
|
|
109
|
-
}
|
|
110
|
-
Decrypted (Password): This is some secret text
|
|
111
|
-
```
|
|
112
|
-
|
|
10
|
+
Folders like `/js` are meant to show the current available implementations, meaning as of right now dSyncSign is only available in JavaScript/NodeJS.
|
package/index.mjs
CHANGED
|
@@ -1,310 +1,341 @@
|
|
|
1
|
-
import {promises as fs} from "fs";
|
|
2
|
-
import crypto from "crypto";
|
|
3
|
-
|
|
4
|
-
export class dSyncSign {
|
|
5
|
-
constructor(keyFile = "./privatekey.json") {
|
|
6
|
-
this.KEY_FILE = keyFile;
|
|
7
|
-
this.sigField = "sig";
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
canonicalize(x) {
|
|
11
|
-
if (x === null || typeof x !== "object") return x;
|
|
12
|
-
if (Array.isArray(x)) return x.map(v => this.canonicalize(v));
|
|
13
|
-
const out = {};
|
|
14
|
-
|
|
15
|
-
for (const k of Object.keys(x).sort()) out[k] = this.canonicalize(x[k]);
|
|
16
|
-
return out;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
stableStringify(obj) {
|
|
20
|
-
return JSON.stringify(this.canonicalize(obj));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
normalizePublicKey(key) {
|
|
24
|
-
return key
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.replace(
|
|
29
|
-
.replace(
|
|
30
|
-
.replace(/
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
let
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
1
|
+
import {promises as fs} from "fs";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
|
|
4
|
+
export class dSyncSign {
|
|
5
|
+
constructor(keyFile = "./privatekey.json") {
|
|
6
|
+
this.KEY_FILE = keyFile;
|
|
7
|
+
this.sigField = "sig";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
canonicalize(x) {
|
|
11
|
+
if (x === null || typeof x !== "object") return x;
|
|
12
|
+
if (Array.isArray(x)) return x.map(v => this.canonicalize(v));
|
|
13
|
+
const out = {};
|
|
14
|
+
|
|
15
|
+
for (const k of Object.keys(x).sort()) out[k] = this.canonicalize(x[k]);
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
stableStringify(obj) {
|
|
20
|
+
return JSON.stringify(this.canonicalize(obj));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
normalizePublicKey(key) {
|
|
24
|
+
if (!key) return key;
|
|
25
|
+
|
|
26
|
+
// fuck you
|
|
27
|
+
key = String(key)
|
|
28
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
29
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
30
|
+
.replace(/\r\n/g, "\n")
|
|
31
|
+
.replace(/\r/g, "\n")
|
|
32
|
+
.trim();
|
|
33
|
+
|
|
34
|
+
if (key.includes("BEGIN PUBLIC KEY") || key.includes("BEGIN RSA PUBLIC KEY")) {
|
|
35
|
+
key = key
|
|
36
|
+
.replace(/\n+/g, "\n")
|
|
37
|
+
.replace(/-----BEGIN PUBLIC KEY-----\s*/g, "-----BEGIN PUBLIC KEY-----\n")
|
|
38
|
+
.replace(/-----END PUBLIC KEY-----/g, "\n-----END PUBLIC KEY-----")
|
|
39
|
+
.replace(/-----BEGIN RSA PUBLIC KEY-----\s*/g, "-----BEGIN RSA PUBLIC KEY-----\n")
|
|
40
|
+
.replace(/-----END RSA PUBLIC KEY-----/g, "\n-----END RSA PUBLIC KEY-----");
|
|
41
|
+
|
|
42
|
+
const isRsa = key.includes("BEGIN RSA PUBLIC KEY");
|
|
43
|
+
const begin = isRsa ? "-----BEGIN RSA PUBLIC KEY-----" : "-----BEGIN PUBLIC KEY-----";
|
|
44
|
+
const end = isRsa ? "-----END RSA PUBLIC KEY-----" : "-----END PUBLIC KEY-----";
|
|
45
|
+
|
|
46
|
+
let body = key
|
|
47
|
+
.replace(begin, "")
|
|
48
|
+
.replace(end, "")
|
|
49
|
+
.replace(/\s+/g, "");
|
|
50
|
+
|
|
51
|
+
body = body.match(/.{1,64}/g)?.join("\n") || body;
|
|
52
|
+
return `${begin}\n${body}\n${end}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return key;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async ensureKeyPair() {
|
|
59
|
+
try {
|
|
60
|
+
const raw = await fs.readFile(this.KEY_FILE, "utf8");
|
|
61
|
+
const {privateKey} = JSON.parse(raw);
|
|
62
|
+
|
|
63
|
+
crypto.createPrivateKey(privateKey);
|
|
64
|
+
|
|
65
|
+
const pubKey = crypto.createPublicKey(privateKey).export({type: "spki", format: "pem"});
|
|
66
|
+
return {privateKey, publicKey: pubKey.toString()};
|
|
67
|
+
} catch {
|
|
68
|
+
const {privateKey, publicKey} = crypto.generateKeyPairSync("rsa", {
|
|
69
|
+
modulusLength: 2048,
|
|
70
|
+
publicKeyEncoding: {type: "spki", format: "pem"},
|
|
71
|
+
privateKeyEncoding: {type: "pkcs8", format: "pem"}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await fs.writeFile(this.KEY_FILE, JSON.stringify({privateKey}, null, 2), {encoding: "utf8", mode: 0o600});
|
|
75
|
+
return {privateKey, publicKey};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async signString(text) {
|
|
80
|
+
const priv = await this.getPrivateKey();
|
|
81
|
+
const signer = crypto.createSign("SHA256");
|
|
82
|
+
signer.update(text, "utf8");
|
|
83
|
+
signer.end();
|
|
84
|
+
return signer.sign(priv, "base64");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
verifyString(text, signatureBase64, publicKeyPem) {
|
|
89
|
+
publicKeyPem = this.normalizePublicKey(publicKeyPem);
|
|
90
|
+
|
|
91
|
+
const verifier = crypto.createVerify("SHA256");
|
|
92
|
+
verifier.update(text, "utf8");
|
|
93
|
+
verifier.end();
|
|
94
|
+
return verifier.verify(publicKeyPem, signatureBase64, "base64");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
generateGid(publicKey) {
|
|
99
|
+
if (publicKey.length >= 120) {
|
|
100
|
+
return this.encodeToBase64(publicKey.substring(80, 120)) // 40 chars
|
|
101
|
+
} else {
|
|
102
|
+
return this.encodeToBase64(publicKey.substring(0, publicKey.length))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
encodeToBase64(str) {
|
|
107
|
+
return Buffer.from(str, "utf8").toString("base64")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async getPrivateKey() {
|
|
112
|
+
const {privateKey} = await this.ensureKeyPair();
|
|
113
|
+
return privateKey;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async getPublicKey() {
|
|
117
|
+
const {publicKey} = await this.ensureKeyPair();
|
|
118
|
+
return publicKey;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async encrypt(data, recipient) {
|
|
122
|
+
const plaintext = typeof data === "string" ? data : this.stableStringify(data);
|
|
123
|
+
|
|
124
|
+
if (typeof recipient === "string") {
|
|
125
|
+
recipient = this.normalizePublicKey(recipient);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let aesKey;
|
|
129
|
+
let envelope = {method: ""};
|
|
130
|
+
|
|
131
|
+
if (typeof recipient === "string" && (recipient.includes("BEGIN PUBLIC KEY") || recipient.includes("BEGIN RSA PUBLIC KEY"))) {
|
|
132
|
+
aesKey = crypto.randomBytes(32);
|
|
133
|
+
envelope.method = "rsa";
|
|
134
|
+
envelope.encKey = crypto.publicEncrypt(
|
|
135
|
+
{key: recipient, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING},
|
|
136
|
+
aesKey
|
|
137
|
+
).toString("base64");
|
|
138
|
+
} else {
|
|
139
|
+
const salt = crypto.randomBytes(16);
|
|
140
|
+
aesKey = crypto.pbkdf2Sync(recipient, salt, 100000, 32, "sha256");
|
|
141
|
+
envelope.method = "password";
|
|
142
|
+
envelope.salt = salt.toString("base64");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const iv = crypto.randomBytes(12);
|
|
146
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
|
|
147
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
148
|
+
const tag = cipher.getAuthTag();
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
...envelope,
|
|
152
|
+
iv: iv.toString("base64"),
|
|
153
|
+
tag: tag.toString("base64"),
|
|
154
|
+
ciphertext: ciphertext.toString("base64")
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async decrypt(envelope, password = null) {
|
|
159
|
+
let aesKey;
|
|
160
|
+
if (envelope.method === "rsa") {
|
|
161
|
+
const priv = await this.getPrivateKey();
|
|
162
|
+
aesKey = crypto.privateDecrypt(
|
|
163
|
+
{key: priv, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING},
|
|
164
|
+
Buffer.from(envelope.encKey, "base64")
|
|
165
|
+
);
|
|
166
|
+
} else if (envelope.method === "password") {
|
|
167
|
+
if (!password) throw new Error("Password required for password-based decryption");
|
|
168
|
+
aesKey = crypto.pbkdf2Sync(
|
|
169
|
+
password,
|
|
170
|
+
Buffer.from(envelope.salt, "base64"),
|
|
171
|
+
100000,
|
|
172
|
+
32,
|
|
173
|
+
"sha256"
|
|
174
|
+
);
|
|
175
|
+
} else {
|
|
176
|
+
throw new Error("Unsupported envelope method");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const iv = Buffer.from(envelope.iv, "base64");
|
|
180
|
+
const tag = Buffer.from(envelope.tag, "base64");
|
|
181
|
+
const ciphertext = Buffer.from(envelope.ciphertext, "base64");
|
|
182
|
+
|
|
183
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
184
|
+
decipher.setAuthTag(tag);
|
|
185
|
+
|
|
186
|
+
const dec = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
187
|
+
const txt = dec.toString("utf8");
|
|
188
|
+
try {
|
|
189
|
+
return JSON.parse(txt);
|
|
190
|
+
} catch {
|
|
191
|
+
return txt;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async signData(data) {
|
|
196
|
+
const priv = await this.getPrivateKey();
|
|
197
|
+
const signer = crypto.createSign("SHA256");
|
|
198
|
+
const payload = typeof data === "string" ? data : this.stableStringify(data);
|
|
199
|
+
|
|
200
|
+
signer.update(payload, "utf8");
|
|
201
|
+
signer.end();
|
|
202
|
+
|
|
203
|
+
return signer.sign(priv, "base64");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
verifyData(data, signature, publicKey) {
|
|
207
|
+
publicKey = this.normalizePublicKey(publicKey);
|
|
208
|
+
|
|
209
|
+
const verifier = crypto.createVerify("SHA256");
|
|
210
|
+
const payload = typeof data === "string" ? data : this.stableStringify(data);
|
|
211
|
+
|
|
212
|
+
verifier.update(payload, "utf8");
|
|
213
|
+
verifier.end();
|
|
214
|
+
|
|
215
|
+
return verifier.verify(publicKey, signature, "base64");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
getByPath(root, path) {
|
|
219
|
+
if (!path) return root;
|
|
220
|
+
const re = /([^.\[\]]+)|\[(\d+)\]/g;
|
|
221
|
+
const parts = [];
|
|
222
|
+
let m;
|
|
223
|
+
|
|
224
|
+
while ((m = re.exec(path)) !== null) parts.push(m[1] !== undefined ? m[1] : Number(m[2]));
|
|
225
|
+
let cur = root;
|
|
226
|
+
|
|
227
|
+
for (const p of parts) {
|
|
228
|
+
if (cur == null) return undefined;
|
|
229
|
+
cur = cur[p];
|
|
230
|
+
}
|
|
231
|
+
return cur;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
cloneWithoutSig(obj) {
|
|
235
|
+
if (obj == null || typeof obj !== "object") return obj;
|
|
236
|
+
let copy;
|
|
237
|
+
|
|
238
|
+
if (typeof structuredClone === "function") {
|
|
239
|
+
try {
|
|
240
|
+
copy = structuredClone(obj);
|
|
241
|
+
} catch {
|
|
242
|
+
copy = JSON.parse(JSON.stringify(obj));
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
copy = JSON.parse(JSON.stringify(obj));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (copy && Object.prototype.hasOwnProperty.call(copy, this.sigField)) delete copy[this.sigField];
|
|
249
|
+
return copy;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async signJson(targetOrRoot, path) {
|
|
253
|
+
let target = path ? this.getByPath(targetOrRoot, path) : targetOrRoot;
|
|
254
|
+
|
|
255
|
+
if (target == null) {
|
|
256
|
+
if (path) return false;
|
|
257
|
+
throw new TypeError("target required");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (Array.isArray(target)) {
|
|
261
|
+
const out = [];
|
|
262
|
+
for (const item of target) {
|
|
263
|
+
if (item == null || typeof item !== "object") {
|
|
264
|
+
out.push(null);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (Object.prototype.hasOwnProperty.call(item, this.sigField)) {
|
|
268
|
+
out.push(item[this.sigField]);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const payload = this.cloneWithoutSig(item);
|
|
272
|
+
const s = await this.signData(payload);
|
|
273
|
+
|
|
274
|
+
item[this.sigField] = s;
|
|
275
|
+
out.push(s);
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (typeof target === "object") {
|
|
281
|
+
if (Object.prototype.hasOwnProperty.call(target, this.sigField)) return target[this.sigField];
|
|
282
|
+
const payload = this.cloneWithoutSig(target);
|
|
283
|
+
const s = await this.signData(payload);
|
|
284
|
+
|
|
285
|
+
target[this.sigField] = s;
|
|
286
|
+
return s;
|
|
287
|
+
}
|
|
288
|
+
throw new TypeError("target must be object or array");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async verifyJson(targetOrRoot, publicKeyOrGetter, path) {
|
|
292
|
+
let target = path ? this.getByPath(targetOrRoot, path) : targetOrRoot;
|
|
293
|
+
|
|
294
|
+
if (target == null) {
|
|
295
|
+
if (path) return false;
|
|
296
|
+
throw new TypeError("target required");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (Array.isArray(target)) {
|
|
300
|
+
const out = [];
|
|
301
|
+
|
|
302
|
+
for (const item of target) {
|
|
303
|
+
if (item == null || typeof item !== "object") {
|
|
304
|
+
out.push(false);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!Object.prototype.hasOwnProperty.call(item, this.sigField)) {
|
|
309
|
+
out.push(false);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const signature = item[this.sigField];
|
|
314
|
+
let pub = publicKeyOrGetter;
|
|
315
|
+
|
|
316
|
+
if (typeof publicKeyOrGetter === "function") pub = await publicKeyOrGetter(item, targetOrRoot);
|
|
317
|
+
if (!pub) {
|
|
318
|
+
out.push(false);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const payload = this.cloneWithoutSig(item);
|
|
323
|
+
out.push(Boolean(this.verifyData(payload, signature, pub)));
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
if (typeof target === "object") {
|
|
328
|
+
if (!Object.prototype.hasOwnProperty.call(target, this.sigField)) return false;
|
|
329
|
+
|
|
330
|
+
const signature = target[this.sigField];
|
|
331
|
+
let pub = publicKeyOrGetter;
|
|
332
|
+
|
|
333
|
+
if (typeof publicKeyOrGetter === "function") pub = await publicKeyOrGetter(target, targetOrRoot);
|
|
334
|
+
if (!pub) return false;
|
|
335
|
+
|
|
336
|
+
const payload = this.cloneWithoutSig(target);
|
|
337
|
+
return Boolean(this.verifyData(payload, signature, pub));
|
|
338
|
+
}
|
|
339
|
+
throw new TypeError("target must be object or array");
|
|
340
|
+
}
|
|
341
|
+
}
|
package/js/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# dSyncSign
|
|
2
|
+
|
|
3
|
+
dSyncSign is an additional package that comes with a few helper functions that can enhance the plain dSync package. dSyncSign comes with the following features:
|
|
4
|
+
|
|
5
|
+
- Creation of a private key file and public key
|
|
6
|
+
- Ability to sign strings or json objects, or even nested objects inside a json object
|
|
7
|
+
- Ability to verify signed strings/objects using a known public key
|
|
8
|
+
- Ability to encrypt and decrypt data with a private key or password
|
|
9
|
+
|
|
10
|
+
------
|
|
11
|
+
|
|
12
|
+
## Importing
|
|
13
|
+
|
|
14
|
+
You can import the package with the following line into your code and install it like below
|
|
15
|
+
|
|
16
|
+
```js
|
|
17
|
+
import { dSyncSign } from "@hackthedev/dsync-sign";
|
|
18
|
+
|
|
19
|
+
const signer = new dSyncSign("./mykeys.json"); // optional path for private key file
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
npm i @hackthedev/dsync-sign
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
------
|
|
27
|
+
|
|
28
|
+
## Signing & Verifying JSON Objects
|
|
29
|
+
|
|
30
|
+
You can also sign and very JSON objects pretty easily.
|
|
31
|
+
|
|
32
|
+
```js
|
|
33
|
+
const obj = { hello: "world" };
|
|
34
|
+
await signer.signJson(obj);
|
|
35
|
+
console.log("Object with sig", obj);
|
|
36
|
+
|
|
37
|
+
const verified = await signer.verifyJson(obj, await signer.getPublicKey());
|
|
38
|
+
console.log("JSON valid?", verified);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Output:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
Object with sig {
|
|
45
|
+
hello: 'world',
|
|
46
|
+
sig: 'W3tGrkWdCT62Zc7eJKM2Pr13CgsQc65diH4N5d0pGasyKEpWQVZG5wz6WhlKoJmYqE8O4OSIcm/WVCBtnZM66zpic0PAtuGaTKt224AO/zDWrQhuCDflvR29OHzeKcnHXNVS924PXK24dA2MiILTYlSbGLguIw0bfIWN1hDeVHYWu3VeDmOSBFUlkaviJzxV/lALRSBySIDd5SFFQQWfk0hLv0Hy8MMHzGQetrs9/l5mBLGU8iSrA85alXFN+OKz0Qo57zgPV5cBCl19LB/ZL0oR+GsQv171Jn04UO8hFUsyJJqI2VnPAw11LgPqwXqHUDuQwdCS7zvTyDmlM7+rvA=='
|
|
47
|
+
}
|
|
48
|
+
JSON valid? true
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
------
|
|
52
|
+
|
|
53
|
+
## Verifying & Signing nested JSON Objects
|
|
54
|
+
|
|
55
|
+
Its also possible to sign nested or specific objects.
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
const obj = {
|
|
59
|
+
hello: {
|
|
60
|
+
world: "hi"
|
|
61
|
+
},
|
|
62
|
+
bye: {
|
|
63
|
+
crazy: "indeed"
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
await signer.signJson(obj.hello);
|
|
67
|
+
console.log("Object with sig", obj);
|
|
68
|
+
|
|
69
|
+
const verified = await signer.verifyJson(obj.hello, await signer.getPublicKey());
|
|
70
|
+
console.log("JSON valid?", verified);
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Output:
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
Object with sig {
|
|
77
|
+
hello: {
|
|
78
|
+
world: 'hi',
|
|
79
|
+
sig: 'qEsJ8O7HVohexEFpvjVljfvSXdPp93DHAcg1PiLxNA0TjE48FdHd11cS5vYJlLPmpPEG/80cqETsHwlCjTiOZI6xC90IxdGTKGttjv1gFYM5bOgQlgcLW83BtlWdC0PES3xU5nEUCiNfXNKSeUT8HJTEsggQ6c17WjMcunZENEWiRqCQNY3ZXzvrqGKrJ/mm9BrRsgaFZMRh5j0eUhT1eJ4pVp6fleTAYIumuagpwG41MR3CG57dImxCoeFAcCDMikJEQKBknmhaDsEa9UFHzl8+hTsroI30ktTK7kOPf4XKbkuNGX+lZZwZPlWkfh/sQLSD59psvJDVvEQTrX1/KQ=='
|
|
80
|
+
},
|
|
81
|
+
bye: { crazy: 'indeed' }
|
|
82
|
+
}
|
|
83
|
+
JSON valid? true
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
------
|
|
87
|
+
|
|
88
|
+
## Encryption & Decryption using password
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
const secretMsg = "This is some secret text";
|
|
92
|
+
|
|
93
|
+
const envPwd = await signer.encrypt(secretMsg, "somepass1234");
|
|
94
|
+
console.log("Envelope (Password):", envPwd);
|
|
95
|
+
|
|
96
|
+
const decPwd = await signer.decrypt(envPwd, "somepass1234");
|
|
97
|
+
console.log("Decrypted (Password):", decPwd);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Output:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
Envelope (Password): {
|
|
104
|
+
method: 'password',
|
|
105
|
+
salt: 'QxGLufg1lXn6boTVHme8+Q==',
|
|
106
|
+
iv: 'yum57MIAZc0hUBLXPd7hxQ==',
|
|
107
|
+
tag: 'jok95AmgKTj0okoLrFeL0A==',
|
|
108
|
+
ciphertext: 'cDz+F+z8i9emqTO5COKOYfnYWxTB4spC'
|
|
109
|
+
}
|
|
110
|
+
Decrypted (Password): This is some secret text
|
|
111
|
+
```
|
|
112
|
+
|
package/js/index.mjs
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import {promises as fs} from "fs";
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
|
|
4
|
+
export class dSyncSign {
|
|
5
|
+
constructor(keyFile = "./privatekey.json") {
|
|
6
|
+
this.KEY_FILE = keyFile;
|
|
7
|
+
this.sigField = "sig";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
canonicalize(x) {
|
|
11
|
+
if (x === null || typeof x !== "object") return x;
|
|
12
|
+
if (Array.isArray(x)) return x.map(v => this.canonicalize(v));
|
|
13
|
+
const out = {};
|
|
14
|
+
|
|
15
|
+
for (const k of Object.keys(x).sort()) out[k] = this.canonicalize(x[k]);
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
stableStringify(obj) {
|
|
20
|
+
return JSON.stringify(this.canonicalize(obj));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
normalizePublicKey(key) {
|
|
24
|
+
if (!key) return key;
|
|
25
|
+
|
|
26
|
+
// fuck you
|
|
27
|
+
key = String(key)
|
|
28
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
29
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
30
|
+
.replace(/\r\n/g, "\n")
|
|
31
|
+
.replace(/\r/g, "\n")
|
|
32
|
+
.trim();
|
|
33
|
+
|
|
34
|
+
if (key.includes("BEGIN PUBLIC KEY") || key.includes("BEGIN RSA PUBLIC KEY")) {
|
|
35
|
+
key = key
|
|
36
|
+
.replace(/\n+/g, "\n")
|
|
37
|
+
.replace(/-----BEGIN PUBLIC KEY-----\s*/g, "-----BEGIN PUBLIC KEY-----\n")
|
|
38
|
+
.replace(/-----END PUBLIC KEY-----/g, "\n-----END PUBLIC KEY-----")
|
|
39
|
+
.replace(/-----BEGIN RSA PUBLIC KEY-----\s*/g, "-----BEGIN RSA PUBLIC KEY-----\n")
|
|
40
|
+
.replace(/-----END RSA PUBLIC KEY-----/g, "\n-----END RSA PUBLIC KEY-----");
|
|
41
|
+
|
|
42
|
+
const isRsa = key.includes("BEGIN RSA PUBLIC KEY");
|
|
43
|
+
const begin = isRsa ? "-----BEGIN RSA PUBLIC KEY-----" : "-----BEGIN PUBLIC KEY-----";
|
|
44
|
+
const end = isRsa ? "-----END RSA PUBLIC KEY-----" : "-----END PUBLIC KEY-----";
|
|
45
|
+
|
|
46
|
+
let body = key
|
|
47
|
+
.replace(begin, "")
|
|
48
|
+
.replace(end, "")
|
|
49
|
+
.replace(/\s+/g, "");
|
|
50
|
+
|
|
51
|
+
body = body.match(/.{1,64}/g)?.join("\n") || body;
|
|
52
|
+
return `${begin}\n${body}\n${end}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return key;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async ensureKeyPair() {
|
|
59
|
+
try {
|
|
60
|
+
const raw = await fs.readFile(this.KEY_FILE, "utf8");
|
|
61
|
+
const {privateKey} = JSON.parse(raw);
|
|
62
|
+
|
|
63
|
+
crypto.createPrivateKey(privateKey);
|
|
64
|
+
|
|
65
|
+
const pubKey = crypto.createPublicKey(privateKey).export({type: "spki", format: "pem"});
|
|
66
|
+
return {privateKey, publicKey: pubKey.toString()};
|
|
67
|
+
} catch {
|
|
68
|
+
const {privateKey, publicKey} = crypto.generateKeyPairSync("rsa", {
|
|
69
|
+
modulusLength: 2048,
|
|
70
|
+
publicKeyEncoding: {type: "spki", format: "pem"},
|
|
71
|
+
privateKeyEncoding: {type: "pkcs8", format: "pem"}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await fs.writeFile(this.KEY_FILE, JSON.stringify({privateKey}, null, 2), {encoding: "utf8", mode: 0o600});
|
|
75
|
+
return {privateKey, publicKey};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async signString(text) {
|
|
80
|
+
const priv = await this.getPrivateKey();
|
|
81
|
+
const signer = crypto.createSign("SHA256");
|
|
82
|
+
signer.update(text, "utf8");
|
|
83
|
+
signer.end();
|
|
84
|
+
return signer.sign(priv, "base64");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
verifyString(text, signatureBase64, publicKeyPem) {
|
|
89
|
+
publicKeyPem = this.normalizePublicKey(publicKeyPem);
|
|
90
|
+
|
|
91
|
+
const verifier = crypto.createVerify("SHA256");
|
|
92
|
+
verifier.update(text, "utf8");
|
|
93
|
+
verifier.end();
|
|
94
|
+
return verifier.verify(publicKeyPem, signatureBase64, "base64");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
generateGid(publicKey) {
|
|
99
|
+
if (publicKey.length >= 120) {
|
|
100
|
+
return this.encodeToBase64(publicKey.substring(80, 120)) // 40 chars
|
|
101
|
+
} else {
|
|
102
|
+
return this.encodeToBase64(publicKey.substring(0, publicKey.length))
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
encodeToBase64(str) {
|
|
107
|
+
return Buffer.from(str, "utf8").toString("base64")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async getPrivateKey() {
|
|
112
|
+
const {privateKey} = await this.ensureKeyPair();
|
|
113
|
+
return privateKey;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async getPublicKey() {
|
|
117
|
+
const {publicKey} = await this.ensureKeyPair();
|
|
118
|
+
return publicKey;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async encrypt(data, recipient) {
|
|
122
|
+
const plaintext = typeof data === "string" ? data : this.stableStringify(data);
|
|
123
|
+
|
|
124
|
+
if (typeof recipient === "string") {
|
|
125
|
+
recipient = this.normalizePublicKey(recipient);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let aesKey;
|
|
129
|
+
let envelope = {method: ""};
|
|
130
|
+
|
|
131
|
+
if (typeof recipient === "string" && (recipient.includes("BEGIN PUBLIC KEY") || recipient.includes("BEGIN RSA PUBLIC KEY"))) {
|
|
132
|
+
aesKey = crypto.randomBytes(32);
|
|
133
|
+
envelope.method = "rsa";
|
|
134
|
+
envelope.encKey = crypto.publicEncrypt(
|
|
135
|
+
{key: recipient, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING},
|
|
136
|
+
aesKey
|
|
137
|
+
).toString("base64");
|
|
138
|
+
} else {
|
|
139
|
+
const salt = crypto.randomBytes(16);
|
|
140
|
+
aesKey = crypto.pbkdf2Sync(recipient, salt, 100000, 32, "sha256");
|
|
141
|
+
envelope.method = "password";
|
|
142
|
+
envelope.salt = salt.toString("base64");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const iv = crypto.randomBytes(12);
|
|
146
|
+
const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
|
|
147
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
148
|
+
const tag = cipher.getAuthTag();
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
...envelope,
|
|
152
|
+
iv: iv.toString("base64"),
|
|
153
|
+
tag: tag.toString("base64"),
|
|
154
|
+
ciphertext: ciphertext.toString("base64")
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async decrypt(envelope, password = null) {
|
|
159
|
+
let aesKey;
|
|
160
|
+
if (envelope.method === "rsa") {
|
|
161
|
+
const priv = await this.getPrivateKey();
|
|
162
|
+
aesKey = crypto.privateDecrypt(
|
|
163
|
+
{key: priv, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING},
|
|
164
|
+
Buffer.from(envelope.encKey, "base64")
|
|
165
|
+
);
|
|
166
|
+
} else if (envelope.method === "password") {
|
|
167
|
+
if (!password) throw new Error("Password required for password-based decryption");
|
|
168
|
+
aesKey = crypto.pbkdf2Sync(
|
|
169
|
+
password,
|
|
170
|
+
Buffer.from(envelope.salt, "base64"),
|
|
171
|
+
100000,
|
|
172
|
+
32,
|
|
173
|
+
"sha256"
|
|
174
|
+
);
|
|
175
|
+
} else {
|
|
176
|
+
throw new Error("Unsupported envelope method");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const iv = Buffer.from(envelope.iv, "base64");
|
|
180
|
+
const tag = Buffer.from(envelope.tag, "base64");
|
|
181
|
+
const ciphertext = Buffer.from(envelope.ciphertext, "base64");
|
|
182
|
+
|
|
183
|
+
const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
|
|
184
|
+
decipher.setAuthTag(tag);
|
|
185
|
+
|
|
186
|
+
const dec = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
187
|
+
const txt = dec.toString("utf8");
|
|
188
|
+
try {
|
|
189
|
+
return JSON.parse(txt);
|
|
190
|
+
} catch {
|
|
191
|
+
return txt;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async signData(data) {
|
|
196
|
+
const priv = await this.getPrivateKey();
|
|
197
|
+
const signer = crypto.createSign("SHA256");
|
|
198
|
+
const payload = typeof data === "string" ? data : this.stableStringify(data);
|
|
199
|
+
|
|
200
|
+
signer.update(payload, "utf8");
|
|
201
|
+
signer.end();
|
|
202
|
+
|
|
203
|
+
return signer.sign(priv, "base64");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
verifyData(data, signature, publicKey) {
|
|
207
|
+
publicKey = this.normalizePublicKey(publicKey);
|
|
208
|
+
|
|
209
|
+
const verifier = crypto.createVerify("SHA256");
|
|
210
|
+
const payload = typeof data === "string" ? data : this.stableStringify(data);
|
|
211
|
+
|
|
212
|
+
verifier.update(payload, "utf8");
|
|
213
|
+
verifier.end();
|
|
214
|
+
|
|
215
|
+
return verifier.verify(publicKey, signature, "base64");
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
getByPath(root, path) {
|
|
219
|
+
if (!path) return root;
|
|
220
|
+
const re = /([^.\[\]]+)|\[(\d+)\]/g;
|
|
221
|
+
const parts = [];
|
|
222
|
+
let m;
|
|
223
|
+
|
|
224
|
+
while ((m = re.exec(path)) !== null) parts.push(m[1] !== undefined ? m[1] : Number(m[2]));
|
|
225
|
+
let cur = root;
|
|
226
|
+
|
|
227
|
+
for (const p of parts) {
|
|
228
|
+
if (cur == null) return undefined;
|
|
229
|
+
cur = cur[p];
|
|
230
|
+
}
|
|
231
|
+
return cur;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
cloneWithoutSig(obj) {
|
|
235
|
+
if (obj == null || typeof obj !== "object") return obj;
|
|
236
|
+
let copy;
|
|
237
|
+
|
|
238
|
+
if (typeof structuredClone === "function") {
|
|
239
|
+
try {
|
|
240
|
+
copy = structuredClone(obj);
|
|
241
|
+
} catch {
|
|
242
|
+
copy = JSON.parse(JSON.stringify(obj));
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
copy = JSON.parse(JSON.stringify(obj));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (copy && Object.prototype.hasOwnProperty.call(copy, this.sigField)) delete copy[this.sigField];
|
|
249
|
+
return copy;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async signJson(targetOrRoot, path) {
|
|
253
|
+
let target = path ? this.getByPath(targetOrRoot, path) : targetOrRoot;
|
|
254
|
+
|
|
255
|
+
if (target == null) {
|
|
256
|
+
if (path) return false;
|
|
257
|
+
throw new TypeError("target required");
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (Array.isArray(target)) {
|
|
261
|
+
const out = [];
|
|
262
|
+
for (const item of target) {
|
|
263
|
+
if (item == null || typeof item !== "object") {
|
|
264
|
+
out.push(null);
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
if (Object.prototype.hasOwnProperty.call(item, this.sigField)) {
|
|
268
|
+
out.push(item[this.sigField]);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const payload = this.cloneWithoutSig(item);
|
|
272
|
+
const s = await this.signData(payload);
|
|
273
|
+
|
|
274
|
+
item[this.sigField] = s;
|
|
275
|
+
out.push(s);
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (typeof target === "object") {
|
|
281
|
+
if (Object.prototype.hasOwnProperty.call(target, this.sigField)) return target[this.sigField];
|
|
282
|
+
const payload = this.cloneWithoutSig(target);
|
|
283
|
+
const s = await this.signData(payload);
|
|
284
|
+
|
|
285
|
+
target[this.sigField] = s;
|
|
286
|
+
return s;
|
|
287
|
+
}
|
|
288
|
+
throw new TypeError("target must be object or array");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async verifyJson(targetOrRoot, publicKeyOrGetter, path) {
|
|
292
|
+
let target = path ? this.getByPath(targetOrRoot, path) : targetOrRoot;
|
|
293
|
+
|
|
294
|
+
if (target == null) {
|
|
295
|
+
if (path) return false;
|
|
296
|
+
throw new TypeError("target required");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (Array.isArray(target)) {
|
|
300
|
+
const out = [];
|
|
301
|
+
|
|
302
|
+
for (const item of target) {
|
|
303
|
+
if (item == null || typeof item !== "object") {
|
|
304
|
+
out.push(false);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (!Object.prototype.hasOwnProperty.call(item, this.sigField)) {
|
|
309
|
+
out.push(false);
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const signature = item[this.sigField];
|
|
314
|
+
let pub = publicKeyOrGetter;
|
|
315
|
+
|
|
316
|
+
if (typeof publicKeyOrGetter === "function") pub = await publicKeyOrGetter(item, targetOrRoot);
|
|
317
|
+
if (!pub) {
|
|
318
|
+
out.push(false);
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const payload = this.cloneWithoutSig(item);
|
|
323
|
+
out.push(Boolean(this.verifyData(payload, signature, pub)));
|
|
324
|
+
}
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
if (typeof target === "object") {
|
|
328
|
+
if (!Object.prototype.hasOwnProperty.call(target, this.sigField)) return false;
|
|
329
|
+
|
|
330
|
+
const signature = target[this.sigField];
|
|
331
|
+
let pub = publicKeyOrGetter;
|
|
332
|
+
|
|
333
|
+
if (typeof publicKeyOrGetter === "function") pub = await publicKeyOrGetter(target, targetOrRoot);
|
|
334
|
+
if (!pub) return false;
|
|
335
|
+
|
|
336
|
+
const payload = this.cloneWithoutSig(target);
|
|
337
|
+
return Boolean(this.verifyData(payload, signature, pub));
|
|
338
|
+
}
|
|
339
|
+
throw new TypeError("target must be object or array");
|
|
340
|
+
}
|
|
341
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hackthedev/dsync-sign",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.11",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -9,5 +9,9 @@
|
|
|
9
9
|
},
|
|
10
10
|
"keywords": [],
|
|
11
11
|
"author": "",
|
|
12
|
-
"license": "ISC"
|
|
12
|
+
"license": "ISC",
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"crypto": "^1.0.1",
|
|
15
|
+
"fs": "^0.0.1-security"
|
|
16
|
+
}
|
|
13
17
|
}
|