@mblaney/holster 0.1.0

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.
Files changed (46) hide show
  1. package/.github/workflows/node.js.yml +31 -0
  2. package/LICENSE.md +55 -0
  3. package/README.md +24 -0
  4. package/build/holster.js +4 -0
  5. package/build/holster.js.map +7 -0
  6. package/examples/index.html +33 -0
  7. package/examples/index.js +49 -0
  8. package/examples/user.html +31 -0
  9. package/examples/user.js +88 -0
  10. package/examples/wire.html +27 -0
  11. package/examples/wire.js +55 -0
  12. package/package.json +44 -0
  13. package/src/array.js +34 -0
  14. package/src/buffer.js +83 -0
  15. package/src/dup.js +23 -0
  16. package/src/get.js +19 -0
  17. package/src/ham.js +151 -0
  18. package/src/holster.js +502 -0
  19. package/src/index.js +21 -0
  20. package/src/radisk.js +438 -0
  21. package/src/radix.js +106 -0
  22. package/src/sea-utils.js +65 -0
  23. package/src/sea.js +224 -0
  24. package/src/store.js +225 -0
  25. package/src/user.js +330 -0
  26. package/src/utils.js +124 -0
  27. package/src/wire.js +290 -0
  28. package/test/holster.get.test.js +60 -0
  29. package/test/holster.lex.test.js +196 -0
  30. package/test/holster.off.test.js +53 -0
  31. package/test/holster.on.test.js +168 -0
  32. package/test/holster.put.test.js +278 -0
  33. package/test/holster.secure.test.js +210 -0
  34. package/test/holster.user.get.test.js +86 -0
  35. package/test/holster.user.lex.test.js +148 -0
  36. package/test/holster.user.off.test.js +72 -0
  37. package/test/holster.user.on.test.js +185 -0
  38. package/test/holster.user.put.test.js +381 -0
  39. package/test/kitty/! +9 -0
  40. package/test/names.js +225 -0
  41. package/test/radisk.test.js +191 -0
  42. package/test/radix.test.js +117 -0
  43. package/test/sea.test.js +284 -0
  44. package/test/store.test.js +316 -0
  45. package/test/user.test.js +173 -0
  46. package/test/wire.test.js +159 -0
