Skip to content

Commit

Permalink
Merge pull request #1785 from okTurtles/e2e-protocol-ricardo
Browse files Browse the repository at this point in the history
E2e protocol ricardo
  • Loading branch information
taoeffect authored Nov 17, 2023
2 parents 71c7a71 + baacd80 commit 9458715
Show file tree
Hide file tree
Showing 25 changed files with 993 additions and 673 deletions.
27 changes: 6 additions & 21 deletions frontend/common/errors.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,10 @@
'use strict'

export class GIErrorIgnoreAndBan extends Error {
// ugly boilerplate because JavaScript is stupid
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Custom_Error_Types
constructor (...params: any[]) {
super(...params)
this.name = 'GIErrorIgnoreAndBan'
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor)
}
}
}
import { ChelErrorGenerator } from '~/shared/domains/chelonia/errors.js'

export const GIErrorIgnoreAndBan: typeof Error = ChelErrorGenerator('GIErrorIgnoreAndBan')

// Used to throw human readable errors on UI.
export class GIErrorUIRuntimeError extends Error {
constructor (...params: any[]) {
super(...params)
// this.name = this.constructor.name
this.name = 'GIErrorUIRuntimeError' // string literal so minifier doesn't overwrite
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor)
}
}
}
export const GIErrorUIRuntimeError: typeof Error = ChelErrorGenerator('GIErrorUIRuntimeError')

