@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.
- package/.github/workflows/node.js.yml +31 -0
- package/LICENSE.md +55 -0
- package/README.md +24 -0
- package/build/holster.js +4 -0
- package/build/holster.js.map +7 -0
- package/examples/index.html +33 -0
- package/examples/index.js +49 -0
- package/examples/user.html +31 -0
- package/examples/user.js +88 -0
- package/examples/wire.html +27 -0
- package/examples/wire.js +55 -0
- package/package.json +44 -0
- package/src/array.js +34 -0
- package/src/buffer.js +83 -0
- package/src/dup.js +23 -0
- package/src/get.js +19 -0
- package/src/ham.js +151 -0
- package/src/holster.js +502 -0
- package/src/index.js +21 -0
- package/src/radisk.js +438 -0
- package/src/radix.js +106 -0
- package/src/sea-utils.js +65 -0
- package/src/sea.js +224 -0
- package/src/store.js +225 -0
- package/src/user.js +330 -0
- package/src/utils.js +124 -0
- package/src/wire.js +290 -0
- package/test/holster.get.test.js +60 -0
- package/test/holster.lex.test.js +196 -0
- package/test/holster.off.test.js +53 -0
- package/test/holster.on.test.js +168 -0
- package/test/holster.put.test.js +278 -0
- package/test/holster.secure.test.js +210 -0
- package/test/holster.user.get.test.js +86 -0
- package/test/holster.user.lex.test.js +148 -0
- package/test/holster.user.off.test.js +72 -0
- package/test/holster.user.on.test.js +185 -0
- package/test/holster.user.put.test.js +381 -0
- package/test/kitty/! +9 -0
- package/test/names.js +225 -0
- package/test/radisk.test.js +191 -0
- package/test/radix.test.js +117 -0
- package/test/sea.test.js +284 -0
- package/test/store.test.js +316 -0
- package/test/user.test.js +173 -0
- 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
|