Files: 2b5b9e5f44cb461abdf40982a644f2f48709c965 / index.js
6236 bytesRaw
1 | var AtomicFile = require('atomic-file') |
2 | var path = require('path') |
3 | var Reduce = require('flumeview-reduce') |
4 | var group_box = require('group-box') |
5 | var mkdirp = require('mkdirp') |
6 | var u = require('./util') |
7 | var ref = require('ssb-ref') |
8 | |
9 | //by deriving the message key from the group id (the founding |
10 | //message id) and the unbox key for that message, this ensures |
11 | //someone can't decrypt the message without knowing the founding |
12 | //message, therefore avoiding surruptious sharing the group. |
13 | //they can't _not_ know who made the group, so if someone else |
14 | //shares it to them, they know they are being sneaky. |
15 | |
16 | //and, you can verify this property from the design! you can't |
17 | //rewrite this code so they don't know the founding message |
18 | //and still be able to decrypt these messages. |
19 | |
20 | exports.name = 'private-groups' |
21 | exports.version = require('./package').version |
22 | exports.manifest = { |
23 | get: 'async', |
24 | addGroupKey: 'async', |
25 | addCurvePair: 'async', |
26 | forget: 'async' |
27 | } |
28 | |
29 | exports.init = function (sbot, config) { |
30 | |
31 | var dir = path.join(config.path, 'private-groups') |
32 | |
33 | var af = AtomicFile(path.join(dir, 'local-keys.json')) |
34 | var ready = false, waiting = [] |
35 | mkdirp(dir, function () { |
36 | af.get(function (err, data) { |
37 | keyState = data || {msgKeys: [], groupKeys: []} |
38 | ready = true |
39 | while(waiting.length) waiting.shift()() |
40 | }) |
41 | }) |
42 | |
43 | function onReady (fn) { |
44 | if(ready) fn() |
45 | else waiting.push(fn) |
46 | } |
47 | |
48 | //no, pass in from id too. |
49 | sbot.box.hook(function (fn, args) { |
50 | var content = args[0] |
51 | var state = args[1] |
52 | var recps = content.recps |
53 | //check if this is something we can't handle as box2 |
54 | if(!recps.every(function (id) { |
55 | return ref.isFeed(id) ? state[id] : keyState.groupKeys[id] |
56 | })) |
57 | return fn.apply(this, args) //fallback |
58 | |
59 | var prev = u.id2Buffer(state.id) |
60 | |
61 | return group_box( |
62 | Buffer.from(JSON.stringify(content), 'base64'), |
63 | prev, |
64 | recps.map(function (id) { |
65 | return id //??? |
66 | }) |
67 | ) |
68 | |
69 | } |
70 | |
71 | //state: |
72 | /* |
73 | { |
74 | <author>: [{ |
75 | sequence: <sequence at which author set this key>, |
76 | key: <author's latest privacy key> |
77 | }] |
78 | } |
79 | */ |
80 | |
81 | var keyState = null |
82 | var state = null |
83 | //cache: {<author>: [scalar_mult(msg_keys[i], <author's latest privacy key>)] |
84 | var cache = {} |
85 | |
86 | //maybe in the future, use a level db here. |
87 | var remoteKeys = sbot._flumeUse('private-groups/remote-keys', Reduce(1, function (acc, data) { |
88 | state = acc = acc || {} |
89 | var msg = data.value |
90 | if(msg.content.type === 'private-msg-key') { |
91 | acc[msg.author] = [{sequence: msg.sequence, key: msg.content.key}] |
92 | cache[msg.author] = null |
93 | } |
94 | return acc |
95 | })) |
96 | |
97 | sbot.addMap(function (data, cb) { |
98 | if(!u.isBox2(data.value.content)) return cb(null, data) |
99 | //the views and keyState have not been loaded |
100 | //delay processing any box2 messages until they are. |
101 | if(ready) cb(null, data) |
102 | else waiting.push(function () { |
103 | cb(null, data) |
104 | }) |
105 | }) |
106 | |
107 | sbot.addUnboxer({ |
108 | name: 'private-msg-key', |
109 | key: function (content, value) { |
110 | if(!u.isBox2(content)) return |
111 | //a_state is reverse chrono list of author's private-msg-keys |
112 | //take the latest key that has sequence less than message |
113 | //to decrypt |
114 | var a_state = state[value.author] |
115 | if(!a_state) return console.log('no author state') |
116 | |
117 | var keys_to_try = cache[value.author] |
118 | var a_key |
119 | for(var i = 0; i < a_state.length; i++) { |
120 | if(a_state[i].sequence < value.sequence) { |
121 | a_key = a_state[i].key |
122 | break; |
123 | } |
124 | } |
125 | if(!a_key) return console.log('no author key') |
126 | |
127 | if(!keys_to_try) |
128 | keys_to_try = cache[value.author] = u.scalarmultKeys(a_key, keyState.msgKeys) |
129 | |
130 | //the very first message cannot be a group_box. |
131 | if(value.previous == null) return |
132 | var ctxt = u.ctxt2Buffer(content) |
133 | var nonce = u.id2Buffer(value.previous) |
134 | console.log(content, ctxt, ctxt.length) |
135 | |
136 | var key = group_box.unboxKey(ctxt, nonce, keys_to_try, 8) |
137 | if(key) return key |
138 | |
139 | //should group keys be included in this plugin? |
140 | //yes, because box2 supports both direct keys and group keys. |
141 | var group_keys = [] |
142 | for(var id in keyState.groupKeys) |
143 | group_keys.push(u.getGroupMsgKey(nonce, keyState.groupKeys[id])) |
144 | |
145 | //note: if we only allow groups in the first 4 slots |
146 | //that means better sort them before any individuals |
147 | key = group_box.unboxKey( //groups we are in |
148 | ctxt, nonce, group_keys, 4 |
149 | ) |
150 | if(key) return key |
151 | |
152 | }, |
153 | value: function (content, key, value) { |
154 | if(!u.isBox2(content)) return |
155 | var ctxt = u.ctxt2Buffer(content) |
156 | var nonce = u.id2Buffer(value.previous) |
157 | try { |
158 | return JSON.parse(group_box.unboxBody(ctxt, nonce, key).toString()) |
159 | } catch (_) {} |
160 | } |
161 | }) |
162 | |
163 | return { |
164 | addGroupKey: function (group, cb) { |
165 | console.log(group, u.isUnboxKey(group.unbox)) |
166 | if(!ref.isMsg(group.id)) return cb(new Error('id must be a message id')) |
167 | if(!u.isUnboxKey(group.unbox)) return cb(new Error('id must be a 32 byte base64 value')) |
168 | af.get(function () { |
169 | keyState.groupKeys[u.hmac(u.id2Buffer(group.id), Buffer.from(group.unbox, 'base64'))] = group |
170 | af.set(keyState, cb) |
171 | }) |
172 | }, |
173 | addCurvePair: function (curve_keys, cb) { |
174 | onReady(function () { |
175 | if(!u.isCurvePair(curve_keys)) |
176 | return cb(new Error('expected a pair of curve25519 keys')) |
177 | keyState.msgKeys.push(curve_keys) |
178 | cache = {} //clear cache, so it's regenerated up to date. |
179 | //NOTE: identiy adding this key must publish |
180 | // a message advertising this receive key, or no one |
181 | // will send them messages! |
182 | af.set(keyState, cb) |
183 | }) |
184 | }, |
185 | //forgetting old keys. crude basis for forward secrecy. |
186 | //you will no longer be able to decrypt messages sent to curve_pk |
187 | forget: function (curve_pk, cb) { |
188 | af.get(function () { |
189 | for(var i = msg_keys.length-1; i >= 0; i--) |
190 | if(curve_pk == msg_keys[i].public) |
191 | msg_keys.splice(i, 1) |
192 | cache = {} //clear cache, will be regenerated. |
193 | af.set(msg_keys, cb) |
194 | }) |
195 | } |
196 | } |
197 | } |
198 | |
199 | |
200 | |
201 | |
202 |
Built with git-ssb-web