Skip to content

Commit 838794c

Browse files
committed
Merge branch 'master' into 1927-design-implement-contract-deletion-op_contract_delete
2 parents e8a30e3 + 63bf41c commit 838794c

27 files changed

+483
-162
lines changed

backend/push.js

+22-12
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const deleteSubscriptionFromIndex = async (subcriptionId: string) => {
3030
}
3131

3232
const saveSubscription = (server, subscriptionId) => {
33-
sbp('chelonia/db/set', `_private_webpush_${subscriptionId}`, JSON.stringify({
33+
return sbp('chelonia/db/set', `_private_webpush_${subscriptionId}`, JSON.stringify({
3434
subscription: server.pushSubscriptions[subscriptionId],
3535
channelIDs: [...server.pushSubscriptions[subscriptionId].subscriptions]
3636
})).catch(e => {
@@ -163,22 +163,25 @@ export const subscriptionInfoWrapper = (subcriptionId: string, subscriptionInfo:
163163
return subscriptionInfo
164164
}
165165

166-
const removeSubscription = (server, subscriptionId) => {
167-
const subscription = server.pushSubscriptions[subscriptionId]
168-
delete server.pushSubscriptions[subscriptionId]
169-
if (server.subscribersByChannelID) {
170-
subscription.subscriptions.forEach((channelID) => {
171-
server.subscribersByChannelID[channelID].delete(subscription)
172-
})
166+
const removeSubscription = async (server, subscriptionId) => {
167+
try {
168+
const subscription = server.pushSubscriptions[subscriptionId]
169+
delete server.pushSubscriptions[subscriptionId]
170+
if (server.subscribersByChannelID) {
171+
subscription.subscriptions.forEach((channelID) => {
172+
server.subscribersByChannelID[channelID].delete(subscription)
173+
})
174+
}
175+
await deleteSubscriptionFromIndex(subscriptionId)
176+
await sbp('chelonia/db/delete', `_private_webpush_${subscriptionId}`)
177+
} catch (e) {
178+
console.error(e, 'Error removing subscription', subscriptionId)
173179
}
174-
deleteSubscriptionFromIndex(subscriptionId).then(() => {
175-
return sbp('chelonia/db/delete', `_private_webpush_${subscriptionId}`)
176-
}).catch((e) => console.error(e, 'Error removing subscription', subscriptionId))
177180
}
178181

179182
const deleteClient = (subscriptionId) => {
180183
const server = sbp('okTurtles.data/get', PUBSUB_INSTANCE)
181-
removeSubscription(server, subscriptionId)
184+
return removeSubscription(server, subscriptionId)
182185
}
183186

184187
// Web push subscriptions (that contain a body) are mandatorily encrypted. The
@@ -239,6 +242,7 @@ export const postEvent = async (subscription: Object, event: ?string): Promise<v
239242
})
240243

241244
if (!req.ok) {
245+
console.warn('Error sending push notification', subscription.id, req.status)
242246
// If the response was 401 (Unauthorized), 404 (Not found) or 410 (Gone),
243247
// it likely means that the subscription no longer exists.
244248
if ([401, 404, 410].includes(req.status)) {
@@ -268,11 +272,17 @@ export const pushServerActionhandlers: any = {
268272
const subscriptionId = await getSubscriptionId(subscription)
269273

270274
if (!server.pushSubscriptions[subscriptionId]) {
275+
console.debug(`saving new push subscription '${subscriptionId}':`, subscription)
271276
// If this is a new subscription, we call `subscriptionInfoWrapper` and
272277
// store it in memory.
273278
server.pushSubscriptions[subscriptionId] = subscriptionInfoWrapper(subscriptionId, subscription)
274279
addSubscriptionToIndex(subscriptionId).then(() => {
275280
return sbp('chelonia/db/set', `_private_webpush_${subscriptionId}`, JSON.stringify({ subscription: subscription, channelIDs: [] }))
281+
.catch(async e => {
282+
console.error(e, 'removing subscription from index because of error saving subscription', subscriptionId)
283+
await deleteSubscriptionFromIndex(subscriptionId)
284+
throw e
285+
})
276286
}).catch((e) => console.error(e, 'Error saving subscription', subscriptionId))
277287
// Send an initial push notification to verify that the endpoint works
278288
// This is mostly for testing to be able to auto-remove invalid or expired

backend/server.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -325,8 +325,9 @@ sbp('sbp/selectors/register', {
325325
})
326326

327327
if (process.env.NODE_ENV === 'development' && !process.env.CI) {
328-
hapi.events.on('response', (request, event, tags) => {
329-
console.debug(chalk`{grey ${request.info.remoteAddress}: ${request.method.toUpperCase()} ${request.path} --> ${request.response.statusCode}}`)
328+
hapi.events.on('response', (req, event, tags) => {
329+
const ip = req.headers['x-real-ip'] || req.info.remoteAddress
330+
console.debug(chalk`{grey ${ip}: ${req.method} ${req.path} --> ${req.response.statusCode}}`)
330331
})
331332
}
332333

@@ -378,10 +379,9 @@ sbp('okTurtles.data/set', PUBSUB_INSTANCE, createServer(hapi.listener, {
378379
try {
379380
await handler.call(socket, payload)
380381
} catch (error) {
381-
socket.send(createPushErrorResponse({
382-
actionType: action,
383-
message: error?.message || `push server failed to perform [${action}] action`
384-
}))
382+
const message = error?.message || `push server failed to perform [${action}] action`
383+
console.warn(`Handler '${REQUEST_TYPE.PUSH_ACTION}' failed: ${message}`)
384+
socket.send(createPushErrorResponse({ actionType: action, message }))
385385
}
386386
} else {
387387
socket.send(createPushErrorResponse({ message: `No handler for the '${action}' action` }))

backend/vapid.js

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const vapid = { VAPID_EMAIL: process.env.VAPID_EMAIL || 'mailto:[email protected]
1515
export const initVapid = async () => {
1616
const vapidKeyPair = await sbp('chelonia/db/get', '_private_immutable_vapid_key').then(async (vapidKeyPair: string): Promise<[Object, string]> => {
1717
if (!vapidKeyPair) {
18+
console.info('Generating new VAPID keypair...')
1819
// Generate a new ECDSA key pair
1920
const keyPair = await crypto.subtle.generateKey(
2021
{
@@ -35,6 +36,7 @@ export const initVapid = async () => {
3536
])
3637

3738
return sbp('chelonia/db/set', '_private_immutable_vapid_key', JSON.stringify(serializedKeyPair)).then(() => {
39+
console.info('Successfully saved newly generated VAPID keys')
3840
return [keyPair.privateKey, serializedKeyPair[1]]
3941
})
4042
}

frontend/controller/actions/chatroom.js

+21-15
Original file line numberDiff line numberDiff line change
@@ -215,8 +215,9 @@ export default (sbp('sbp/selectors/register', {
215215
await sbp('chelonia/contract/release', userID, { ephemeral: true })
216216
}
217217
},
218-
'gi.actions/chatroom/shareNewKeys': (contractID: string, newKeys) => {
218+
'gi.actions/chatroom/shareNewKeys': async (contractID: string, newKeys) => {
219219
const state = sbp('chelonia/contract/state', contractID)
220+
const mainCEKid = await sbp('chelonia/contract/currentKeyIdByName', state, 'cek')
220221

221222
const originatingContractID = state.attributes.groupContractID ? state.attributes.groupContractID : contractID
222223

@@ -227,20 +228,25 @@ export default (sbp('sbp/selectors/register', {
227228
console.warn(`Unable to share rotated keys for ${originatingContractID} with ${pContractID}: Missing CEK`)
228229
return
229230
}
230-
return {
231-
contractID,
232-
foreignContractID: pContractID,
233-
// $FlowFixMe
234-
keys: Object.values(newKeys).map(([, newKey, newId]: [any, Key, string]) => ({
235-
id: newId,
236-
meta: {
237-
private: {
238-
content: encryptedOutgoingData(pContractID, CEKid, serializeKey(newKey, true))
239-
}
240-
}
241-
}))
242-
}
243-
}))
231+
return [
232+
'chelonia/out/keyShare',
233+
{
234+
data: encryptedOutgoingData(contractID, mainCEKid, {
235+
contractID,
236+
foreignContractID: pContractID,
237+
// $FlowFixMe
238+
keys: Object.values(newKeys).map(([, newKey, newId]: [any, Key, string]) => ({
239+
id: newId,
240+
meta: {
241+
private: {
242+
content: encryptedOutgoingData(pContractID, CEKid, serializeKey(newKey, true))
243+
}
244+
}
245+
}))
246+
})
247+
}
248+
]
249+
})).then((keys) => [keys.filter(Boolean)])
244250
},
245251
'gi.actions/chatroom/_ondeleted': async (contractID: string, state: Object) => {
246252
const rootGetters = sbp('state/vuex/getters')

frontend/controller/actions/group.js

+21-16
Original file line numberDiff line numberDiff line change
@@ -626,9 +626,10 @@ export default (sbp('sbp/selectors/register', {
626626
}
627627
})
628628
},
629-
'gi.actions/group/shareNewKeys': (contractID: string, newKeys) => {
629+
'gi.actions/group/shareNewKeys': async (contractID: string, newKeys) => {
630630
const rootState = sbp('chelonia/rootState')
631631
const state = rootState[contractID]
632+
const mainCEKid = await sbp('chelonia/contract/currentKeyIdByName', state, 'cek')
632633

633634
// $FlowFixMe
634635
return Promise.all(
@@ -638,22 +639,26 @@ export default (sbp('sbp/selectors/register', {
638639
const CEKid = await sbp('chelonia/contract/currentKeyIdByName', rootState[pContractID], 'cek')
639640
if (!CEKid) {
640641
console.warn(`Unable to share rotated keys for ${contractID} with ${pContractID}: Missing CEK`)
641-
return Promise.resolve()
642+
return
642643
}
643-
return {
644-
contractID,
645-
foreignContractID: pContractID,
646-
// $FlowFixMe
647-
keys: Object.values(newKeys).map(([, newKey, newId]: [any, Key, string]) => ({
648-
id: newId,
649-
meta: {
650-
private: {
651-
content: encryptedOutgoingData(pContractID, CEKid, serializeKey(newKey, true))
652-
}
653-
}
654-
}))
655-
}
656-
}))
644+
return [
645+
'chelonia/out/keyShare',
646+
{
647+
data: encryptedOutgoingData(contractID, mainCEKid, {
648+
contractID,
649+
foreignContractID: pContractID,
650+
// $FlowFixMe
651+
keys: Object.values(newKeys).map(([, newKey, newId]: [any, Key, string]) => ({
652+
id: newId,
653+
meta: {
654+
private: {
655+
content: encryptedOutgoingData(pContractID, CEKid, serializeKey(newKey, true))
656+
}
657+
}
658+
}))
659+
})
660+
}]
661+
})).then((keys) => [keys.filter(Boolean)])
657662
},
658663
...encryptedAction('gi.actions/group/addChatRoom', L('Failed to add chat channel'), async function (sendMessage, params) {
659664
const rootState = sbp('chelonia/rootState')

frontend/controller/actions/identity.js

+14-1
Original file line numberDiff line numberDiff line change
@@ -667,7 +667,20 @@ export default (sbp('sbp/selectors/register', {
667667
// share along with OP_KEY_UPDATE. In this case, we're sharing all keys
668668
// to their respective contracts and there are no keys to include in
669669
// the same event as OP_KEY_UPDATE. Therefore, we return undefined
670-
return undefined
670+
if (!newKeys.pek) return undefined
671+
return [
672+
undefined, // Nothing before OP_KEY_UPDATE
673+
[
674+
// Re-encrypt attributes with the new PEK
675+
await sbp('gi.actions/identity/setAttributes', {
676+
contractID,
677+
data: state.attributes,
678+
encryptionKey: newKeys.pek[1],
679+
encryptionKeyId: newKeys.pek[2],
680+
returnInvocation: true
681+
})
682+
]
683+
]
671684
},
672685
...encryptedAction('gi.actions/identity/setAttributes', L('Failed to set profile attributes.'), undefined, 'pek'),
673686
...encryptedAction('gi.actions/identity/updateSettings', L('Failed to update profile settings.')),

frontend/controller/actions/index.js

+13-10
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,12 @@ sbp('sbp/selectors/register', {
9393
contractID: string,
9494
contractName: string,
9595
keysToRotate: string[] | '*' | 'pending',
96-
shareNewKeysSelector?: string
96+
// Additional operations to be done along with key roation.
97+
// If any, it should return an array of arrays of invocations that can be
98+
// passed to `'chelonia/out/atomic'`. The first element of the array are
99+
// operations to be done before `keyUpdate` and the second element are
100+
// to be added after `keyUpdate`.
101+
addtionalOperationsSelector?: string
97102
) => {
98103
const state = sbp('chelonia/contract/state', contractID)
99104

@@ -167,12 +172,9 @@ sbp('sbp/selectors/register', {
167172
throw new Error('No suitable signing key found')
168173
}
169174

170-
// TODO: GI-specific
171-
const CEKid = findKeyIdByName(state, 'cek')
172-
if (!CEKid) return
173-
174-
// Share new keys with other contracts
175-
const keyShares = shareNewKeysSelector ? await sbp(shareNewKeysSelector, contractID, newKeys) : undefined
175+
// Additional operations to be done along with key roation.
176+
// E.g., share new keys with other contracts
177+
const additionalOperations = addtionalOperationsSelector ? await sbp(addtionalOperationsSelector, contractID, newKeys) : undefined
176178

177179
const preSendCheck = (msg, state) => {
178180
const updatedKeysRemaining = updatedKeys.filter((key) => {
@@ -185,14 +187,15 @@ sbp('sbp/selectors/register', {
185187
return true
186188
}
187189

188-
if (Array.isArray(keyShares) && keyShares.length > 0) {
190+
if (Array.isArray(additionalOperations) && additionalOperations.length > 0) {
189191
// Issue OP_ATOMIC
190192
await sbp('chelonia/out/atomic', {
191193
contractID,
192194
contractName,
193195
data: [
194-
...keyShares.map((data) => ['chelonia/out/keyShare', { data: encryptedOutgoingData(contractID, CEKid, data) }]),
195-
['chelonia/out/keyUpdate', { data: updatedKeys }]
196+
...(additionalOperations[0] ?? []),
197+
['chelonia/out/keyUpdate', { data: updatedKeys }],
198+
...(additionalOperations[1] ?? [])
196199
],
197200
signingKeyId,
198201
hooks: {

frontend/controller/app/group.js

+50-2
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,62 @@
33
import { L } from '@common/common.js'
44
import {
55
INVITE_INITIAL_CREATOR,
6-
MAX_GROUP_MEMBER_COUNT
6+
MAX_GROUP_MEMBER_COUNT,
7+
PROFILE_STATUS
78
} from '@model/contracts/shared/constants.js'
89
import sbp from '@sbp/sbp'
9-
import { JOINED_GROUP, LEFT_GROUP, NEW_LAST_LOGGED_IN, OPEN_MODAL, REPLACE_MODAL, SWITCH_GROUP } from '@utils/events.js'
10+
import { ERROR_GROUP_GENERAL_CHATROOM_DOES_NOT_EXIST, ERROR_JOINING_CHATROOM, JOINED_GROUP, LEFT_GROUP, NEW_LAST_LOGGED_IN, OPEN_MODAL, REPLACE_MODAL, SWITCH_GROUP } from '@utils/events.js'
1011
import ALLOWED_URLS from '@view-utils/allowedUrls.js'
1112
import type { ChelKeyRequestParams } from '~/shared/domains/chelonia/chelonia.js'
1213
import type { GIActionParams } from '../actions/types.js'
1314

15+
sbp('okTurtles.events/on', ERROR_GROUP_GENERAL_CHATROOM_DOES_NOT_EXIST, ({ identityContractID, groupContractID }) => {
16+
const rootState = sbp('state/vuex/state')
17+
if (rootState.loggedIn?.identityContractID !== identityContractID) return
18+
if (!rootState[groupContractID]) return
19+
20+
sbp('chelonia/contract/wait', groupContractID).then(() => {
21+
const ourGroups = sbp('state/vuex/getters').ourGroups
22+
if (!ourGroups.includes(groupContractID)) return
23+
24+
const rootState = sbp('state/vuex/state')
25+
if (!rootState[groupContractID].generalChatRoomId) {
26+
sbp('gi.ui/prompt', {
27+
heading: L('Error joining the #general chatroom'),
28+
question: L('There was an error joining the #general chatroom because it doesn\'t exist'),
29+
primaryButton: L('Close')
30+
})
31+
}
32+
})
33+
})
34+
35+
sbp('okTurtles.events/on', ERROR_JOINING_CHATROOM, ({ identityContractID, groupContractID, chatRoomID }) => {
36+
const rootState = sbp('state/vuex/state')
37+
if (rootState.loggedIn?.identityContractID !== identityContractID) return
38+
if (!rootState[groupContractID]) return
39+
40+
sbp('chelonia/contract/wait', groupContractID).then(() => {
41+
const ourGroups = sbp('state/vuex/getters').ourGroups
42+
if (!ourGroups.includes(groupContractID)) return
43+
44+
const rootState = sbp('state/vuex/state')
45+
if (
46+
rootState[groupContractID].chatRooms[chatRoomID]?.members[identityContractID]?.status === PROFILE_STATUS.ACTIVE &&
47+
!rootState[chatRoomID]?.members[identityContractID]
48+
) {
49+
sbp('gi.ui/prompt', {
50+
heading: L('Error joining chatroom'),
51+
question: L('There was an error joining the {chatRoomName} chatroom', {
52+
chatRoomName: rootState[groupContractID].chatRooms[chatRoomID]?.name
53+
? `#${rootState[groupContractID].chatRooms[chatRoomID].name}`
54+
: L('(unknown)')
55+
}),
56+
primaryButton: L('Close')
57+
})
58+
}
59+
})
60+
})
61+
1462
// handle incoming group-related events that are sent from the service worker
1563
sbp('okTurtles.events/on', JOINED_GROUP, ({ identityContractID, groupContractID }) => {
1664
const rootState = sbp('state/vuex/state')

0 commit comments

Comments
 (0)