Files: 4de96c011f2e32da25a86df102a8af549f39d44c / index.js
5639 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 cl = require('chloride') |
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 | function hmac (a, b) { |
21 | return cl.crypto_auth(u.toBuffer(a), u.toBuffer(b)) |
22 | } |
23 | |
24 | function getGroupMsgKey(previous, group) { |
25 | //or would it be better to use generic hash (with key?) |
26 | return hmac(Buffer.concat([previous, group.id]), u.toBuffer(group.unbox)) |
27 | } |
28 | |
29 | exports.name = 'private-groups' |
30 | |
31 | exports.init = function (sbot, config) { |
32 | |
33 | var dir = path.join(config.path, 'private-groups') |
34 | |
35 | var af = AtomicFile(path.join(dir, 'local-keys.json')) |
36 | var ready = false, waiting = [] |
37 | mkdirp(dir, function () { |
38 | af.get(function (err, data) { |
39 | keyState = data || {msgKeys: [], groupKeys: []} |
40 | ready = true |
41 | while(waiting.length) waiting.shift()() |
42 | }) |
43 | }) |
44 | |
45 | function onReady (fn) { |
46 | if(ready) fn() |
47 | else waiting.push(fn) |
48 | } |
49 | |
50 | //state: |
51 | /* |
52 | { |
53 | <author>: [{ |
54 | sequence: <sequence at which author set this key>, |
55 | key: <author's latest privacy key> |
56 | }] |
57 | } |
58 | */ |
59 | |
60 | var keyState = null |
61 | var state = null |
62 | //cache: {<author>: [scalar_mult(msg_keys[i], <author's latest privacy key>)] |
63 | var cache = {} |
64 | |
65 | //maybe in the future, use a level db here. |
66 | var remoteKeys = sbot._flumeUse('private-groups/remote-keys', Reduce(1, function (acc, data) { |
67 | state = acc = acc || {} |
68 | var msg = data.value |
69 | if(msg.content.type === 'private-msg-key') { |
70 | acc[msg.author] = [{sequence: msg.sequence, key: msg.content.key}] |
71 | cache[msg.author] = null |
72 | } |
73 | return acc |
74 | })) |
75 | |
76 | sbot.addMap(function (data, cb) { |
77 | if(!u.isBox2(data.value.content)) return cb(null, data) |
78 | //the views and keyState have not been loaded |
79 | //delay processing any box2 messages until they are. |
80 | if(ready) cb(null, data) |
81 | else waiting.push(function () { |
82 | cb(null, data) |
83 | }) |
84 | }) |
85 | |
86 | sbot.addUnboxer({ |
87 | name: 'private-msg-key', |
88 | key: function (content, value) { |
89 | if(!u.isBox2(content)) return |
90 | //a_state is reverse chrono list of author's private-msg-keys |
91 | //take the latest key that has sequence less than message |
92 | //to decrypt |
93 | var a_state = state[value.author] |
94 | if(!a_state) return console.log('no author state') |
95 | |
96 | var keys_to_try = cache[value.author] |
97 | var a_key |
98 | for(var i = 0; i < a_state.length; i++) { |
99 | if(a_state[i].sequence < value.sequence) { |
100 | a_key = a_state[i].key |
101 | break; |
102 | } |
103 | } |
104 | if(!a_key) return console.log('no author key') |
105 | |
106 | if(!keys_to_try) |
107 | keys_to_try = cache[value.author] = keyState.msgKeys.map(function (curve) { |
108 | return cl.crypto_scalarmult( |
109 | Buffer.from(curve.private, 'base64'), |
110 | Buffer.from(a_key, 'base64') |
111 | ) |
112 | }) |
113 | |
114 | //the very first message cannot be a group_box. |
115 | if(value.previous == null) return |
116 | var ctxt = u.ctxt2Buffer(content) |
117 | var nonce = u.id2Buffer(value.previous) |
118 | |
119 | var key = group_box.unboxKey(ctxt, nonce, keys_to_try, 8) |
120 | if(key) return key |
121 | |
122 | //should group keys be included in this plugin? |
123 | //yes, because box2 supports both direct keys and group keys. |
124 | var group_keys = [] |
125 | for(var id in keyState.groupKeys) |
126 | group_keys.push(getGroupMsgKey(nonce, keyState.groupKeys[id])) |
127 | |
128 | //note: if we only allow groups in the first 4 slots |
129 | //that means better sort them before any individuals |
130 | key = group_box.unboxKey( //groups we are in |
131 | ctxt, nonce, group_keys, 4 |
132 | ) |
133 | if(key) return key |
134 | |
135 | }, |
136 | value: function (content, key, value) { |
137 | if(!u.isBox2(content)) return |
138 | var ctxt = u.ctxt2Buffer(content) |
139 | var nonce = u.id2Buffer(value.previous) |
140 | try { |
141 | return JSON.parse(group_box.unboxBody(ctxt, nonce, key).toString()) |
142 | } catch (_) {} |
143 | } |
144 | }) |
145 | |
146 | return { |
147 | get: remoteKeys.get, |
148 | addGroupKey: function (group, cb) { |
149 | af.get(function () { |
150 | keyState.groupKeys[hmac(group.id, group.unbox)] = group |
151 | af.set(keys, cb) |
152 | }) |
153 | }, |
154 | addCurvePair: function (curve_keys, cb) { |
155 | onReady(function () { |
156 | if(!u.isCurvePair(curve_keys)) |
157 | return cb(new Error('expected a pair of curve25519 keys')) |
158 | keyState.msgKeys.push(curve_keys) |
159 | cache = {} //clear cache, so it's regenerated up to date. |
160 | //NOTE: identiy adding this key must publish |
161 | // a message advertising this receive key, or no one |
162 | // will send them messages! |
163 | af.set(keyState, cb) |
164 | }) |
165 | }, |
166 | //forgetting old keys. crude basis for forward secrecy. |
167 | //you will no longer be able to decrypt messages sent to curve_pk |
168 | forget: function (curve_pk, cb) { |
169 | af.get(function () { |
170 | for(var i = msg_keys.length-1; i >= 0; i--) |
171 | if(curve_pk == msg_keys[i].public) |
172 | msg_keys.splice(i, 1) |
173 | cache = {} //clear cache, will be regenerated. |
174 | af.set(msg_keys, cb) |
175 | }) |
176 | } |
177 | } |
178 | } |
179 | |
180 |
Built with git-ssb-web