@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 ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -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
- .replace(/\r|\n|\s+/g, '')
26
- .replace('-----BEGINPUBLICKEY-----', '-----BEGIN PUBLIC KEY-----')
27
- .replace('-----ENDPUBLICKEY-----', '-----END PUBLIC KEY-----')
28
- .replace(/-----BEGIN PUBLIC KEY-----/, '-----BEGIN PUBLIC KEY-----\n')
29
- .replace(/-----END PUBLIC KEY-----/, '\n-----END PUBLIC KEY-----')
30
- .replace(/(.{64})/g, '$1\n')
31
- .trim();
32
- }
33
-
34
- async ensureKeyPair() {
35
- try {
36
- const raw = await fs.readFile(this.KEY_FILE, "utf8");
37
- const {privateKey} = JSON.parse(raw);
38
-
39
- crypto.createPrivateKey(privateKey);
40
-
41
- const pubKey = crypto.createPublicKey(privateKey).export({type: "spki", format: "pem"});
42
- return {privateKey, publicKey: pubKey.toString()};
43
- } catch {
44
- const {privateKey, publicKey} = crypto.generateKeyPairSync("rsa", {
45
- modulusLength: 2048,
46
- publicKeyEncoding: {type: "spki", format: "pem"},
47
- privateKeyEncoding: {type: "pkcs8", format: "pem"}
48
- });
49
-
50
- await fs.writeFile(this.KEY_FILE, JSON.stringify({privateKey}, null, 2), {encoding: "utf8", mode: 0o600});
51
- return {privateKey, publicKey};
52
- }
53
- }
54
-
55
- async signString(text) {
56
- const priv = await this.getPrivateKey();
57
- const signer = crypto.createSign("SHA256");
58
- signer.update(text, "utf8");
59
- signer.end();
60
- return signer.sign(priv, "base64");
61
- }
62
-
63
-
64
- verifyString(text, signatureBase64, publicKeyPem) {
65
- const verifier = crypto.createVerify("SHA256");
66
- verifier.update(text, "utf8");
67
- verifier.end();
68
- return verifier.verify(publicKeyPem, signatureBase64, "base64");
69
- }
70
-
71
-
72
- generateGid(publicKey) {
73
- if (publicKey.length >= 120) {
74
- return this.encodeToBase64(publicKey.substring(80, 120)) // 40 chars
75
- } else {
76
- return this.encodeToBase64(publicKey.substring(0, publicKey.length))
77
- }
78
- }
79
-
80
- encodeToBase64(str) {
81
- return Buffer.from(str, "utf8").toString("base64")
82
- }
83
-
84
-
85
- async getPrivateKey() {
86
- const {privateKey} = await this.ensureKeyPair();
87
- return privateKey;
88
- }
89
-
90
- async getPublicKey() {
91
- const {publicKey} = await this.ensureKeyPair();
92
- return publicKey;
93
- }
94
-
95
- async encrypt(data, recipient) {
96
- const plaintext = typeof data === "string" ? data : this.stableStringify(data);
97
- let aesKey;
98
- let envelope = {method: ""};
99
-
100
- if (recipient.includes("BEGIN PUBLIC KEY") || recipient.includes("BEGIN RSA PUBLIC KEY")) {
101
- aesKey = crypto.randomBytes(32);
102
- envelope.method = "rsa";
103
- envelope.encKey = crypto.publicEncrypt(
104
- {key: recipient, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING},
105
- aesKey
106
- ).toString("base64");
107
- } else {
108
- const salt = crypto.randomBytes(16);
109
- aesKey = crypto.pbkdf2Sync(recipient, salt, 100000, 32, "sha256");
110
- envelope.method = "password";
111
- envelope.salt = salt.toString("base64");
112
- }
113
-
114
- // Standard-konformer 12-Byte-IV (statt 16)
115
- const iv = crypto.randomBytes(12);
116
-
117
- const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
118
- const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
119
- const tag = cipher.getAuthTag();
120
-
121
- return {
122
- ...envelope,
123
- iv: iv.toString("base64"),
124
- tag: tag.toString("base64"),
125
- ciphertext: ciphertext.toString("base64")
126
- };
127
- }
128
-
129
- async decrypt(envelope, password = null) {
130
- let aesKey;
131
- if (envelope.method === "rsa") {
132
- const priv = await this.getPrivateKey();
133
- aesKey = crypto.privateDecrypt(
134
- {key: priv, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING},
135
- Buffer.from(envelope.encKey, "base64")
136
- );
137
- } else if (envelope.method === "password") {
138
- if (!password) throw new Error("Password required for password-based decryption");
139
- aesKey = crypto.pbkdf2Sync(
140
- password,
141
- Buffer.from(envelope.salt, "base64"),
142
- 100000,
143
- 32,
144
- "sha256"
145
- );
146
- } else {
147
- throw new Error("Unsupported envelope method");
148
- }
149
-
150
- const iv = Buffer.from(envelope.iv, "base64");
151
- const tag = Buffer.from(envelope.tag, "base64");
152
- const ciphertext = Buffer.from(envelope.ciphertext, "base64");
153
-
154
- const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
155
- decipher.setAuthTag(tag);
156
-
157
- const dec = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
158
- const txt = dec.toString("utf8");
159
- try {
160
- return JSON.parse(txt);
161
- } catch {
162
- return txt;
163
- }
164
- }
165
-
166
- async signData(data) {
167
- const priv = await this.getPrivateKey();
168
- const signer = crypto.createSign("SHA256");
169
- const payload = typeof data === "string" ? data : this.stableStringify(data);
170
-
171
- signer.update(payload, "utf8");
172
- signer.end();
173
-
174
- return signer.sign(priv, "base64");
175
- }
176
-
177
- verifyData(data, signature, publicKey) {
178
- const verifier = crypto.createVerify("SHA256");
179
- const payload = typeof data === "string" ? data : this.stableStringify(data);
180
-
181
- verifier.update(payload, "utf8");
182
- verifier.end();
183
-
184
- return verifier.verify(publicKey, signature, "base64");
185
- }
186
-
187
- getByPath(root, path) {
188
- if (!path) return root;
189
- const re = /([^.\[\]]+)|\[(\d+)\]/g;
190
- const parts = [];
191
- let m;
192
-
193
- while ((m = re.exec(path)) !== null) parts.push(m[1] !== undefined ? m[1] : Number(m[2]));
194
- let cur = root;
195
-
196
- for (const p of parts) {
197
- if (cur == null) return undefined;
198
- cur = cur[p];
199
- }
200
- return cur;
201
- }
202
-
203
- cloneWithoutSig(obj) {
204
- if (obj == null || typeof obj !== "object") return obj;
205
- let copy;
206
-
207
- if (typeof structuredClone === "function") {
208
- try {
209
- copy = structuredClone(obj);
210
- } catch {
211
- copy = JSON.parse(JSON.stringify(obj));
212
- }
213
- } else {
214
- copy = JSON.parse(JSON.stringify(obj));
215
- }
216
-
217
- if (copy && Object.prototype.hasOwnProperty.call(copy, this.sigField)) delete copy[this.sigField];
218
- return copy;
219
- }
220
-
221
- async signJson(targetOrRoot, path) {
222
- let target = path ? this.getByPath(targetOrRoot, path) : targetOrRoot;
223
-
224
- if (target == null) {
225
- if (path) return false;
226
- throw new TypeError("target required");
227
- }
228
-
229
- if (Array.isArray(target)) {
230
- const out = [];
231
- for (const item of target) {
232
- if (item == null || typeof item !== "object") {
233
- out.push(null);
234
- continue;
235
- }
236
- if (Object.prototype.hasOwnProperty.call(item, this.sigField)) {
237
- out.push(item[this.sigField]);
238
- continue;
239
- }
240
- const payload = this.cloneWithoutSig(item);
241
- const s = await this.signData(payload);
242
-
243
- item[this.sigField] = s;
244
- out.push(s);
245
- }
246
- return out;
247
- }
248
-
249
- if (typeof target === "object") {
250
- if (Object.prototype.hasOwnProperty.call(target, this.sigField)) return target[this.sigField];
251
- const payload = this.cloneWithoutSig(target);
252
- const s = await this.signData(payload);
253
-
254
- target[this.sigField] = s;
255
- return s;
256
- }
257
- throw new TypeError("target must be object or array");
258
- }
259
-
260
- async verifyJson(targetOrRoot, publicKeyOrGetter, path) {
261
- let target = path ? this.getByPath(targetOrRoot, path) : targetOrRoot;
262
-
263
- if (target == null) {
264
- if (path) return false;
265
- throw new TypeError("target required");
266
- }
267
-
268
- if (Array.isArray(target)) {
269
- const out = [];
270
-
271
- for (const item of target) {
272
- if (item == null || typeof item !== "object") {
273
- out.push(false);
274
- continue;
275
- }
276
-
277
- if (!Object.prototype.hasOwnProperty.call(item, this.sigField)) {
278
- out.push(false);
279
- continue;
280
- }
281
-
282
- const signature = item[this.sigField];
283
- let pub = publicKeyOrGetter;
284
-
285
- if (typeof publicKeyOrGetter === "function") pub = await publicKeyOrGetter(item, targetOrRoot);
286
- if (!pub) {
287
- out.push(false);
288
- continue;
289
- }
290
-
291
- const payload = this.cloneWithoutSig(item);
292
- out.push(Boolean(this.verifyData(payload, signature, pub)));
293
- }
294
- return out;
295
- }
296
- if (typeof target === "object") {
297
- if (!Object.prototype.hasOwnProperty.call(target, this.sigField)) return false;
298
-
299
- const signature = target[this.sigField];
300
- let pub = publicKeyOrGetter;
301
-
302
- if (typeof publicKeyOrGetter === "function") pub = await publicKeyOrGetter(target, targetOrRoot);
303
- if (!pub) return false;
304
-
305
- const payload = this.cloneWithoutSig(target);
306
- return Boolean(this.verifyData(payload, signature, pub));
307
- }
308
- throw new TypeError("target must be object or array");
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(/&lt;br\s*\/?&gt;/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.10",
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
  }