package/src/sea.js ADDED
@@ -0,0 +1,224 @@
1
+ import {userPublicKey} from "./utils.js"
2
+ import * as utils from "./sea-utils.js"
3
+ import SafeBuffer from "./buffer.js"
4
+
5
+ // Security, Encryption, and Authorization: SEA.js from GunDB.
6
+ const SEA = {
7
+ pair: async cb => {
8
+ // ECDSA keys for signing/verifying.
9
+ const ecdsa = await utils.subtle
10
+ .generateKey({name: "ECDSA", namedCurve: "P-256"}, true, [
11
+ "sign",
12
+ "verify",
13
+ ])
14
+ .then(async keys => {
15
+ const pub = await utils.subtle.exportKey("jwk", keys.publicKey)
16
+ return {
17
+ priv: (await utils.subtle.exportKey("jwk", keys.privateKey)).d,
18
+ pub: pub.x + "." + pub.y,
19
+ }
20
+ })
21
+
22
+ // ECDH keys for encryption/decryption.
23
+ const ecdh = await utils.subtle
24
+ .generateKey({name: "ECDH", namedCurve: "P-256"}, true, ["deriveKey"])
25
+ .then(async keys => {
26
+ const pub = await utils.subtle.exportKey("jwk", keys.publicKey)
27
+ return {
28
+ epriv: (await utils.subtle.exportKey("jwk", keys.privateKey)).d,
29
+ epub: pub.x + "." + pub.y,
30
+ }
31
+ })
32
+
33
+ const pair = {
34
+ pub: ecdsa.pub,
35
+ priv: ecdsa.priv,
36
+ epub: ecdh.epub,
37
+ epriv: ecdh.epriv,
38
+ }
39
+ if (cb) cb(pair)
40
+ return pair
41
+ },
42
+ encrypt: async (data, pair, cb) => {
43
+ if (!pair || !pair.epriv) {
44
+ if (cb) cb(null)
45
+ return null
46
+ }
47
+
48
+ const rand = {s: utils.random(9), iv: utils.random(15)}
49
+ const ct = await utils.aeskey(pair.epriv, rand.s).then(aes => {
50
+ return utils.subtle.encrypt(
51
+ {
52
+ name: "AES-GCM",
53
+ iv: new Uint8Array(rand.iv),
54
+ },
55
+ aes,
56
+ new TextEncoder().encode(utils.stringify(data)),
57
+ )
58
+ })
59
+ const enc = {
60
+ ct: SafeBuffer.from(ct, "binary").toString("base64"),
61
+ iv: rand.iv.toString("base64"),
62
+ s: rand.s.toString("base64"),
63
+ }
64
+ if (cb) cb(enc)
65
+ return enc
66
+ },
67
+ decrypt: async (enc, pair, cb) => {
68
+ if (!enc || !enc.ct || !enc.iv || !enc.s || !pair || !pair.epriv) {
69
+ if (cb) cb(null)
70
+ return null
71
+ }
72
+
73
+ const data = {
74
+ ct: SafeBuffer.from(enc.ct, "base64"),
75
+ iv: SafeBuffer.from(enc.iv, "base64"),
76
+ s: SafeBuffer.from(enc.s, "base64"),
77
+ }
78
+ try {
79
+ const ct = await utils.aeskey(pair.epriv, data.s).then(aes => {
80
+ return utils.subtle.decrypt(
81
+ {
82
+ name: "AES-GCM",
83
+ iv: new Uint8Array(data.iv),
84
+ tagLength: 128,
85
+ },
86
+ aes,
87
+ new Uint8Array(data.ct),
88
+ )
89
+ })
90
+ const dec = utils.parse(new TextDecoder("utf8").decode(ct))
91
+ if (cb) cb(dec)
92
+ return dec
93
+ } catch (err) {
94
+ // An error will be thrown if the wrong key is used.
95
+ if (cb) cb(null)
96
+ return null
97
+ }
98
+ },
99
+ verify: async (data, pair, cb) => {
100
+ if (!pair || !pair.pub) {
101
+ if (cb) cb(null)
102
+ return null
103
+ }
104
+
105
+ const signed = utils.parse(data)
106
+ const key = await utils.subtle.importKey(
107
+ "jwk",
108
+ utils.jwk(pair.pub),
109
+ {name: "ECDSA", namedCurve: "P-256"},
110
+ false,
111
+ ["verify"],
112
+ )
113
+
114
+ let msg = {}
115
+ if (typeof signed.m === "string") {
116
+ msg = signed.m
117
+ } else {
118
+ // Allow data to be passed in with graph meta data,
119
+ // (which should not be part of signature, so not verified).
120
+ for (const k of Object.keys(signed.m)) {
121
+ if (k !== "_" && k != userPublicKey) msg[k] = signed.m[k]
122
+ }
123
+ }
124
+ const hash = await utils.sha256(msg)
125
+ const sig = new Uint8Array(SafeBuffer.from(signed.s, "base64"))
126
+ const alg = {name: "ECDSA", hash: {name: "SHA-256"}}
127
+ if (await utils.subtle.verify(alg, key, sig, new Uint8Array(hash))) {
128
+ const verified = utils.parse(signed.m)
129
+ if (cb) cb(verified)
130
+ return verified
131
+ }
132
+
133
+ if (cb) cb(null)
134
+ return null
135
+ },
136
+ sign: async (data, pair, cb) => {
137
+ if (!pair || !pair.pub || !pair.priv) {
138
+ if (cb) cb(null)
139
+ return null
140
+ }
141
+
142
+ const msg = utils.parse(data)
143
+ const hash = await utils.sha256(msg)
144
+ const jwk = utils.jwk(pair.pub, pair.priv)
145
+ const alg = {name: "ECDSA", namedCurve: "P-256"}
146
+ const sig = await utils.subtle
147
+ .importKey("jwk", jwk, alg, false, ["sign"])
148
+ .then(key =>
149
+ utils.subtle.sign(
150
+ {name: "ECDSA", hash: {name: "SHA-256"}},
151
+ key,
152
+ new Uint8Array(hash),
153
+ ),
154
+ )
155
+ const signed = {
156
+ m: msg,
157
+ s: SafeBuffer.from(sig, "binary").toString("base64"),
158
+ }
159
+
160
+ if (cb) cb(signed)
161
+ return signed
162
+ },
163
+ work: async (data, salt, cb) => {
164
+ if (typeof salt === "function") {
165
+ cb = salt
166
+ salt = undefined
167
+ }
168
+ if (typeof salt === "undefined") salt = utils.random(9)
169
+
170
+ const key = await utils.subtle.importKey(
171
+ "raw",
172
+ new TextEncoder().encode(utils.stringify(data)),
173
+ {name: "PBKDF2"},
174
+ false,
175
+ ["deriveBits"],
176
+ )
177
+ const alg = {
178
+ name: "PBKDF2",
179
+ iterations: 100000,
180
+ salt: new TextEncoder().encode(salt),
181
+ hash: {name: "SHA-256"},
182
+ }
183
+ const work = await utils.subtle.deriveBits(alg, key, 512)
184
+ // Use "pair" format so that work can be used as epriv by decrypt.
185
+ const pair = {epriv: SafeBuffer.from(work, "binary").toString("base64")}
186
+ if (cb) cb(pair)
187
+ return pair
188
+ },
189
+ secret: async (to, from, cb) => {
190
+ if (!to || !to.epub || !from || !from.epub || !from.epriv) {
191
+ if (cb) cb(null)
192
+ return null
193
+ }
194
+
195
+ const alg = {name: "ECDH", namedCurve: "P-256"}
196
+ const pub = utils.jwk(to.epub)
197
+ const pubKey = await utils.subtle.importKey("jwk", pub, alg, true, [])
198
+ const priv = utils.jwk(from.epub, from.epriv, false)
199
+ // utils.jwk provides default key_ops but it shouldn't be used here.
200
+ delete priv.key_ops
201
+ const derived = await utils.subtle
202
+ .importKey("jwk", priv, alg, false, ["deriveBits"])
203
+ .then(async key => {
204
+ const derivedBits = await utils.subtle.deriveBits(
205
+ {public: pubKey, name: "ECDH", namedCurve: "P-256"},
206
+ key,
207
+ 256,
208
+ )
209
+ const derivedKey = await utils.subtle.importKey(
210
+ "raw",
211
+ new Uint8Array(derivedBits),
212
+ {name: "AES-GCM", length: 256},
213
+ true,
214
+ ["encrypt", "decrypt"],
215
+ )
216
+ return utils.subtle.exportKey("jwk", derivedKey).then(({k}) => k)
217
+ })
218
+ // Use "pair" format so that secret can be used as epriv by encrypt.
219
+ if (cb) cb({epriv: derived})
220
+ return {epriv: derived}
221
+ },
222
+ }
223
+
224
+ export default SEA
package/src/store.js ADDED
@@ -0,0 +1,225 @@
1
+ import Radisk from "./radisk.js"
2
+ import Radix from "./radix.js"
3
+ import * as utils from "./utils.js"
4
+
5
+ const isNode = typeof document === "undefined"
6
+ const fs = isNode ? await import("node:fs") : undefined
7
+
8
+ // ASCII character for enquiry.
9
+ const enq = String.fromCharCode(5)
10
+ // ASCII character for unit separator.
11
+ const unit = String.fromCharCode(31)
12
+ // On-disk root node format.
13
+ const root = unit + "+0" + unit + "#" + unit + '"root' + unit
14
+
15
+ const fileSystem = opt => {
16
+ const dir = opt.file
17
+
18
+ if (isNode) {
19
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir)
20
+ if (!fs.existsSync(dir + "/!")) fs.writeFileSync(dir + "/!", root)
21
+
22
+ return {
23
+ get: (file, cb) => {
24
+ fs.readFile(dir + "/" + file, (err, data) => {
25
+ if (err) {
26
+ if (err.code === "ENOENT") {
27
+ cb()
28
+ return
29
+ }
30
+
31
+ console.log("fs.readFile error:", err)
32
+ }
33
+ if (data) data = data.toString()
34
+ cb(err, data)
35
+ })
36
+ },
37
+ put: (file, data, cb) => {
38
+ var random = Math.random().toString(36).slice(-9)
39
+ // Don't put tmp files under dir so that they're not listed.
40
+ var tmp = file + "." + random + ".tmp"
41
+ fs.writeFile(tmp, data, err => {
42
+ if (err) {
43
+ cb(err)
44
+ return
45
+ }
46
+
47
+ fs.rename(tmp, dir + "/" + file, cb)
48
+ })
49
+ },
50
+ list: cb => {
51
+ fs.readdir(dir, (err, files) => {
52
+ files.forEach(cb)
53
+ cb()
54
+ })
55
+ },
56
+ }
57
+ }
58
+
59
+ if (opt.indexedDB) {
60
+ let db
61
+ const o = indexedDB.open(dir, 1)
62
+ o.onupgradeneeded = event => {
63
+ event.target.result.createObjectStore(dir)
64
+ }
65
+ o.onerror = event => {
66
+ console.log(event)
67
+ }
68
+ o.onsuccess = () => {
69
+ db = o.result
70
+ // Create the root node if it doesn't exist.
71
+ if (db) {
72
+ const tx = db.transaction([dir], "readonly")
73
+ const req = tx.objectStore(dir).getKey("!")
74
+ req.onerror = () => {
75
+ console.log(`error getting key ${dir}/!`)
76
+ }
77
+ req.onsuccess = () => {
78
+ if (!req.result) {
79
+ const tx = db.transaction([dir], "readwrite")
80
+ const req = tx.objectStore(dir).put(root, "!")
81
+ req.onerror = () => {
82
+ console.log(`error putting root on ${dir}/!`)
83
+ }
84
+ }
85
+ }
86
+ } else {
87
+ console.log("error indexedDB not available")
88
+ }
89
+ }
90
+
91
+ return {
92
+ get: (file, cb) => {
93
+ if (db) {
94
+ const tx = db.transaction([dir], "readonly")
95
+ const req = tx.objectStore(dir).get(file)
96
+ req.onerror = () => {
97
+ console.log(`error getting ${dir}/${file}`)
98
+ }
99
+ req.onsuccess = () => {
100
+ cb(null, req.result)
101
+ }
102
+ } else {
103
+ cb("error indexedDB not available")
104
+ }
105
+ },
106
+ put: (file, data, cb) => {
107
+ if (db) {
108
+ const tx = db.transaction([dir], "readwrite")
109
+ const req = tx.objectStore(dir).put(data, file)
110
+ req.onerror = () => {
111
+ console.log(`error putting data on ${dir}/${file}`)
112
+ }
113
+ req.onsuccess = () => {
114
+ cb(null)
115
+ }
116
+ } else {
117
+ cb("error indexedDB not available")
118
+ }
119
+ },
120
+ list: cb => {
121
+ if (db) {
122
+ const tx = db.transaction([dir], "readonly")
123
+ const req = tx.objectStore(dir).getAllKeys()
124
+ req.onerror = () => console.log("error getting keys for", dir)
125
+ req.onsuccess = () => {
126
+ req.result.forEach(cb)
127
+ cb()
128
+ }
129
+ } else {
130
+ console.log("error indexedDB not available")
131
+ cb()
132
+ }
133
+ },
134
+ }
135
+ }
136
+
137
+ // No browser storage.
138
+ return {
139
+ get: (file, cb) => {
140
+ cb(null, root)
141
+ },
142
+ put: (file, data, cb) => {
143
+ cb(null)
144
+ },
145
+ list: cb => {
146
+ cb("!")
147
+ cb()
148
+ },
149
+ }
150
+ }
151
+
152
+ // Store provides get and put methods that can access radisk.
153
+ const Store = opt => {
154
+ if (!utils.obj.is(opt)) opt = {}
155
+ opt.file = String(opt.file || "radata")
156
+ if (!opt.store) opt.store = fileSystem(opt)
157
+ const radisk = Radisk(opt)
158
+
159
+ return {
160
+ get: (lex, cb) => {
161
+ if (!lex) {
162
+ cb("lex required")
163
+ return
164
+ }
165
+
166
+ var soul = lex["#"]
167
+ var key = typeof lex["."] === "string" ? lex["."] : ""
168
+ var node
169
+ const each = (value, key) => {
170
+ if (!utils.match(lex["."], key)) return
171
+
172
+ if (!node) node = {_: {"#": soul, ">": {}}}
173
+ node[key] = value[0]
174
+ node._[">"][key] = value[1]
175
+ }
176
+
177
+ radisk(soul + enq + key, (err, value) => {
178
+ let graph
179
+ if (utils.obj.is(value)) {
180
+ Radix.map(value, each)
181
+ if (!node) each(value, key)
182
+ graph = {[soul]: node}
183
+ } else if (value) {
184
+ each(value, key)
185
+ graph = {[soul]: node}
186
+ }
187
+ cb(err, graph)
188
+ })
189
+ },
190
+ put: (graph, cb) => {
191
+ if (!graph) {
192
+ cb("graph required")
193
+ return
194
+ }
195
+
196
+ var count = 0
197
+ const ack = err => {
198
+ count--
199
+ if (ack.err) return
200
+
201
+ ack.err = err
202
+ if (ack.err) {
203
+ cb(ack.err)
204
+ return
205
+ }
206
+
207
+ if (count === 0) cb(null)
208
+ }
209
+
210
+ Object.keys(graph).forEach(soul => {
211
+ var node = graph[soul]
212
+ Object.keys(node).forEach(key => {
213
+ if (key === "_") return
214
+
215
+ count++
216
+ let value = node[key]
217
+ let state = node._[">"][key]
218
+ radisk(soul + enq + key, [value, state], ack)
219
+ })
220
+ })
221
+ },
222
+ }
223
+ }
224
+
225
+ export default Store