index.jsView |
---|
1 | | -var ssbKeys = require('ssb-keys') |
| 1 … | +var I = require('./valid') |
2 | 2 … | |
3 | | -var u = require('./util') |
| 3 … | + |
| 4 … | +uxer (someone who observes an invite, but not directly involved): |
4 | 5 … | |
5 | | -var invite_key = require('./cap') |
| 6 … | +if we see |
| 7 … | + someone post an invite I |
| 8 … | + someone else post a confirmation C that I has been accepted A |
| 9 … | + |
| 10 … | + emded that A inside a C(A) |
| 11 … | + match valid I->A's and interpret them like follows. |
6 | 12 … | |
7 | | -function code(err, c) { |
8 | | - err.code = 'user-invites:'+c |
9 | | - return err |
10 | | -} |
| 13 … | + we only care about confirmations if it's of an invite we follow, |
| 14 … | + and it hasn't been confirmed already. |
11 | 15 … | |
12 | | -exports.createInvite = function (seed, host, reveal, private) { |
13 | | - var keys = ssbKeys.generate(null, seed) |
14 | | - if(keys.id === host) |
15 | | - throw code(new Error('do not create invite with own public key'), 'user-invites:no-own-goal') |
16 | | - return ssbKeys.signObj(keys, invite_key, { |
17 | | - type: 'invite', |
18 | | - invite: keys.id, |
19 | | - host: host, |
20 | | - reveal: reveal ? u.box(reveal, u.hash(u.hash(seed))) : undefined, |
21 | | - private: private ? u.box(private, u.hash(seed)) : undefined |
22 | | - }) |
23 | | -} |
| 16 … | +{ |
| 17 … | + invited: { |
| 18 … | + <alice>: { <bob>: true, ...} |
| 19 … | + } |
24 | 20 … | |
25 | | -exports.verifyInvitePublic = function (msg) { |
26 | | - if(msg.content.host != msg.author) |
27 | | - throw code(new Error('host did not match author'), 'host-must-match-author') |
| 21 … | + invites: { <invites>, ...} |
| 22 … | + accepts: { <accepts>, ...} |
28 | 23 … | |
29 | | - if(!ssbKeys.verifyObj(msg.content.invite, invite_key, msg.content)) |
30 | | - throw code(new Error('invalid invite signature'), 'invite-signature-failed') |
31 | | - |
32 | | - |
33 | | - if(!ssbKeys.verifyObj(msg.author, msg)) |
34 | | - throw code(new Error('invalid host signature'), 'host-signature-failed') |
35 | | - return true |
36 | 24 … | } |
37 | 25 … | |
38 | | -exports.verifyInvitePrivate = function (msg, seed) { |
39 | | - exports.verifyInvitePublic(msg) |
40 | | - if(msg.content.reveal) { |
41 | | - var reveal = u.unbox(msg.content.reveal, u.hash(u.hash(seed))) |
42 | | - if(!reveal) throw code(new Error('could not decrypt reveal field'), 'decrypt-reveal-failed') |
43 | | - } |
44 | | - if(msg.content.private) { |
45 | | - var private = u.unbox(msg.content.private, u.hash(seed)) |
46 | | - if(!private) throw code(new Error('could not decrypt private field'), 'decrypt-private-failed') |
47 | | - } |
| 26 … | +--- |
48 | 27 … | |
49 | | - return {reveal: reveal, private: private} |
50 | | -} |
| 28 … | +pub |
51 | 29 … | |
52 | | -exports.createAccept = function (msg, seed, id) { |
53 | | - exports.verifyInvitePrivate(msg, seed) |
54 | | - var keys = ssbKeys.generate(null, seed) |
55 | | - if(keys.id != msg.content.invite) |
56 | | - throw code(new Error('seed does not match invite'), 'seed-must-match-invite') |
57 | | - var inviteId = '%'+ssbKeys.hash(JSON.stringify(msg, null, 2)) |
58 | | - return ssbKeys.signObj(keys, invite_key, { |
59 | | - type: 'invite/accept', |
60 | | - receipt: inviteId, |
61 | | - id: id, |
62 | | - key: msg.content.reveal ? u.hash(u.hash(seed)).toString('base64') : undefined |
63 | | - }) |
64 | | -} |
| 30 … | + someone connects, using key from an open invite I |
| 31 … | + they request that invite I (by it's id) |
| 32 … | + they send a message accepting A the invite. |
| 33 … | + the pub then posts confirmation (I,A) |
65 | 34 … | |
66 | | -exports.verifyAccept = function (accept, invite) { |
67 | | - if(!invite) throw new Error('invite must be provided') |
| 35 … | +*/ |
68 | 36 … | |
69 | | - if(accept.content.type !== 'invite/accept') |
70 | | - throw code(new Error('accept must be type: "invite/accept", was:'+JSON.stringify(accept.content.type)), 'user-invites:accept-message-type') |
71 | | - if(invite.content.type !== 'invite') |
72 | | - throw code(new Error('accept must be type: invite, was:'+accept.content.type), 'user-invites:invite-message-type') |
| 37 … | +exports.name = 'invites' |
73 | 38 … | |
74 | | - var invite_id = '%'+ssbKeys.hash(JSON.stringify(invite, null, 2)) |
75 | | - var reveal |
| 39 … | +exports.version = '1.0.0' |
| 40 … | +exports.manifest = { |
76 | 41 … | |
77 | | - if(invite_id !== accept.content.receipt) |
78 | | - throw code(new Error('acceptance not matched to given invite, got:'+invite_id+' expected:'+accept.content.receipt), 'accept-wrong-invite') |
79 | | - |
80 | | - if(accept.author === invite.content.id) |
81 | | - throw code(new Error('invitee must use a new key, not the same seed'), 'guest-key-reuse') |
82 | | - if(invite.content.reveal) { |
83 | | - if(!accept.content.key) |
84 | | - throw code(new Error('accept missing reveal key, when invite has it'), 'accept-must-reveal-key') |
85 | | - reveal = u.unbox(invite.content.reveal, new Buffer(accept.content.key, 'base64')) |
86 | | - if(!reveal) throw code(new Error('accept did not correctly reveal invite'), 'decrypt-accept-reveal-failed') |
87 | | - } |
88 | | - |
89 | | - if(!ssbKeys.verifyObj(invite.content.invite, invite_key, accept.content)) |
90 | | - throw code(new Error('did not verify invite-acceptance contents'), 'accept-invite-signature-failed') |
91 | | - |
92 | | - if(!ssbKeys.verifyObj(accept.content.id, accept)) |
93 | | - throw code(new Error('acceptance must be signed by claimed key'), 'accept-signature-failed') |
94 | | - return reveal || true |
95 | 42 … | } |
96 | 43 … | |
| 44 … | + |
| 45 … | + |
| 46 … | + |
| 47 … | + |
| 48 … | + |
| 49 … | + |
| 50 … | + |
| 51 … | +exports.init = function (sbot, config) { |
97 | 52 … | |
| 53 … | + var index = sbot._flumeUse('invites', Reduce(1, function (acc, data) { |
| 54 … | + if(!acc) acc = {invited: {}, invites:{}, accepts: {}} |
98 | 55 … | |
| 56 … | + var msg = data.value |
| 57 … | + var invite, accept |
| 58 … | + if(msg.content.type === 'invite') { |
| 59 … | + invite = msg |
| 60 … | + accept = acc.accepts[data.key] |
| 61 … | + } |
| 62 … | + else if(msg.content.type === 'invite/accept') { |
| 63 … | + accept = msg |
| 64 … | + invite = acc.invites[accept.content.receipt] |
| 65 … | + } |
| 66 … | + else if(msg.content.type === 'invite/confirm') { |
| 67 … | + accept = msg.content.embed |
| 68 … | + invite = acc.invites[accept.content.receipt] |
| 69 … | + } |
| 70 … | + if(invite && accept) { |
| 71 … | + if(invite === true) |
| 72 … | + return acc |
| 73 … | + try { |
| 74 … | + I.validateAccept(accept, invite) |
| 75 … | + |
| 76 … | + delete acc.accepts[accept.receipt] |
| 77 … | + |
| 78 … | + acc.invites[accept.receipt] = true |
| 79 … | + } catch (err) { |
| 80 … | + return acc |
| 81 … | + } |
| 82 … | + } |
| 83 … | + else if(invite) |
| 84 … | + acc.invites[data.key] = invite |
| 85 … | + else if(accept) |
| 86 … | + acc.accepts[accept.receipt] = accept |
99 | 87 … | |
| 88 … | + return acc |
100 | 89 … | |
| 90 … | + })) |
101 | 91 … | |
| 92 … | + sbot.auth.hook(function (fn, args) { |
| 93 … | + var id = args[0], cb = args[1] |
| 94 … | + index.get(function (err, v) { |
| 95 … | + if(err) return cb(err) |
| 96 … | + for(var k in v.invites) |
| 97 … | + if(v.invites[k].invite === id) |
| 98 … | + return cb(null, { |
| 99 … | + allow: ['invite.getInvite', 'invites.accept'], |
| 100 … | + deny: null |
| 101 … | + }) |
| 102 … | + }) |
| 103 … | + }) |
102 | 104 … | |
| 105 … | + |
| 106 … | + invites.getInvite = function (invite_id, cb) { |
| 107 … | + var self = this |
| 108 … | + invites.get(function (err, v) { |
| 109 … | + var invite = v.invites[invite_id] |
| 110 … | + if(err) return cb(err) |
| 111 … | + if(!invite) |
| 112 … | + cb(code( |
| 113 … | + new Error('unknown invite:'+invite_id), |
| 114 … | + 'unknown-invite' |
| 115 … | + )) |
| 116 … | + else if(invite === true) |
| 117 … | + cb(code( |
| 118 … | + new Error('invite already used:'+invite_id), |
| 119 … | + 'invite-already-used' |
| 120 … | + )) |
| 121 … | + |
| 122 … | + else if(self.id !== invite.content.invite) |
| 123 … | + cb(code( |
| 124 … | + new Error('invite did not match client id'), |
| 125 … | + 'invite-mismatch' |
| 126 … | + )) |
| 127 … | + else |
| 128 … | + cb(null, v.invites[invite_id]) |
| 129 … | + }) |
| 130 … | + } |
103 | 131 … | |
| 132 … | + var accepted = {} |
104 | 133 … | |
| 134 … | + invites.accept = function (accept, cb) { |
| 135 … | + |
| 136 … | + invites.get(function (err, v) { |
| 137 … | + var invite_id = accept.content.receipt |
| 138 … | + var invite = v.invites[invite_id] |
| 139 … | + if(invite === true || accepted[invite_id]) |
| 140 … | + return cb(code( |
| 141 … | + new Error('invite already used:'+invite_id), |
| 142 … | + 'invite-already-used' |
| 143 … | + )) |
| 144 … | + try { |
| 145 … | + I.validateAccept(accept, invite) |
| 146 … | + } catch (err) { |
| 147 … | + return cb(err) |
| 148 … | + } |
| 149 … | + |
| 150 … | + accepted[invite_id] = true |
| 151 … | + sbot.publish({type: 'invite/confirm', embed: accept}, function (err, msg) { |
| 152 … | + delete accepted[invite_id] |
| 153 … | + cb(err, msg) |
| 154 … | + }) |
| 155 … | + }) |
| 156 … | + } |
105 | 157 … | |
| 158 … | + return invites |
106 | 159 … | |
| 160 … | +} |
107 | 161 … | |