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

E2e protocol ricardo #1826

Merged
merged 4 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
16 changes: 3 additions & 13 deletions frontend/controller/actions/chatroom.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use strict'
import sbp from '@sbp/sbp'

import { PUBSUB_INSTANCE } from '@controller/instance-keys.js'
import { GIErrorUIRuntimeError, L } from '@common/common.js'
import { has, omit } from '@model/contracts/shared/giLodash.js'
import { GIMessage } from '~/shared/domains/chelonia/GIMessage.js'
Expand All @@ -10,8 +9,7 @@ import { encryptedOutgoingData, encryptedOutgoingDataWithRawKey } from '~/shared
// 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'
import type { GIRegParams } from './types.js'
import { encryptedAction } from './utils.js'
import { CHATROOM_USER_TYPING, CHATROOM_USER_STOP_TYPING } from '@utils/events.js'
import { encryptedAction, encryptedNotification } from './utils.js'

export default (sbp('sbp/selectors/register', {
'gi.actions/chatroom/create': async function (params: GIRegParams) {
Expand Down Expand Up @@ -183,16 +181,8 @@ export default (sbp('sbp/selectors/register', {
}
}))
},
'gi.actions/chatroom/emit-user-typing-event': (chatroomId: string, username: string) => {
// publish CHATROOM_USER_TYPING event to every subscribers of the pubsub channel with chatroomId
const pubsub = sbp('okTurtles.data/get', PUBSUB_INSTANCE)
pubsub.pub(chatroomId, { type: CHATROOM_USER_TYPING, username })
},
'gi.actions/chatroom/emit-user-stop-typing-event': (chatroomId: string, username: string) => {
// publish CHATROOM_USER_STOP_TYPING event to every subscribers of the pubsub channel with chatroomId
const pubsub = sbp('okTurtles.data/get', PUBSUB_INSTANCE)
pubsub.pub(chatroomId, { type: CHATROOM_USER_STOP_TYPING, username })
},
...encryptedNotification('gi.actions/chatroom/user-typing-event', L('Failed to send typing notification')),
...encryptedNotification('gi.actions/chatroom/user-stop-typing-event', L('Failed to send stopped typing notification')),
...encryptedAction('gi.actions/chatroom/addMessage', L('Failed to add message.')),
...encryptedAction('gi.actions/chatroom/editMessage', L('Failed to edit message.')),
...encryptedAction('gi.actions/chatroom/deleteMessage', L('Failed to delete message.')),
Expand Down
106 changes: 106 additions & 0 deletions frontend/controller/actions/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,112 @@ export const encryptedAction = (
}
}

export const encryptedNotification = (
action: string,
humanError: string | Function,
handler?: (sendMessage: (params: $Shape<GIActionParams>) => any, params: GIActionParams, signingKeyId: string, encryptionKeyId: string, originatingContractID: ?string) => Promise<void>,
encryptionKeyName?: string,
signingKeyName?: string,
innerSigningKeyName?: string
): Object => {
const sendMessageFactory = (outerParams: GIActionParams, signingKeyId: string, innerSigningKeyId: ?string, encryptionKeyId: string, originatingContractID: ?string) => (innerParams?: $Shape<GIActionParams>): any[] | Promise<void> => {
const params = innerParams ?? outerParams

const actionReplaced = action.replace('gi.actions', 'gi.contracts')

return sbp('chelonia/out/encryptedOrUnencryptedPubMessage', {
contractID: params.contractID,
contractName: actionReplaced.split('/', 2).join('/'),
innerSigningKeyId,
encryptionKeyId,
signingKeyId,
data: [actionReplaced, params.data]
})
}
return {
[action]: async function (params: GIActionParams) {
const contractID = params.contractID
if (!contractID) {
throw new Error('Missing contract ID')
}

try {
// Writing to a contract requires being subscribed to it
await sbp('chelonia/contract/sync', contractID, { deferredRemove: true })
const state = {
[contractID]: await sbp('chelonia/latestContractState', contractID)
}
const rootState = sbp('state/vuex/state')

// Default signingContractID is the current contract
const signingContractID = params.signingContractID || contractID
if (!state[signingContractID]) {
state[signingContractID] = await sbp('chelonia/latestContractState', signingContractID)
}

// Default innerSigningContractID is the current logged in identity
// contract ID, unless we are signing for the current identity contract
// If params.innerSigningContractID is explicitly set to null, then
// no default value will be used.
const innerSigningContractID = params.innerSigningContractID !== undefined
? params.innerSigningContractID
: contractID === rootState.loggedIn.identityContractID
? null
: rootState.loggedIn.identityContractID

if (innerSigningContractID && !state[innerSigningContractID]) {
state[innerSigningContractID] = await sbp('chelonia/latestContractState', innerSigningContractID)
}

const signingKeyId = params.signingKeyId || findKeyIdByName(state[signingContractID], signingKeyName ?? 'csk')
// Inner signing key ID:
// (1) If params.innerSigningKeyId is set, honor it
// (a) If it's null, then no inner signature will be used
// (b) If it's undefined, it's treated the same as if it's not set
// (2) If params.innerSigningKeyId is not set:
// (a) If innerSigningContractID is not set, then no inner
// signature will be used
// (b) Else, use the key by name `innerSigningKeyName` in
// `innerSigningContractID`

const innerSigningKeyId = params.innerSigningKeyId || (
params.innerSigningKeyId !== null &&
innerSigningContractID &&
findKeyIdByName(state[innerSigningContractID], innerSigningKeyName ?? 'csk')
)
const encryptionKeyId = params.encryptionKeyId || findKeyIdByName(state[contractID], encryptionKeyName ?? 'cek')

if (!signingKeyId || !encryptionKeyId || !sbp('chelonia/haveSecretKey', signingKeyId)) {
console.warn(`Refusing to send action ${action} due to missing CSK or CEK`, { contractID, action, signingKeyName, encryptionKeyName, signingKeyId, encryptionKeyId, signingContractID: params.signingContractID, originatingContractID: params.originatingContractID })
throw new GIErrorMissingSigningKeyError(`No key found to send ${action} for contract ${contractID}`)
}

if (innerSigningContractID && (!innerSigningKeyId || !sbp('chelonia/haveSecretKey', innerSigningKeyId))) {
console.warn(`Refusing to send action ${action} due to missing inner signing key ID`, { contractID, action, signingKeyName, encryptionKeyName, signingKeyId, encryptionKeyId, signingContractID: params.signingContractID, originatingContractID: params.originatingContractID, innerSigningKeyId })
throw new GIErrorMissingSigningKeyError(`No key found to send ${action} for contract ${contractID}`)
}

const sm = sendMessageFactory(params, signingKeyId, innerSigningKeyId || null, encryptionKeyId, params.originatingContractID)

// make sure to await here so that if there's an error we show user-facing string
if (handler) {
return await handler(sm, params, signingKeyId, encryptionKeyId, params.originatingContractID)
} else {
return await sm()
}
} catch (e) {
console.error(`${action} failed!`, e)
const userFacingErrStr = typeof humanError === 'string'
? `${humanError} ${LError(e).reportError}`
: humanError(params, e)
throw new GIErrorUIRuntimeError(userFacingErrStr, { cause: e })
} finally {
await sbp('chelonia/contract/remove', contractID, { removeIfPending: true })
}
}
}
}

export async function createInvite ({ quantity = 1, creator, expires, invitee }: {
quantity: number, creator: string, expires: number, invitee?: string
}): Promise<{inviteKeyId: string; creator: string; invitee?: string; }> {
Expand Down
14 changes: 7 additions & 7 deletions frontend/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -213,19 +213,19 @@ async function startApp () {
sbp('okTurtles.events/emit', REQUEST_TYPE.PUSH_ACTION, { data: msg.data })
},
[NOTIFICATION_TYPE.PUB] (msg) {
const { channelID, data } = msg
const { contractID, innerSigningContractID, data } = msg

switch (data.type) {
case CHATROOM_USER_TYPING: {
sbp('okTurtles.events/emit', CHATROOM_USER_TYPING, { username: data.username })
switch (data[0]) {
case 'gi.contracts/chatroom/user-typing-event': {
sbp('okTurtles.events/emit', CHATROOM_USER_TYPING, { contractID, innerSigningContractID })
break
}
case CHATROOM_USER_STOP_TYPING: {
sbp('okTurtles.events/emit', CHATROOM_USER_STOP_TYPING, { username: data.username })
case 'gi.contracts/chatroom/user-stop-typing-event': {
sbp('okTurtles.events/emit', CHATROOM_USER_STOP_TYPING, { contractID, innerSigningContractID })
break
}
default: {
console.log(`[pubsub] Received data from channel ${channelID}:`, data)
console.log(`[pubsub] Received data from channel ${contractID}:`, data)
}
}
}
Expand Down
2 changes: 0 additions & 2 deletions frontend/views/containers/chatroom/ChatMain.vue
Original file line number Diff line number Diff line change
Expand Up @@ -227,8 +227,6 @@ export default ({
},
data () {
return {
GIMessage,
JSON,
config: {
isPhone: null
},
Expand Down
29 changes: 19 additions & 10 deletions frontend/views/containers/chatroom/SendArea.vue
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,7 @@ export default ({
'currentChatRoomId',
'chatRoomAttributes',
'ourContactProfiles',
'ourContactProfilesById',
'globalProfile',
'ourUsername'
]),
Expand Down Expand Up @@ -509,7 +510,11 @@ export default ({

if (!newValue) {
// if the textarea has become empty, emit CHATROOM_USER_STOP_TYPING event.
sbp('gi.actions/chatroom/emit-user-stop-typing-event', this.currentChatRoomId, this.ourUsername)
sbp('gi.actions/chatroom/user-stop-typing-event', {
contractID: this.currentChatRoomId
}).catch(e => {
console.error('Error emitting user stopped typing event', e)
})
} else if (this.ephemeral.textWithLines.length < newValue.length) {
// if the user is typing and the textarea value is growing, emit CHATROOM_USER_TYPING event.
this.throttledEmitUserTypingEvent()
Expand Down Expand Up @@ -697,9 +702,10 @@ export default ({
}
},
onUserTyping (data) {
const typingUser = data.username
if (data.contractID !== this.currentChatRoomId) return
const typingUser = this.ourContactProfilesById[data.innerSigningContractID]?.username

if (typingUser !== this.ourUsername) {
if (typingUser && typingUser !== this.ourUsername) {
const addToList = username => {
this.ephemeral.typingUsers = uniq([...this.ephemeral.typingUsers, username])
}
Expand All @@ -710,8 +716,11 @@ export default ({
}
},
onUserStopTyping (data) {
if (data.username !== this.ourUsername) {
this.removeFromTypingUsersArray(data.username)
if (data.contractID !== this.currentChatRoomId) return
const typingUser = this.ourContactProfilesById[data.innerSigningContractID]?.username

if (typingUser && typingUser !== this.ourUsername) {
this.removeFromTypingUsersArray(typingUser)
}
},
removeFromTypingUsersArray (username) {
Expand All @@ -723,14 +732,14 @@ export default ({
}
},
emitUserTypingEvent () {
sbp('gi.actions/chatroom/emit-user-typing-event',
this.currentChatRoomId,
this.ourUsername
)
sbp('gi.actions/chatroom/user-typing-event', {
contractID: this.currentChatRoomId
}).catch(e => {
console.error('Error emitting user typing event', e)
})
},
onBtnClick (e) {
e.preventDefault()
console.log('!@# on btn click: ', e)
}
}
}: Object)
Expand Down
Loading
Loading