git ssb

1+

Dominic / ssb-private-groups



Tree: 2b5b9e5f44cb461abdf40982a644f2f48709c965

Files: 2b5b9e5f44cb461abdf40982a644f2f48709c965 / index.js

6236 bytesRaw
1var AtomicFile = require('atomic-file')
2var path = require('path')
3var Reduce = require('flumeview-reduce')
4var group_box = require('group-box')
5var mkdirp = require('mkdirp')
6var u = require('./util')
7var 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
20exports.name = 'private-groups'
21exports.version = require('./package').version
22exports.manifest = {
23 get: 'async',
24 addGroupKey: 'async',
25 addCurvePair: 'async',
26 forget: 'async'
27}
28
29exports.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