broadcast-encryption 1.1.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/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # broadcast-encryption
2
+
3
+ Distribute encryption keys to a dynamic set of receivers
4
+
5
+ ## Usage
6
+
7
+ ```js
8
+ const core = new Hypercore(storage)
9
+ const broadcast = new BroadcastEncryption(core, { keyPair })
10
+
11
+ const encryptionKey = Buffer.alloc(32)
12
+
13
+ // distribute key to everyone
14
+ await broadcast.update(encryptionKey, [peer1, peer2, peer3])
15
+
16
+ encryptionKey.fill(0xff) // update encryption key
17
+
18
+ // distribute new key to everyone except peer3
19
+ await broadcast.update(encryptionKey, [peer1, peer2])
20
+
21
+ await broadcast.get(id) // get the encryption key corresponding to id
22
+ ```
23
+
24
+ ## API
25
+
26
+ #### `const broadcast = new BroadcastEncryption(core, opts)`
27
+
28
+ Instantiate a new broadcast instance.
29
+
30
+ `opts` include:
31
+
32
+ ```
33
+ {
34
+ keyPair, // receiver key pair used for decryption
35
+ bootstrap // initial key information to use
36
+ }
37
+ ```
38
+
39
+ #### `const current = broadcast.id()`
40
+
41
+ The current encryption key id.
42
+
43
+ #### `await broadcast.update(key, recipients)`
44
+
45
+ Distribute the updated `key` to all members of `recipients`.
46
+
47
+ #### `const { id, encryptionKey } = await broadcast.get(id)`
48
+
49
+ Get the encryption key corresponding to `id`.
50
+
51
+ If `id` is passed as `-1`, the latest encryption key shall be returned.
52
+
53
+ #### `const encryption = await broadcast.createEncryptionProvider(opts)`
54
+
55
+ Create a [`HypercoreEncryption`](https://github.com/holepunchto/hypercore-encryption) provider to pass to a hypercore.
56
+
57
+ #### `broadcast.on('update', (id) => {})`
58
+
59
+ An `update` event is emitted when a new encryption key is loaded.
60
+
61
+ #### `const payload = BroadcastEncryption.encrypt(data, recipients)`
62
+
63
+ Static helper to broadcast encrypt a message.
64
+
65
+ #### `const data = BroadcastEncryption.decrypt(payload, recipientSecretKey)`
66
+
67
+ Static helper to decrypt a broadcast encrypted message.
68
+
69
+ #### `const payload = BroadcastEncryption.verify(payload, data, recipients)`
70
+
71
+ Static helper to verify a `payload`. Returns true if all members on `recipients` can decrypt `data`.
72
+
73
+ ## License
74
+
75
+ Apache-2.0
package/index.js ADDED
@@ -0,0 +1,291 @@
1
+ const sodium = require('sodium-universal')
2
+ const ReadyResource = require('ready-resource')
3
+ const crypto = require('hypercore-crypto')
4
+ const c = require('compact-encoding')
5
+ const safetyCatch = require('safety-catch')
6
+ const b4a = require('b4a')
7
+ const rrp = require('resolve-reject-promise')
8
+
9
+ const schema = require('./spec/broadcast-encryption')
10
+
11
+ const [DEFAULT_NAMESPACE, GENESIS_ENTROPY, NS_NONCE, NS_KEYPAIR_SEED, NS_SYMMETRIC_NONCE] =
12
+ crypto.namespace('broadcast-encryption', 5)
13
+
14
+ // ephemeral state
15
+ const nonce = b4a.alloc(sodium.crypto_stream_NONCEBYTES)
16
+ const hash = nonce.subarray(0, sodium.crypto_generichash_BYTES_MIN)
17
+ const secretKey = b4a.alloc(sodium.crypto_box_SECRETKEYBYTES)
18
+ const publicKey = b4a.alloc(sodium.crypto_box_PUBLICKEYBYTES)
19
+ const recipientKey = b4a.alloc(sodium.crypto_box_PUBLICKEYBYTES)
20
+ const symmetricKey = b4a.alloc(sodium.crypto_secretbox_KEYBYTES)
21
+
22
+ const BroadcastPayload = schema.resolveStruct('@broadcast/payload')
23
+ const BroadcastMessage = schema.resolveStruct('@broadcast/message')
24
+
25
+ module.exports = class BroadcastEncryption extends ReadyResource {
26
+ constructor(core, opts = {}) {
27
+ super()
28
+
29
+ this.core = core
30
+ this.keyPair = opts.keyPair || null
31
+
32
+ this._bootstrap = opts.bootstrap || null
33
+ this._latest = 0
34
+ }
35
+
36
+ async _open() {
37
+ await this.core.ready()
38
+
39
+ this.core.on('append', this.refresh.bind(this))
40
+ }
41
+
42
+ _close() {
43
+ return this.core.close()
44
+ }
45
+
46
+ id() {
47
+ return this.core ? this.core.length : 0
48
+ }
49
+
50
+ async refresh() {
51
+ let key = null
52
+ try {
53
+ key = await this._getLatestKey()
54
+ } catch (err) {
55
+ safetyCatch(err)
56
+ }
57
+
58
+ if (!key || key.id < this._latest) return
59
+
60
+ this._latest = key.id
61
+ this.emit('update', key.id)
62
+ }
63
+
64
+ async append(payload) {
65
+ await this._append({ payload, pointer: null })
66
+ const id = this.core.length
67
+
68
+ await this.point(id - 2) // point to previous key
69
+
70
+ return id
71
+ }
72
+
73
+ async update(key, recipients) {
74
+ const payload = await BroadcastEncryption.encrypt(key, recipients)
75
+ return this.append(payload)
76
+ }
77
+
78
+ async point(to) {
79
+ // first pointer is null to maintain offset
80
+ if (to < 0) {
81
+ return this._append({ pointer: null, payload: null })
82
+ }
83
+
84
+ const [old, current] = await Promise.all([this.get(to), this._getLatestKey()])
85
+
86
+ const buffer = encryptPointer(old.encryptionKey, current.encryptionKey, nonce)
87
+ const pointer = { to: old.id, from: current.id, nonce, buffer }
88
+
89
+ return this._append({ payload: null, pointer })
90
+ }
91
+
92
+ async _get(index, opts) {
93
+ return this.core.get(index, { ...opts, valueEncoding: BroadcastMessage })
94
+ }
95
+
96
+ async _append({ pointer, payload }) {
97
+ return this.core.append(c.encode(BroadcastMessage, { version: 0, pointer, payload }))
98
+ }
99
+
100
+ async _getLatestKey(opts) {
101
+ let id = this.core.length
102
+
103
+ while (id > 0) {
104
+ const block = await this._get(id - 1, opts)
105
+
106
+ if (block && block.payload) {
107
+ // use bootstrap if we have it
108
+ if (this._bootstrap && this._bootstrap.id === id) {
109
+ return this._bootstrap
110
+ }
111
+
112
+ const key = {
113
+ id,
114
+ encryptionKey: this._unpack(block.payload)
115
+ }
116
+
117
+ return key
118
+ }
119
+
120
+ id--
121
+ }
122
+
123
+ return { id: 0, encryptionKey: null }
124
+ }
125
+
126
+ async get(id, opts) {
127
+ if (id === -1) {
128
+ return this._getLatestKey()
129
+ }
130
+
131
+ if (id === 0) {
132
+ return { id: 0, encryptionKey: null }
133
+ }
134
+
135
+ if (this._bootstrap && this._bootstrap.id === id) {
136
+ return this._bootstrap
137
+ }
138
+
139
+ const block = await this._get(id - 1, opts)
140
+ if (!block) return null // no key in core
141
+
142
+ let encryptionKey = null
143
+
144
+ try {
145
+ encryptionKey = this._unpack(block.payload)
146
+ } catch (err) {
147
+ encryptionKey = await this._getByPointer(id)
148
+ if (!encryptionKey) throw err
149
+ }
150
+
151
+ const key = { id, encryptionKey }
152
+
153
+ return key
154
+ }
155
+
156
+ bootstrap(key) {
157
+ if (!this._bootstrap || this._bootstrap.id < key.id) {
158
+ this._bootstrap = key
159
+ this.emit('update', key.id)
160
+ }
161
+ }
162
+
163
+ async getBootstrap() {
164
+ return this._getLatestKey()
165
+ }
166
+
167
+ async _getByPointer(target) {
168
+ const bootstrap = await this._getLatestKey()
169
+ if (!bootstrap || bootstrap.id < target) return null
170
+
171
+ let id = null
172
+
173
+ let seq = bootstrap.id
174
+ let key = bootstrap.encryptionKey
175
+
176
+ while (seq > target) {
177
+ const block = await this._get(seq--)
178
+
179
+ if (block.pointer === null) continue
180
+
181
+ id = block.pointer.to
182
+ key = decryptPointer(block.pointer.buffer, block.pointer.nonce, key)
183
+
184
+ if (key === null) return null
185
+ }
186
+
187
+ return id === null ? null : key
188
+ }
189
+
190
+ _unpack(block) {
191
+ if (!this.keyPair) throw new Error('No key pair provided')
192
+
193
+ const encryptionKey = BroadcastEncryption.decrypt(block, this.keyPair.secretKey)
194
+ if (!encryptionKey) throw new Error('Broadcast decryption failed')
195
+
196
+ return encryptionKey
197
+ }
198
+
199
+ static PayloadEncoding = BroadcastPayload
200
+
201
+ static encrypt(data, recipients) {
202
+ const seed = crypto.hash([NS_KEYPAIR_SEED, data])
203
+
204
+ sodium.crypto_box_seed_keypair(publicKey, secretKey, seed)
205
+ sodium.crypto_generichash_batch(nonce, [NS_NONCE, publicKey])
206
+
207
+ const broadcast = {
208
+ publicKey,
209
+ payload: []
210
+ }
211
+
212
+ for (const recipient of recipients) {
213
+ if (recipient === null) continue
214
+
215
+ const enc = b4a.alloc(data.byteLength + sodium.crypto_box_MACBYTES)
216
+
217
+ sodium.crypto_sign_ed25519_pk_to_curve25519(recipientKey, recipient)
218
+ sodium.crypto_box_easy(enc, data, nonce, recipientKey, secretKey)
219
+
220
+ broadcast.payload.push(enc)
221
+ }
222
+
223
+ return broadcast
224
+ }
225
+
226
+ static decrypt(broadcast, recipientSecretKey) {
227
+ const { publicKey, payload } = broadcast
228
+
229
+ const data = b4a.alloc(payload[0].byteLength - sodium.crypto_box_MACBYTES)
230
+
231
+ sodium.crypto_generichash_batch(nonce, [NS_NONCE, publicKey])
232
+ sodium.crypto_sign_ed25519_sk_to_curve25519(secretKey, recipientSecretKey)
233
+
234
+ try {
235
+ for (const ciphertext of payload) {
236
+ if (sodium.crypto_box_open_easy(data, ciphertext, nonce, publicKey, secretKey)) {
237
+ return data
238
+ }
239
+ }
240
+ } finally {
241
+ b4a.fill(secretKey, 0)
242
+ }
243
+
244
+ return null
245
+ }
246
+
247
+ static verify(ciphertext, data, recipients) {
248
+ const seed = crypto.hash([NS_KEYPAIR_SEED, data])
249
+
250
+ sodium.crypto_box_seed_keypair(publicKey, secretKey, seed)
251
+ sodium.crypto_generichash_batch(nonce, [NS_NONCE, publicKey])
252
+
253
+ if (!b4a.equals(publicKey, ciphertext.publicKey)) return false
254
+
255
+ const expected = b4a.alloc(data.byteLength + sodium.crypto_box_MACBYTES)
256
+
257
+ for (const target of recipients) {
258
+ sodium.crypto_sign_ed25519_pk_to_curve25519(recipientKey, target)
259
+ sodium.crypto_box_easy(expected, data, nonce, recipientKey, secretKey)
260
+
261
+ let found = false
262
+
263
+ for (const buffer of ciphertext.payload) {
264
+ if (b4a.equals(buffer, expected)) {
265
+ found = true
266
+ break
267
+ }
268
+ }
269
+
270
+ if (!found) return false
271
+ }
272
+
273
+ return true
274
+ }
275
+ }
276
+
277
+ function encryptPointer(to, from, nonce) {
278
+ const buffer = b4a.alloc(to.byteLength + sodium.crypto_secretbox_MACBYTES)
279
+
280
+ sodium.crypto_generichash_batch(nonce, [NS_SYMMETRIC_NONCE, to])
281
+ sodium.crypto_secretbox_easy(buffer, to, nonce, from)
282
+
283
+ return buffer
284
+ }
285
+
286
+ function decryptPointer(data, nonce, key) {
287
+ const buffer = b4a.alloc(data.byteLength - sodium.crypto_secretbox_MACBYTES)
288
+ if (!sodium.crypto_secretbox_open_easy(buffer, data, nonce, key)) return null
289
+
290
+ return buffer
291
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "broadcast-encryption",
3
+ "version": "1.1.1",
4
+ "description": "Distribute encryption keys to a dynamic set of receivers",
5
+ "main": "index.js",
6
+ "files": [
7
+ "index.js",
8
+ "spec/*"
9
+ ],
10
+ "scripts": {
11
+ "format": "prettier --write .",
12
+ "test": "prettier --check . && node test/*.js"
13
+ },
14
+ "keywords": [],
15
+ "author": "Holepunch Inc.",
16
+ "license": "Apache-2.0",
17
+ "dependencies": {
18
+ "b4a": "^1.7.3",
19
+ "compact-encoding": "^2.17.0",
20
+ "hypercore": "^11.18.2",
21
+ "hyperschema": "^1.16.0",
22
+ "ready-resource": "^1.2.0",
23
+ "resolve-reject-promise": "^1.1.0",
24
+ "safety-catch": "^1.0.2",
25
+ "sodium-universal": "^5.0.1"
26
+ },
27
+ "devDependencies": {
28
+ "brittle": "^3.19.0",
29
+ "corestore": "^7.5.0",
30
+ "hypercore-crypto": "^3.6.1",
31
+ "prettier": "^3.6.2",
32
+ "prettier-config-holepunch": "^2.0.0"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/holepunchto/broadcast-encryption.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/holepunchto/broadcast-encryption/issues"
40
+ },
41
+ "homepage": "https://github.com/holepunchto/broadcast-encryption#readme"
42
+ }
@@ -0,0 +1,165 @@
1
+ // This file is autogenerated by the hyperschema compiler
2
+ // Schema Version: 2
3
+ /* eslint-disable camelcase */
4
+ /* eslint-disable quotes */
5
+ /* eslint-disable space-before-function-paren */
6
+
7
+ const { c } = require('hyperschema/runtime')
8
+
9
+ const VERSION = 2
10
+
11
+ // eslint-disable-next-line no-unused-vars
12
+ let version = VERSION
13
+
14
+ // @broadcast/payload.payload
15
+ const encoding0_1 = c.array(c.buffer)
16
+
17
+ // @broadcast/payload
18
+ const encoding0 = {
19
+ preencode(state, m) {
20
+ c.fixed32.preencode(state, m.publicKey)
21
+ encoding0_1.preencode(state, m.payload)
22
+ },
23
+ encode(state, m) {
24
+ c.fixed32.encode(state, m.publicKey)
25
+ encoding0_1.encode(state, m.payload)
26
+ },
27
+ decode(state) {
28
+ const r0 = c.fixed32.decode(state)
29
+ const r1 = encoding0_1.decode(state)
30
+
31
+ return {
32
+ publicKey: r0,
33
+ payload: r1
34
+ }
35
+ }
36
+ }
37
+
38
+ // @broadcast/pointer
39
+ const encoding1 = {
40
+ preencode(state, m) {
41
+ c.uint.preencode(state, m.to)
42
+ c.uint.preencode(state, m.from)
43
+ c.buffer.preencode(state, m.nonce)
44
+ c.buffer.preencode(state, m.buffer)
45
+ },
46
+ encode(state, m) {
47
+ c.uint.encode(state, m.to)
48
+ c.uint.encode(state, m.from)
49
+ c.buffer.encode(state, m.nonce)
50
+ c.buffer.encode(state, m.buffer)
51
+ },
52
+ decode(state) {
53
+ const r0 = c.uint.decode(state)
54
+ const r1 = c.uint.decode(state)
55
+ const r2 = c.buffer.decode(state)
56
+ const r3 = c.buffer.decode(state)
57
+
58
+ return {
59
+ to: r0,
60
+ from: r1,
61
+ nonce: r2,
62
+ buffer: r3
63
+ }
64
+ }
65
+ }
66
+
67
+ // @broadcast/message.payload
68
+ const encoding2_1 = c.frame(encoding0)
69
+ // @broadcast/message.pointer
70
+ const encoding2_2 = c.frame(encoding1)
71
+
72
+ // @broadcast/message
73
+ const encoding2 = {
74
+ preencode(state, m) {
75
+ c.uint.preencode(state, m.version)
76
+ state.end++ // max flag is 2 so always one byte
77
+
78
+ if (m.payload) encoding2_1.preencode(state, m.payload)
79
+ if (m.pointer) encoding2_2.preencode(state, m.pointer)
80
+ },
81
+ encode(state, m) {
82
+ const flags = (m.payload ? 1 : 0) | (m.pointer ? 2 : 0)
83
+
84
+ c.uint.encode(state, m.version)
85
+ c.uint.encode(state, flags)
86
+
87
+ if (m.payload) encoding2_1.encode(state, m.payload)
88
+ if (m.pointer) encoding2_2.encode(state, m.pointer)
89
+ },
90
+ decode(state) {
91
+ const r0 = c.uint.decode(state)
92
+ const flags = c.uint.decode(state)
93
+
94
+ return {
95
+ version: r0,
96
+ payload: (flags & 1) !== 0 ? encoding2_1.decode(state) : null,
97
+ pointer: (flags & 2) !== 0 ? encoding2_2.decode(state) : null
98
+ }
99
+ }
100
+ }
101
+
102
+ function setVersion(v) {
103
+ version = v
104
+ }
105
+
106
+ function encode(name, value, v = VERSION) {
107
+ version = v
108
+ return c.encode(getEncoding(name), value)
109
+ }
110
+
111
+ function decode(name, buffer, v = VERSION) {
112
+ version = v
113
+ return c.decode(getEncoding(name), buffer)
114
+ }
115
+
116
+ function getEnum(name) {
117
+ switch (name) {
118
+ default:
119
+ throw new Error('Enum not found ' + name)
120
+ }
121
+ }
122
+
123
+ function getEncoding(name) {
124
+ switch (name) {
125
+ case '@broadcast/payload':
126
+ return encoding0
127
+ case '@broadcast/pointer':
128
+ return encoding1
129
+ case '@broadcast/message':
130
+ return encoding2
131
+ default:
132
+ throw new Error('Encoder not found ' + name)
133
+ }
134
+ }
135
+
136
+ function getStruct(name, v = VERSION) {
137
+ const enc = getEncoding(name)
138
+ return {
139
+ preencode(state, m) {
140
+ version = v
141
+ enc.preencode(state, m)
142
+ },
143
+ encode(state, m) {
144
+ version = v
145
+ enc.encode(state, m)
146
+ },
147
+ decode(state) {
148
+ version = v
149
+ return enc.decode(state)
150
+ }
151
+ }
152
+ }
153
+
154
+ const resolveStruct = getStruct // compat
155
+
156
+ module.exports = {
157
+ resolveStruct,
158
+ getStruct,
159
+ getEnum,
160
+ getEncoding,
161
+ encode,
162
+ decode,
163
+ setVersion,
164
+ version
165
+ }
@@ -0,0 +1,84 @@
1
+ {
2
+ "version": 2,
3
+ "schema": [
4
+ {
5
+ "name": "payload",
6
+ "namespace": "broadcast",
7
+ "compact": false,
8
+ "flagsPosition": -1,
9
+ "fields": [
10
+ {
11
+ "name": "publicKey",
12
+ "required": true,
13
+ "type": "fixed32",
14
+ "version": 1
15
+ },
16
+ {
17
+ "name": "payload",
18
+ "required": true,
19
+ "array": true,
20
+ "type": "buffer",
21
+ "version": 1
22
+ }
23
+ ]
24
+ },
25
+ {
26
+ "name": "pointer",
27
+ "namespace": "broadcast",
28
+ "compact": false,
29
+ "flagsPosition": -1,
30
+ "fields": [
31
+ {
32
+ "name": "to",
33
+ "required": true,
34
+ "type": "uint",
35
+ "version": 1
36
+ },
37
+ {
38
+ "name": "from",
39
+ "required": true,
40
+ "type": "uint",
41
+ "version": 1
42
+ },
43
+ {
44
+ "name": "nonce",
45
+ "required": true,
46
+ "type": "buffer",
47
+ "version": 1
48
+ },
49
+ {
50
+ "name": "buffer",
51
+ "required": true,
52
+ "type": "buffer",
53
+ "version": 2
54
+ }
55
+ ]
56
+ },
57
+ {
58
+ "name": "message",
59
+ "namespace": "broadcast",
60
+ "compact": false,
61
+ "flagsPosition": 1,
62
+ "fields": [
63
+ {
64
+ "name": "version",
65
+ "required": true,
66
+ "type": "uint",
67
+ "version": 1
68
+ },
69
+ {
70
+ "name": "payload",
71
+ "required": false,
72
+ "type": "@broadcast/payload",
73
+ "version": 1
74
+ },
75
+ {
76
+ "name": "pointer",
77
+ "required": false,
78
+ "type": "@broadcast/pointer",
79
+ "version": 1
80
+ }
81
+ ]
82
+ }
83
+ ]
84
+ }