git ssb

1+

Dominic / ssb-private-groups



Tree: f57b58569dafb4a037a0648c9e7731063a14b691

Files: f57b58569dafb4a037a0648c9e7731063a14b691 / index.js

5389 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 getGroupMsgKey(previous, group) {
21 return hmac(Buffer.concat([previous, group.id]), group.unbox)
22}
23
24exports.name = 'private-groups'
25
26exports.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