Files: 86f3b92eaac8f991e0ec002cb08d0b0120608e21 / index.js
4600 bytesRaw
1 | function isBox2(ctxt) { |
2 | //we can just do a fairly lax check here, don't check the content |
3 | //is canonical base64, because that check has already been done. |
4 | return 'string' == typeof ctxt && /\.box2$/.test(ctxt) |
5 | } |
6 | |
7 | function ctxtToBuffer(ctxt) { |
8 | return isBox2(ctxt) && Buffer.from(ctxt.substring(ctxt.indexOf('.')), 'base64') |
9 | } |
10 | |
11 | function idToBuffer (id) { |
12 | return Buffer.from(id.substring(1, id.indexOf('.')), 'base64') |
13 | } |
14 | |
15 | //by deriving the message key from the group id (the founding |
16 | //message id) and the unbox key for that message, this ensures |
17 | //someone can't decrypt the message without knowing the founding |
18 | //message, therefore avoiding surruptious sharing the group. |
19 | //they can't _not_ know who made the group, so if someone else |
20 | //shares it to them, they know they are being sneaky. |
21 | |
22 | //and, you can verify this property from the design! you can't |
23 | //rewrite this code so they don't know the founding message |
24 | //and still be able to decrypt these messages. |
25 | |
26 | function getGroupMsgKey(previous, group) { |
27 | return hmac(Buffer.concat([previous, group.id]), group.unbox) |
28 | } |
29 | |
30 | exports.init = function () { |
31 | |
32 | var af = AtomicFile( |
33 | path.join(config.path, 'private-groups/local-keys.json') |
34 | ) |
35 | var ready = false, waiting = [] |
36 | af.get(function (err, data) { |
37 | keyState = data || {msgKeys: [], groupKeys: []} |
38 | ready = true |
39 | while(waiting.length) waiting.shift()() |
40 | }) |
41 | |
42 | //state: |
43 | /* |
44 | { |
45 | <author>: { |
46 | key: <author's latest privacy key> |
47 | } |
48 | } |
49 | */ |
50 | |
51 | var keyState = null |
52 | var state = null |
53 | //cache: {<author>: [scalar_mult(msg_keys[i], <author's latest privacy key>)] |
54 | var cache = {} |
55 | |
56 | |
57 | sbot._flumeUse('private-groups/remote-keys', Reduce(1, function (acc, msg) { |
58 | state = acc = acc || {} |
59 | if(msg.content.type === 'private-msg-key') { |
60 | acc[msg.author] = [{sequence: msg.sequence, key: msg.content.key}] |
61 | cache[msg.author] = null |
62 | } |
63 | }) |
64 | |
65 | //sbot._flumeUse('private-groups/old-remote-keys', Level(1, function (data) { |
66 | // if(msg.content.type === 'private-msg-key') { |
67 | // return [msg.author, msg.sequence, msg.content.type] |
68 | // } |
69 | //}) |
70 | |
71 | sbot.addMap(function (data, cb) { |
72 | if(!isBox2(data.value.content)) return cb(null, data) |
73 | //the views and keyState have not been loaded |
74 | //delay processing any box2 messages until they are. |
75 | if(ready) cb(null, data) |
76 | else waiting.push(function () { |
77 | cb(null, data) |
78 | }) |
79 | }) |
80 | |
81 | sbot.addUnboxer({ |
82 | key: |
83 | function (content, value) { |
84 | if(!isBox2(content)) return |
85 | var a_state = state[value.author] |
86 | if(!a_state) return |
87 | |
88 | var keys_to_try = cache[value.author] |
89 | if(!keys_to_try) { |
90 | keys_to_try = cache[value.author] = keyState.msgKeys.map(function (key) { |
91 | return scalarmult(a_state.key, curve.private) |
92 | }) |
93 | |
94 | var ctxt = ctxtToBuffer(content), nonce = idToBuffer(value.previous) |
95 | var key = groupbox.unboxKey( //direct recipients |
96 | ctxt, nonce, keys_to_try, 8 |
97 | ) |
98 | if(key) return key |
99 | |
100 | var group_keys = [] |
101 | for(var id in keyState.groupKeys) |
102 | group_keys.push(getGroupMsgKey(nonce, keyState.groupKeys[id]) |
103 | //note: if we only allow groups in the first 4 slots |
104 | //that means better sort them before any individuals |
105 | key = groupbox.unboxKey( //groups we are in |
106 | ctxt, nonce, group_keys, 4 |
107 | ) |
108 | if(key) return key |
109 | }, |
110 | value: function (content, key) { |
111 | if(!isBox2(content)) return |
112 | return groupbox.unboxBody(content, key) |
113 | } |
114 | }) |
115 | |
116 | return { |
117 | // addGroupKey: function (group, cb) { |
118 | // af.get(function () { |
119 | // keyState.groupKeys[hmac(group.id, group.unbox)] = group) |
120 | // af.set(keys, cb) |
121 | // }) |
122 | // }, |
123 | addCurvePair: function (curve_keys, cb) { |
124 | if(!isCurvePair(curve_keys)) return cb(new Error('expected a pair of curve25519 keys') |
125 | keyState.msgKeys.push(curve_keys) |
126 | cache = {} //clear cache, so it's regenerated up to date. |
127 | //NOTE: identiy adding this key must publish |
128 | // a message advertising this receive key, or no one |
129 | // will send them messages! |
130 | af.set(msg_keys, cb) |
131 | }, |
132 | //forgetting old keys. crude basis for forward secrecy. |
133 | //you will no longer be able to decrypt messages sent to curve_pk |
134 | forget: function (curve_pk, cb) { |
135 | af.get(function () { |
136 | for(var i = msg_keys.length-1; i >= 0; i--) |
137 | if(curve_pk == msg_keys[i].public) |
138 | msg_keys.splice(i, 1) |
139 | cache = {} //clear cache, will be regenerated. |
140 | af.set(msg_keys, cb) |
141 | }) |
142 | } |
143 | } |
144 | } |
145 | |
146 | |
147 | |
148 | |
149 | |
150 | |
151 | |
152 |
Built with git-ssb-web