Commit 86f3b92eaac8f991e0ec002cb08d0b0120608e21
initial
Dominic Tarr committed on 12/27/2018, 9:19:59 AMFiles changed
LICENSE | added |
README.md | added |
index.js | added |
package.json | added |
LICENSE | ||
---|---|---|
@@ -1,0 +1,22 @@ | ||
1 … | +Copyright (c) 2018 Dominic Tarr | |
2 … | + | |
3 … | +Permission is hereby granted, free of charge, | |
4 … | +to any person obtaining a copy of this software and | |
5 … | +associated documentation files (the "Software"), to | |
6 … | +deal in the Software without restriction, including | |
7 … | +without limitation the rights to use, copy, modify, | |
8 … | +merge, publish, distribute, sublicense, and/or sell | |
9 … | +copies of the Software, and to permit persons to whom | |
10 … | +the Software is furnished to do so, | |
11 … | +subject to the following conditions: | |
12 … | + | |
13 … | +The above copyright notice and this permission notice | |
14 … | +shall be included in all copies or substantial portions of the Software. | |
15 … | + | |
16 … | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
17 … | +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES | |
18 … | +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. | |
19 … | +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR | |
20 … | +ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, | |
21 … | +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE | |
22 … | +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
README.md | ||
---|---|---|
@@ -1,0 +1,31 @@ | ||
1 … | +# ssb-private-groups | |
2 … | + | |
3 … | + | |
4 … | +## receive key | |
5 … | + | |
6 … | +to indicate you support box2, post a message | |
7 … | + | |
8 … | +``` | |
9 … | +{ | |
10 … | + type: 'private-msg-key', | |
11 … | + key: curve25519.public | |
12 … | +} | |
13 … | +`` | |
14 … | +when decrypting messages from another feed, | |
15 … | +you know it came from their most recent | |
16 … | +private message key, because they are in a strict order. | |
17 … | + | |
18 … | +when decrypting messages _for your feed_ these must | |
19 … | +be combined with each private-msg-key you retain. | |
20 … | +when rotating keys, it is advisable to keep both keys | |
21 … | +alive for an overlap period, to improve chances that | |
22 … | +sender has your new key when they write a message to you. | |
23 … | + | |
24 … | +If they write a message to a key you have discarded, | |
25 … | +you won't know, because you'll be unable to decrypt that message. | |
26 … | + | |
27 … | + | |
28 … | + | |
29 … | +## License | |
30 … | + | |
31 … | +MIT |
index.js | ||
---|---|---|
@@ -1,0 +1,151 @@ | ||
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 … | + |
package.json | ||
---|---|---|
@@ -1,0 +1,19 @@ | ||
1 … | +{ | |
2 … | + "name": "ssb-private-groups", | |
3 … | + "description": "", | |
4 … | + "version": "1.0.0", | |
5 … | + "homepage": "https://github.com/dominictarr/ssb-private-groups", | |
6 … | + "repository": { | |
7 … | + "type": "git", | |
8 … | + "url": "git://github.com/dominictarr/ssb-private-groups.git" | |
9 … | + }, | |
10 … | + "dependencies": { | |
11 … | + }, | |
12 … | + "devDependencies": { | |
13 … | + }, | |
14 … | + "scripts": { | |
15 … | + "test": "set -e; for t in test/*.js; do node $t; done" | |
16 … | + }, | |
17 … | + "author": "Dominic Tarr <dominic.tarr@gmail.com> (http://dominictarr.com)", | |
18 … | + "license": "MIT" | |
19 … | +} |
Built with git-ssb-web