export const GIErrorMissingSigningKeyError: typeof Error = ChelErrorGenerator('GIErrorMissingSigningKeyError')
19 changes: 13 additions & 6 deletions frontend/controller/actions/chatroom.js
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export default (sbp('sbp/selectors/register', {
id: newId,
meta: {
private: {
content: encryptedOutgoingData(rootState[pContractID], CEKid, serializeKey(newKey, true))
content: encryptedOutgoingData(pContractID, CEKid, serializeKey(newKey, true))
}
}
}))
Expand All @@ -228,19 +228,26 @@ 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`)
}

await sbp('chelonia/contract/sync', params.contractID)
const isCurrentUserJoining = rootState.loggedIn.identityContractID === userID

if (isCurrentUserJoining) {
// Cancel remove when sending this (join) action. This is because if we're
// trying to join a chatroom that we've previously left, it'll be removed
// by its side-effects. Calling 'chelonia/contract/cancelRemove' clears
// the pendingRemove flag in the contract, preventing it from being
// removed (which is the intent here, as we're re-joining)
sbp('chelonia/contract/cancelRemove', params.contractID)
}

const CEKid = sbp('chelonia/contract/currentKeyIdByName', params.contractID, 'cek')
const userCSKid = sbp('chelonia/contract/currentKeyIdByName', userID, 'csk')

const state = rootState[params.contractID]

// Add the user's CSK to the contract
await sbp('chelonia/out/keyAdd', {
contractID: params.contractID,
contractName: 'gi.contracts/chatroom',
// TODO: Find a way to have this wrapping be done by Chelonia directly
data: [encryptedOutgoingData(state, CEKid, {
data: [encryptedOutgoingData(params.contractID, CEKid, {
foreignKey: `sp:${encodeURIComponent(userID)}?keyName=${encodeURIComponent('csk')}`,
id: userCSKid,
data: rootState[userID]._vm.authorizedKeys[userCSKid].data,
Expand All @@ -253,7 +260,7 @@ export default (sbp('sbp/selectors/register', {
signingKeyId
})

return sendMessage(params)
return await sendMessage(params)
}),
...encryptedAction('gi.actions/chatroom/rename', L('Failed to rename chat channel.')),
...encryptedAction('gi.actions/chatroom/changeDescription', L('Failed to change chat channel description.')),
Expand Down
118 changes: 31 additions & 87 deletions frontend/controller/actions/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
INVITE_INITIAL_CREATOR,
MAX_GROUP_MEMBER_COUNT,
MESSAGE_TYPES,
PROFILE_STATUS,
PROPOSAL_GENERIC,
PROPOSAL_GROUP_SETTING_CHANGE,
PROPOSAL_INVITE_MEMBER,
Expand Down Expand Up @@ -294,7 +295,7 @@ export default (sbp('sbp/selectors/register', {
// secret keys to be shared with us, (b) ready to call the inviteAccept
// action if we haven't done so yet (because we were previously waiting for
// the keys), or (c) already a member and ready to interact with the group.
'gi.actions/group/join': async function (params: $Exact<ChelKeyRequestParams> & { options?: { skipUsableKeysCheck?: boolean; skipInviteAccept?: boolean } }) {
'gi.actions/group/join': async function (params: $Exact<ChelKeyRequestParams> & { options?: { skipUsableKeysCheck?: boolean; } }) {
sbp('okTurtles.data/set', 'JOINING_GROUP-' + params.contractID, true)
try {
const rootState = sbp('state/vuex/state')
Expand Down Expand Up @@ -322,12 +323,13 @@ export default (sbp('sbp/selectors/register', {
// through an invite link, and we must send a key request to complete
// the joining process.
const sendKeyRequest = (!hasSecretKeys && params.originatingContractID)
const isWaitingForKeyShare = sbp('chelonia/contract/isWaitingForKeyShare', state)

// If we are expecting to receive keys, set up an event listener
// We are expecting to receive keys if:
// (a) we are about to send a key request; or
// (b) we have already sent a key request (!!pendingKeyRequests?.length)
if (sendKeyRequest || sbp('chelonia/contract/isWaitingForKeyShare', state)) {
if (sendKeyRequest || isWaitingForKeyShare) {
console.log('@@@@@@@@ AT join[sendKeyRequest] for ' + params.contractID)

// Event handler for continuing the join process if the keys are
Expand Down Expand Up @@ -377,15 +379,13 @@ export default (sbp('sbp/selectors/register', {
// current group.
// This block must be run after having received the group's secret keys
// (i.e., the CSK and the CEK) that were requested earlier.
} else if (hasSecretKeys && !sbp('chelonia/contract/isWaitingForKeyShare', state)) {
console.log('@@@@@@@@ AT join[firstTimeJoin] for ' + params.contractID)
} else if (hasSecretKeys && !isWaitingForKeyShare) {
console.log('@@@@@@@@ AT join[firstTimeJoin] for ' + params.contractID, 'prfileStatus', state.profiles?.[username]?.status, JSON.parse(JSON.stringify(state.profiles)))

// We're joining for the first time
// In this case, we share our profile key with the group, call the
// inviteAccept action and join the General chatroom
if (!state.profiles?.[username] || state.profiles[username].departedDate) {
const generalChatRoomId = rootState[params.contractID].generalChatRoomId

if (state.profiles?.[username]?.status !== PROFILE_STATUS.ACTIVE) {
const CEKid = sbp('chelonia/contract/currentKeyIdByName', params.contractID, 'cek')

// Share our PEK with the group so that group members can see
Expand All @@ -401,12 +401,11 @@ export default (sbp('sbp/selectors/register', {

const CSKid = sbp('chelonia/contract/currentKeyIdByName', params.contractID, 'csk')
const userCSKid = sbp('chelonia/contract/currentKeyIdByName', userID, 'csk')
const userCEKid = sbp('chelonia/contract/currentKeyIdByName', userID, 'cek')

await sbp('chelonia/out/keyAdd', {
contractID: params.contractID,
contractName: 'gi.contracts/group',
data: [encryptedOutgoingData(state, CEKid, {
data: [encryptedOutgoingData(params.contractID, CEKid, {
foreignKey: `sp:${encodeURIComponent(userID)}?keyName=${encodeURIComponent('csk')}`,
id: userCSKid,
data: rootState[userID]._vm.authorizedKeys[userCSKid].data,
Expand All @@ -425,49 +424,13 @@ export default (sbp('sbp/selectors/register', {
...omit(params, ['options', 'action', 'hooks', 'signingKeyId']),
hooks: {
prepublish: params.hooks?.prepublish,
postpublish: null
postpublish: null,
preSendCheck: (_, state) => {
return state?.profiles?.[username]?.status !== PROFILE_STATUS.ACTIVE
}
}
})

// Add the group's CSK to our identity contract so that we can receive
// key rotation updates and DMs.
await sbp('chelonia/out/keyAdd', {
contractID: userID,
contractName: 'gi.contracts/identity',
data: [encryptedOutgoingData(rootState[userID], userCEKid, {
foreignKey: `sp:${encodeURIComponent(params.contractID)}?keyName=${encodeURIComponent('csk')}`,
id: CSKid,
data: state._vm.authorizedKeys[CSKid].data,
// The OP_ACTION_ENCRYPTED is necessary to let the DM counterparty
// that a chatroom has just been created
permissions: [GIMessage.OP_ACTION_ENCRYPTED + '#inner'],
allowedActions: ['gi.contracts/identity/joinDirectMessage#inner'],
purpose: ['sig'],
ringLevel: Number.MAX_SAFE_INTEGER,
name: `${params.contractID}/${CSKid}`
})],
signingKeyId: sbp('chelonia/contract/suitableSigningKey', userID, [GIMessage.OP_KEY_ADD], ['sig'])
})

if (generalChatRoomId) {
// Join the general chatroom
await sbp('gi.actions/group/joinChatRoom', {
...omit(params, ['options', 'data', 'hooks', 'signingKeyId']),
data: {
chatRoomID: generalChatRoomId
},
hooks: {
prepublish: null,
postpublish: params.hooks?.postpublish
}
})
} else {
// setTimeout to avoid blocking the main thread
setTimeout(() => {
alert(L("Couldn't join the #{chatroomName} in the group. Doesn't exist.", { chatroomName: CHATROOM_GENERAL_NAME }))
}, 0)
}

if (rootState.currentGroupId === params.contractID) {
await sbp('gi.actions/group/updateLastLoggedIn', { contractID: params.contractID })
}
Expand All @@ -482,7 +445,7 @@ export default (sbp('sbp/selectors/register', {
console.log('@@@@@@@@ AT join[alreadyMember] for ' + params.contractID)
// We've already joined
const chatRoomIds = Object.keys(rootState[params.contractID].chatRooms ?? {})
.filter(cId => (rootState[params.contractID].chatRooms?.[cId].users.includes(username)))
.filter(cId => (rootState[params.contractID].chatRooms?.[cId].users?.[username].status === PROFILE_STATUS.ACTIVE))

await sbp('chelonia/contract/sync', chatRoomIds)
sbp('state/vuex/commit', 'setCurrentChatRoomId', {
Expand All @@ -502,12 +465,19 @@ export default (sbp('sbp/selectors/register', {
}

sbp('okTurtles.data/set', 'JOINING_GROUP-' + params.contractID, false)
// We don't have the secret keys and we're not waiting for OP_KEY_SHARE
// This means that we've been removed from the group
} else if (!hasSecretKeys && !isWaitingForKeyShare) {
// We have already sent a key request that hasn't been answered. We cannot
// do much at this point, so we do nothing.
// This could happen, for example, after logging in if we still haven't
// received a response to the key request.
} else {
sbp('okTurtles.data/set', 'JOINING_GROUP-' + params.contractID, false)
console.warn('Requested to join group but we\'ve been removed. contractID=' + params.contractID)
} else if (isWaitingForKeyShare) {
console.info('Requested to join group but already waiting for OP_KEY_SHARE. contractID=' + params.contractID)
} else {
console.warn('Requested to join group but the state appears invalid. contractID=' + params.contractID, { sendKeyRequest, hasSecretKeys, isWaitingForKeyShare })
}
} catch (e) {
console.error('gi.actions/group/join failed!', e)
Expand All @@ -516,7 +486,7 @@ export default (sbp('sbp/selectors/register', {
saveLoginState('joining', params.contractID)
}
},
'gi.actions/group/joinAndSwitch': async function (params: $Exact<ChelKeyRequestParams> & { options?: { skipUsableKeysCheck?: boolean; skipInviteAccept: boolean } }) {
'gi.actions/group/joinAndSwitch': async function (params: $Exact<ChelKeyRequestParams> & { options?: { skipUsableKeysCheck?: boolean; } }) {
await sbp('gi.actions/group/join', params)
// after joining, we can set the current group
sbp('gi.actions/group/switch', params.contractID)
Expand Down Expand Up @@ -548,7 +518,7 @@ export default (sbp('sbp/selectors/register', {
id: newId,
meta: {
private: {
content: encryptedOutgoingData(rootState[pContractID], CEKid, serializeKey(newKey, true))
content: encryptedOutgoingData(pContractID, CEKid, serializeKey(newKey, true))
}
}
}))
Expand All @@ -560,7 +530,7 @@ export default (sbp('sbp/selectors/register', {
const contractState = rootState[params.contractID]
const userID = rootState.loggedIn.identityContractID
for (const contractId in contractState.chatRooms) {
if (params.data.attributes.name.toUpperCase() === contractState.chatRooms[contractId].name.toUpperCase()) {
if (params.data.attributes.name.toUpperCase().normalize() === contractState.chatRooms[contractId].name.toUpperCase().normalize()) {
throw new GIErrorUIRuntimeError(L('Duplicate channel name'))
}
}
Expand Down Expand Up @@ -646,7 +616,7 @@ export default (sbp('sbp/selectors/register', {

// 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 (username !== me && [CHATROOM_PRIVACY_LEVEL.PRIVATE].includes(rootState[params.data.chatRoomID].attributes.privacyLevel)) {
if (username !== me && rootState[params.data.chatRoomID].attributes.privacyLevel === CHATROOM_PRIVACY_LEVEL.PRIVATE) {
await sbp('gi.actions/out/shareVolatileKeys', {
contractID: rootGetters.ourContactProfiles[username].contractID,
contractName: 'gi.contracts/identity',
Expand All @@ -655,35 +625,7 @@ export default (sbp('sbp/selectors/register', {
})
}

const message = await sbp('gi.actions/chatroom/join', {
...omit(params, ['options', 'contractID', 'data', 'hooks']),
contractID: params.data.chatRoomID,
data: { username },
hooks: {
prepublish: params.hooks?.prepublish,
postpublish: null
}
})

if (username === me) {
// 'JOINING_GROUP_CHAT' is necessary to identify the joining chatroom action is NEW or OLD
// Users join the chatroom thru group making group actions
// But when user joins the group, he needs to ignore all the actions about chatroom
// Because the user is joining group, not joining chatroom
// and he is going to make a new action to join 'General' chatroom AGAIN
// While joining group, we don't set this flag because Joining chatroom actions are all OLD ones, which need to be ignored
// Joining 'General' chatroom is one of the steps to join group
// So setting 'JOINING_GROUP_CHAT' can not be out of the 'JOINING_GROUP' scope
sbp('okTurtles.data/set', 'JOINING_GROUP_CHAT', true)
}
await sendMessage({
...omit(params, ['options', 'action', 'hooks']),
hooks: {
prepublish: null,
postpublish: params.hooks?.postpublish
}
})
return message
return await sendMessage(omit(params, ['options', 'action']))
}),
'gi.actions/group/addAndJoinChatRoom': async function (params: GIActionParams) {
const message = await sbp('gi.actions/group/addChatRoom', {
Expand All @@ -694,15 +636,17 @@ export default (sbp('sbp/selectors/register', {
}
})

const chatRoomID = message.contractID()

await sbp('gi.actions/group/joinChatRoom', {
...omit(params, ['options', 'data', 'hooks']),
data: {
chatRoomID: message.contractID()
chatRoomID
},
hooks: {
prepublish: (msg) => {
sbp('okTurtles.events/once', msg.hash(), (cId, m) => {
sbp('state/vuex/commit', 'setCurrentChatRoomId', { chatRoomId: cId })
sbp('okTurtles.events/once', msg.id(), (cId) => {
sbp('state/vuex/commit', 'setCurrentChatRoomId', { chatRoomId: chatRoomID, groupId: cId })
})
},
postpublish: params.hooks?.postpublish
Expand Down
Loading

0 comments on commit 9458715

Please sign in to comment.