git ssb

1+

Dominic / ssb-private-groups



Tree: 4de96c011f2e32da25a86df102a8af549f39d44c

Files: 4de96c011f2e32da25a86df102a8af549f39d44c / index.js

5639 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 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
20function hmac (a, b) {
21 return cl.crypto_auth(u.toBuffer(a), u.toBuffer(b))
22}
23
24function 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
29exports.name = 'private-groups'
30
31exports.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