Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Groundwork to move Chelonia into a service worker #1981

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 12 additions & 12 deletions frontend/controller/actions/chatroom.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import sbp from '@sbp/sbp'
import { GIErrorUIRuntimeError, L } from '@common/common.js'
import { has, omit } from '@model/contracts/shared/giLodash.js'
import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js'
import { findKeyIdByName } from '~/shared/domains/chelonia/utils.js'
import { Secret } from '~/shared/domains/chelonia/Secret.js'
import { encryptedOutgoingData, encryptedOutgoingDataWithRawKey } from '~/shared/domains/chelonia/encryptedData.js'
// Using relative path to crypto.js instead of ~-path to workaround some esbuild bug
import { CURVE25519XSALSA20POLY1305, EDWARDS25519SHA512BATCH, deserializeKey, keyId, keygen, serializeKey } from '../../../shared/domains/chelonia/crypto.js'
Expand Down Expand Up @@ -63,12 +63,12 @@ export default (sbp('sbp/selectors/register', {
}

// Before creating the contract, put all keys into transient store
sbp('chelonia/storeSecretKeys',
await sbp('chelonia/storeSecretKeys',
// $FlowFixMe[incompatible-use]
() => [cekOpts._rawKey, cskOpts._rawKey].map(key => ({ key, transient: true }))
new Secret([cekOpts._rawKey, cskOpts._rawKey].map(key => ({ key, transient: true })))
)

const userCSKid = findKeyIdByName(rootState[userID], 'csk')
const userCSKid = await sbp('chelonia/contract/currentKeyIdByName', userID, 'csk')
if (!userCSKid) throw new Error('User CSK id not found')

const SAK = keygen(EDWARDS25519SHA512BATCH)
Expand Down Expand Up @@ -160,9 +160,9 @@ export default (sbp('sbp/selectors/register', {
})

// After the contract has been created, store pesistent keys
sbp('chelonia/storeSecretKeys',
await sbp('chelonia/storeSecretKeys',
// $FlowFixMe[incompatible-use]
() => [cekOpts._rawKey, cskOpts._rawKey].map(key => ({ key }))
new Secret([cekOpts._rawKey, cskOpts._rawKey].map(key => ({ key })))
)

return chatroom
Expand All @@ -178,11 +178,11 @@ export default (sbp('sbp/selectors/register', {
const originatingContractID = state.attributes.groupContractID ? state.attributes.groupContractID : contractID

// $FlowFixMe
return Promise.all(Object.keys(state.members).map((pContractID) => {
const CEKid = findKeyIdByName(rootState[pContractID], 'cek')
return Promise.all(Object.keys(state.members).map(async (pContractID) => {
const CEKid = await sbp('chelonia/contract/currentKeyIdByName', pContractID, 'cek')
if (!CEKid) {
console.warn(`Unable to share rotated keys for ${originatingContractID} with ${pContractID}: Missing CEK`)
return Promise.resolve()
return
}
return {
contractID,
Expand Down Expand Up @@ -219,9 +219,9 @@ export default (sbp('sbp/selectors/register', {
throw new Error(`Unable to send gi.actions/chatroom/join on ${params.contractID} because user ID contract ${userID} is missing`)
}

const CEKid = params.encryptionKeyId || sbp('chelonia/contract/currentKeyIdByName', params.contractID, 'cek')
const CEKid = params.encryptionKeyId || await sbp('chelonia/contract/currentKeyIdByName', params.contractID, 'cek')

const userCSKid = sbp('chelonia/contract/currentKeyIdByName', userID, 'csk')
const userCSKid = await sbp('chelonia/contract/currentKeyIdByName', userID, 'csk')
return await sbp('chelonia/out/atomic', {
...params,
contractName: 'gi.contracts/chatroom',
Expand Down Expand Up @@ -251,7 +251,7 @@ export default (sbp('sbp/selectors/register', {
...encryptedAction('gi.actions/chatroom/changeDescription', L('Failed to change chat channel description.')),
...encryptedAction('gi.actions/chatroom/leave', L('Failed to leave chat channel.'), async (sendMessage, params, signingKeyId) => {
const userID = params.data.memberID
const keyIds = userID && sbp('chelonia/contract/foreignKeysByContractID', params.contractID, userID)
const keyIds = userID && await sbp('chelonia/contract/foreignKeysByContractID', params.contractID, userID)

if (keyIds?.length) {
return await sbp('chelonia/out/atomic', {
Expand Down
116 changes: 40 additions & 76 deletions frontend/controller/actions/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,27 +17,26 @@ import {
STATUS_OPEN
} from '@model/contracts/shared/constants.js'
import { merge, omit, randomIntFromRange } from '@model/contracts/shared/giLodash.js'
import { addTimeToDate, dateToPeriodStamp, DAYS_MILLIS } from '@model/contracts/shared/time.js'
import { DAYS_MILLIS, addTimeToDate, dateToPeriodStamp } from '@model/contracts/shared/time.js'
import proposals from '@model/contracts/shared/voting/proposals.js'
import { VOTE_FOR } from '@model/contracts/shared/voting/rules.js'
import sbp from '@sbp/sbp'
import {
JOINED_GROUP,
LOGOUT,
OPEN_MODAL,
REPLACE_MODAL,
SWITCH_GROUP,
JOINED_GROUP
REPLACE_MODAL
} from '@utils/events.js'
import { imageUpload } from '@utils/image.js'
import { GIMessage } from '~/shared/domains/chelonia/chelonia.js'
import { findKeyIdByName } from '~/shared/domains/chelonia/utils.js'
import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js'
import { Secret } from '~/shared/domains/chelonia/Secret.js'
import { encryptedOutgoingData, encryptedOutgoingDataWithRawKey } from '~/shared/domains/chelonia/encryptedData.js'
import { CONTRACT_HAS_RECEIVED_KEYS, EVENT_HANDLED } from '~/shared/domains/chelonia/events.js'
import { CONTRACT_HAS_RECEIVED_KEYS } from '~/shared/domains/chelonia/events.js'
// Using relative path to crypto.js instead of ~-path to workaround some esbuild bug
import ALLOWED_URLS from '@view-utils/allowedUrls.js'
import type { ChelKeyRequestParams } from '~/shared/domains/chelonia/chelonia.js'
import type { Key } from '../../../shared/domains/chelonia/crypto.js'
import { CURVE25519XSALSA20POLY1305, EDWARDS25519SHA512BATCH, keygen, keyId, serializeKey } from '../../../shared/domains/chelonia/crypto.js'
import { CURVE25519XSALSA20POLY1305, EDWARDS25519SHA512BATCH, keyId, keygen, serializeKey } from '../../../shared/domains/chelonia/crypto.js'
import type { GIActionParams } from './types.js'
import { encryptedAction } from './utils.js'

Expand All @@ -55,7 +54,7 @@ export default (sbp('sbp/selectors/register', {
},
publishOptions
}) {
let finalPicture = `${window.location.origin}/assets/images/group-avatar-default.png`
let finalPicture = `${self.location.origin}/assets/images/group-avatar-default.png`

const rootState = sbp('state/vuex/state')
const userID = rootState.loggedIn.identityContractID
Expand Down Expand Up @@ -112,14 +111,14 @@ export default (sbp('sbp/selectors/register', {
}

// Before creating the contract, put all keys into transient store
sbp('chelonia/storeSecretKeys',
() => [CEK, CSK].map(key => ({ key, transient: true }))
await sbp('chelonia/storeSecretKeys',
new Secret([CEK, CSK].map(key => ({ key, transient: true })))
)

const userCSKid = findKeyIdByName(rootState[userID], 'csk')
const userCSKid = await sbp('chelonia/contract/currentKeyIdByName', userID, 'csk')
if (!userCSKid) throw new Error('User CSK id not found')

const userCEKid = findKeyIdByName(rootState[userID], 'cek')
const userCEKid = await sbp('chelonia/contract/currentKeyIdByName', userID, 'cek')
if (!userCEKid) throw new Error('User CEK id not found')

const message = await sbp('chelonia/out/registerContract', {
Expand Down Expand Up @@ -238,30 +237,27 @@ export default (sbp('sbp/selectors/register', {
const contractID = message.contractID()

// After the contract has been created, store pesistent keys
sbp('chelonia/storeSecretKeys',
() => [CEK, CSK, inviteKey].map(key => ({ key }))
await sbp('chelonia/storeSecretKeys',
new Secret([CEK, CSK, inviteKey].map(key => ({ key })))
)

await sbp('chelonia/queueInvocation', contractID, ['gi.actions/identity/joinGroup', {
contractID: userID,
data: {
groupContractID: contractID,
inviteSecret: serializeKey(CSK, true),
creatorID: true
}
}])
await sbp('chelonia/contract/wait', contractID).then(() => {
return sbp('gi.actions/identity/joinGroup', {
contractID: userID,
data: {
groupContractID: contractID,
inviteSecret: serializeKey(CSK, true),
creatorID: true
}
})
})

return message
return message.contractID()
} catch (e) {
console.error('gi.actions/group/create failed!', e)
throw new GIErrorUIRuntimeError(L('Failed to create the group: {reportError}', LError(e)))
}
},
'gi.actions/group/createAndSwitch': async function (params: GIActionParams) {
const message = await sbp('gi.actions/group/create', params)
sbp('gi.actions/group/switch', message.contractID())
return message
},
// The 'gi.actions/group/join' selector handles joining a group. It can be
// called from a variety of places: when accepting an invite, when logging
// in, and asynchronously with an event handler defined in this function.
Expand Down Expand Up @@ -314,23 +310,23 @@ export default (sbp('sbp/selectors/register', {
// automatically, even if we have a valid invitation secret and are
// technically able to. However, in the previous situation we *should*
// attempt to rejoin if the action was user-initiated.
const hasKeyShareBeenRespondedBy = sbp('chelonia/contract/hasKeyShareBeenRespondedBy', userID, params.contractID, params.reference)
const hasKeyShareBeenRespondedBy = await sbp('chelonia/contract/hasKeyShareBeenRespondedBy', userID, params.contractID, params.reference)

const state = rootState[params.contractID]

// Do we have the secret keys with the right permissions to be able to
// perform all operations in the group? If we haven't, we are not
// able to participate in the group yet and may need to send a key
// request.
const hasSecretKeys = sbp('chelonia/contract/receivedKeysToPerformOperation', userID, state, '*')
const hasSecretKeys = await sbp('chelonia/contract/receivedKeysToPerformOperation', userID, params.contractID, '*')

// Do we need to send a key request?
// If we don't have the group contract in our state and
// params.originatingContractID is set, it means that we're joining
// through an invite link, and we must send a key request to complete
// the joining process.
const sendKeyRequest = (!hasKeyShareBeenRespondedBy && !hasSecretKeys && params.originatingContractID)
const pendingKeyShares = sbp('chelonia/contract/waitingForKeyShareTo', state, userID, params.reference)
const pendingKeyShares = await sbp('chelonia/contract/waitingForKeyShareTo', params.contractID, userID, params.reference)

// If we are expecting to receive keys, set up an event listener
// We are expecting to receive keys if:
Expand Down Expand Up @@ -385,7 +381,7 @@ export default (sbp('sbp/selectors/register', {
// (originating) contract.
await sbp('chelonia/out/keyRequest', {
...omit(params, ['options']),
innerEncryptionKeyId: sbp('chelonia/contract/currentKeyIdByName', params.contractID, 'cek'),
innerEncryptionKeyId: await sbp('chelonia/contract/currentKeyIdByName', params.contractID, 'cek'),
permissions: [GIMessage.OP_ACTION_ENCRYPTED],
allowedActions: ['gi.contracts/identity/joinDirectMessage'],
reference: params.reference,
Expand Down Expand Up @@ -416,10 +412,10 @@ export default (sbp('sbp/selectors/register', {
// synchronously, before any await calls.
// If reading after an asynchronous operation, we might get inconsistent
// values, as new operations could have been received on the contract
const CEKid = sbp('chelonia/contract/currentKeyIdByName', params.contractID, 'cek')
const PEKid = sbp('chelonia/contract/currentKeyIdByName', userID, 'pek')
const CSKid = sbp('chelonia/contract/currentKeyIdByName', params.contractID, 'csk')
const userCSKid = sbp('chelonia/contract/currentKeyIdByName', userID, 'csk')
const CEKid = await sbp('chelonia/contract/currentKeyIdByName', params.contractID, 'cek')
const PEKid = await sbp('chelonia/contract/currentKeyIdByName', userID, 'pek')
const CSKid = await sbp('chelonia/contract/currentKeyIdByName', params.contractID, 'csk')
const userCSKid = await sbp('chelonia/contract/currentKeyIdByName', userID, 'csk')
const userCSKdata = rootState[userID]._vm.authorizedKeys[userCSKid].data

try {
Expand Down Expand Up @@ -487,11 +483,6 @@ export default (sbp('sbp/selectors/register', {
await sbp('chelonia/contract/release', params.contractID, { ephemeral: true })
}
},
'gi.actions/group/joinAndSwitch': async function (params: $Exact<ChelKeyRequestParams>) {
await sbp('gi.actions/group/join', params)
// after joining, we can set the current group
return sbp('gi.actions/group/switch', params.contractID)
},
'gi.actions/group/joinWithInviteSecret': async function (groupId: string, secret: string) {
const identityContractID = sbp('state/vuex/state').loggedIn.identityContractID

Expand Down Expand Up @@ -523,17 +514,11 @@ export default (sbp('sbp/selectors/register', {
groupContractID: groupId,
inviteSecret: secret
}
}).then(() => {
return sbp('gi.actions/group/switch', groupId)
})
} finally {
await sbp('chelonia/contract/release', groupId, { ephemeral: true })
}
},
'gi.actions/group/switch': function (groupId) {
sbp('state/vuex/commit', 'setCurrentGroupId', groupId)
sbp('okTurtles.events/emit', SWITCH_GROUP)
},
'gi.actions/group/shareNewKeys': (contractID: string, newKeys) => {
const rootState = sbp('state/vuex/state')
const state = rootState[contractID]
Expand All @@ -542,8 +527,8 @@ export default (sbp('sbp/selectors/register', {
return Promise.all(
Object.entries(state.profiles)
.filter(([_, p]) => (p: any).status === PROFILE_STATUS.ACTIVE)
.map(([pContractID]) => {
const CEKid = sbp('chelonia/contract/currentKeyIdByName', rootState[pContractID], 'cek')
.map(async ([pContractID]) => {
const CEKid = await sbp('chelonia/contract/currentKeyIdByName', rootState[pContractID], 'cek')
if (!CEKid) {
console.warn(`Unable to share rotated keys for ${contractID} with ${pContractID}: Missing CEK`)
return Promise.resolve()
Expand Down Expand Up @@ -573,14 +558,14 @@ export default (sbp('sbp/selectors/register', {
}
}

const cskId = sbp('chelonia/contract/currentKeyIdByName', contractState, 'csk')
const cskId = await sbp('chelonia/contract/currentKeyIdByName', contractState, 'csk')
const csk = {
id: cskId,
foreignKey: `sp:${encodeURIComponent(params.contractID)}?keyName=${encodeURIComponent('csk')}`,
data: contractState._vm.authorizedKeys[cskId].data
}

const cekId = sbp('chelonia/contract/currentKeyIdByName', contractState, 'cek')
const cekId = await sbp('chelonia/contract/currentKeyIdByName', contractState, 'cek')
const cek = {
id: cekId,
foreignKey: `sp:${encodeURIComponent(params.contractID)}?keyName=${encodeURIComponent('cek')}`,
Expand Down Expand Up @@ -642,14 +627,9 @@ export default (sbp('sbp/selectors/register', {
}),
...encryptedAction('gi.actions/group/joinChatRoom', L('Failed to join chat channel.'), async function (sendMessage, params) {
const rootState = sbp('state/vuex/state')
const rootGetters = sbp('state/vuex/getters')
const me = rootState.loggedIn.identityContractID
const memberID = params.data.memberID || me

if (!rootGetters.isJoinedChatRoom(params.data.chatRoomID) && memberID !== me) {
throw new GIErrorUIRuntimeError(L('Only channel members can invite others to join.'))
}

// If we are inviting someone else to join, we need to share the chatroom's keys
// with them so that they are able to read messages and participate
if (memberID !== me && rootState[params.data.chatRoomID].attributes.privacyLevel === CHATROOM_PRIVACY_LEVEL.PRIVATE) {
Expand Down Expand Up @@ -680,27 +660,11 @@ export default (sbp('sbp/selectors/register', {
...omit(params, ['options', 'data', 'hooks']),
data: { chatRoomID },
hooks: {
// joinChatRoom sideEffect will trigger a call to 'gi.actions/chatroom/join', we want
// to wait for that action to be received and processed, and then switch the UI to the
// new chatroom. We do this here instead of in the sideEffect for chatroom/join to
// avoid causing the UI to change in other open tabs/windows, as per bug:
// https://github.com/okTurtles/group-income/issues/1960
onprocessed: (msg) => {
const fnEventHandled = (cID, message) => {
if (cID === chatRoomID) {
if (sbp('state/vuex/getters').isJoinedChatRoom(chatRoomID)) {
sbp('state/vuex/commit', 'setCurrentChatRoomId', { chatRoomID, groupID: msg.contractID() })
sbp('okTurtles.events/off', EVENT_HANDLED, fnEventHandled)
}
}
}
sbp('okTurtles.events/on', EVENT_HANDLED, fnEventHandled)
},
Comment on lines -684 to -699
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this removed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because that deals with the Vuex state and doesn't belong into actions.

postpublish: params.hooks?.postpublish
}
})

return message
return chatRoomID
},
...encryptedAction('gi.actions/group/renameChatRoom', L('Failed to rename chat channel.'), async function (sendMessage, params) {
await sbp('gi.actions/chatroom/rename', {
Expand Down Expand Up @@ -961,8 +925,8 @@ export default (sbp('sbp/selectors/register', {
const current = await sbp('chelonia/kv/get', contractID, 'lastLoggedIn')?.data || {}
current[userID] = now
await sbp('chelonia/kv/set', contractID, 'lastLoggedIn', current, {
encryptionKeyId: sbp('chelonia/contract/currentKeyIdByName', contractID, 'cek'),
signingKeyId: sbp('chelonia/contract/currentKeyIdByName', contractID, 'csk')
encryptionKeyId: await sbp('chelonia/contract/currentKeyIdByName', contractID, 'cek'),
signingKeyId: await sbp('chelonia/contract/currentKeyIdByName', contractID, 'csk')
})
})
} catch (e) {
Expand Down
Loading
Loading