@hackthedev/dsync-sign 1.0.1

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
package/README.md ADDED
@@ -0,0 +1,8 @@
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
package/index.mjs ADDED
@@ -0,0 +1,240 @@
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
+ for (const k of Object.keys(x).sort()) out[k] = this.canonicalize(x[k]);
15
+ return out;
16
+ }
17
+
18
+ stableStringify(obj) {
19
+ return JSON.stringify(this.canonicalize(obj));
20
+ }
21
+
22
+ async ensureKeyPair() {
23
+ try {
24
+ const raw = await fs.readFile(this.KEY_FILE, "utf8");
25
+ const { privateKey } = JSON.parse(raw);
26
+ crypto.createPrivateKey(privateKey);
27
+ const pubKey = crypto.createPublicKey(privateKey).export({ type: "spki", format: "pem" });
28
+ return { privateKey, publicKey: pubKey.toString() };
29
+ } catch {
30
+ const { privateKey, publicKey } = crypto.generateKeyPairSync("rsa", {
31
+ modulusLength: 2048,
32
+ publicKeyEncoding: { type: "spki", format: "pem" },
33
+ privateKeyEncoding: { type: "pkcs8", format: "pem" }
34
+ });
35
+ await fs.writeFile(this.KEY_FILE, JSON.stringify({ privateKey }, null, 2), { encoding: "utf8", mode: 0o600 });
36
+ return { privateKey, publicKey };
37
+ }
38
+ }
39
+
40
+ async getPrivateKey() {
41
+ const { privateKey } = await this.ensureKeyPair();
42
+ return privateKey;
43
+ }
44
+
45
+ async getPublicKey() {
46
+ const { publicKey } = await this.ensureKeyPair();
47
+ return publicKey;
48
+ }
49
+
50
+ async encrypt(data, recipient) {
51
+ const plaintext = typeof data === "string" ? data : this.stableStringify(data);
52
+ let aesKey;
53
+ let envelope = { method: "" };
54
+
55
+ if (recipient.includes("BEGIN PUBLIC KEY") || recipient.includes("BEGIN RSA PUBLIC KEY")) {
56
+ aesKey = crypto.randomBytes(32);
57
+ envelope.method = "rsa";
58
+ envelope.encKey = crypto.publicEncrypt(
59
+ { key: recipient, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING },
60
+ aesKey
61
+ ).toString("base64");
62
+ } else {
63
+ const salt = crypto.randomBytes(16);
64
+ aesKey = crypto.pbkdf2Sync(recipient, salt, 100000, 32, "sha256");
65
+ envelope.method = "password";
66
+ envelope.salt = salt.toString("base64");
67
+ }
68
+
69
+ const iv = crypto.randomBytes(16);
70
+ const cipher = crypto.createCipheriv("aes-256-gcm", aesKey, iv);
71
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
72
+ const tag = cipher.getAuthTag();
73
+
74
+ return {
75
+ ...envelope,
76
+ iv: iv.toString("base64"),
77
+ tag: tag.toString("base64"),
78
+ ciphertext: ciphertext.toString("base64")
79
+ };
80
+ }
81
+
82
+ async decrypt(envelope, password = null) {
83
+ let aesKey;
84
+ if (envelope.method === "rsa") {
85
+ const priv = await this.getPrivateKey();
86
+ aesKey = crypto.privateDecrypt(
87
+ { key: priv, padding: crypto.constants.RSA_PKCS1_OAEP_PADDING },
88
+ Buffer.from(envelope.encKey, "base64")
89
+ );
90
+ } else if (envelope.method === "password") {
91
+ if (!password) throw new Error("Password required for password-based decryption");
92
+ aesKey = crypto.pbkdf2Sync(
93
+ password,
94
+ Buffer.from(envelope.salt, "base64"),
95
+ 100000,
96
+ 32,
97
+ "sha256"
98
+ );
99
+ } else {
100
+ throw new Error("Unsupported envelope method");
101
+ }
102
+
103
+ const iv = Buffer.from(envelope.iv, "base64");
104
+ const tag = Buffer.from(envelope.tag, "base64");
105
+ const ciphertext = Buffer.from(envelope.ciphertext, "base64");
106
+
107
+ const decipher = crypto.createDecipheriv("aes-256-gcm", aesKey, iv);
108
+ decipher.setAuthTag(tag);
109
+
110
+ const dec = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
111
+ const txt = dec.toString("utf8");
112
+ try {
113
+ return JSON.parse(txt);
114
+ } catch {
115
+ return txt;
116
+ }
117
+ }
118
+
119
+ async signData(data) {
120
+ const priv = await this.getPrivateKey();
121
+ const signer = crypto.createSign("SHA256");
122
+ const payload = typeof data === "string" ? data : this.stableStringify(data);
123
+ signer.update(payload, "utf8");
124
+ signer.end();
125
+ return signer.sign(priv, "base64");
126
+ }
127
+
128
+ verifyData(data, signature, publicKey) {
129
+ const verifier = crypto.createVerify("SHA256");
130
+ const payload = typeof data === "string" ? data : this.stableStringify(data);
131
+ verifier.update(payload, "utf8");
132
+ verifier.end();
133
+ return verifier.verify(publicKey, signature, "base64");
134
+ }
135
+
136
+ getByPath(root, path) {
137
+ if (!path) return root;
138
+ const re = /([^.\[\]]+)|\[(\d+)\]/g;
139
+ const parts = [];
140
+ let m;
141
+ while ((m = re.exec(path)) !== null) parts.push(m[1] !== undefined ? m[1] : Number(m[2]));
142
+ let cur = root;
143
+ for (const p of parts) {
144
+ if (cur == null) return undefined;
145
+ cur = cur[p];
146
+ }
147
+ return cur;
148
+ }
149
+
150
+ cloneWithoutSig(obj) {
151
+ if (obj == null || typeof obj !== "object") return obj;
152
+ let copy;
153
+ if (typeof structuredClone === "function") {
154
+ try {
155
+ copy = structuredClone(obj);
156
+ } catch {
157
+ copy = JSON.parse(JSON.stringify(obj));
158
+ }
159
+ } else {
160
+ copy = JSON.parse(JSON.stringify(obj));
161
+ }
162
+ if (copy && Object.prototype.hasOwnProperty.call(copy, this.sigField)) delete copy[this.sigField];
163
+ return copy;
164
+ }
165
+
166
+ async signJson(targetOrRoot, path) {
167
+ let target = path ? this.getByPath(targetOrRoot, path) : targetOrRoot;
168
+ if (target == null) {
169
+ if (path) return false;
170
+ throw new TypeError("target required");
171
+ }
172
+ if (Array.isArray(target)) {
173
+ const out = [];
174
+ for (const item of target) {
175
+ if (item == null || typeof item !== "object") {
176
+ out.push(null);
177
+ continue;
178
+ }
179
+ if (Object.prototype.hasOwnProperty.call(item, this.sigField)) {
180
+ out.push(item[this.sigField]);
181
+ continue;
182
+ }
183
+ const payload = this.cloneWithoutSig(item);
184
+ const s = await this.signData(payload);
185
+ item[this.sigField] = s;
186
+ out.push(s);
187
+ }
188
+ return out;
189
+ }
190
+ if (typeof target === "object") {
191
+ if (Object.prototype.hasOwnProperty.call(target, this.sigField)) return target[this.sigField];
192
+ const payload = this.cloneWithoutSig(target);
193
+ const s = await this.signData(payload);
194
+ target[this.sigField] = s;
195
+ return s;
196
+ }
197
+ throw new TypeError("target must be object or array");
198
+ }
199
+
200
+ async verifyJson(targetOrRoot, publicKeyOrGetter, path) {
201
+ let target = path ? this.getByPath(targetOrRoot, path) : targetOrRoot;
202
+ if (target == null) {
203
+ if (path) return false;
204
+ throw new TypeError("target required");
205
+ }
206
+ if (Array.isArray(target)) {
207
+ const out = [];
208
+ for (const item of target) {
209
+ if (item == null || typeof item !== "object") {
210
+ out.push(false);
211
+ continue;
212
+ }
213
+ if (!Object.prototype.hasOwnProperty.call(item, this.sigField)) {
214
+ out.push(false);
215
+ continue;
216
+ }
217
+ const signature = item[this.sigField];
218
+ let pub = publicKeyOrGetter;
219
+ if (typeof publicKeyOrGetter === "function") pub = await publicKeyOrGetter(item, targetOrRoot);
220
+ if (!pub) {
221
+ out.push(false);
222
+ continue;
223
+ }
224
+ const payload = this.cloneWithoutSig(item);
225
+ out.push(Boolean(this.verifyData(payload, signature, pub)));
226
+ }
227
+ return out;
228
+ }
229
+ if (typeof target === "object") {
230
+ if (!Object.prototype.hasOwnProperty.call(target, this.sigField)) return false;
231
+ const signature = target[this.sigField];
232
+ let pub = publicKeyOrGetter;
233
+ if (typeof publicKeyOrGetter === "function") pub = await publicKeyOrGetter(target, targetOrRoot);
234
+ if (!pub) return false;
235
+ const payload = this.cloneWithoutSig(target);
236
+ return Boolean(this.verifyData(payload, signature, pub));
237
+ }
238
+ throw new TypeError("target must be object or array");
239
+ }
240
+ }
package/package.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "@hackthedev/dsync-sign",
3
+ "version": "1.0.1",
4
+ "description": "",
5
+ "main": "index.mjs",
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "echo \"Error: no test specified\" && exit 1"
9
+ },
10
+ "keywords": [],
11
+ "author": "",
12
+ "license": "ISC"
13
+ }