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