Skip to content

Commit

Permalink
E2e protocol ricardo (#1826)
Browse files Browse the repository at this point in the history
* Remove unnecessary references

* Encrypt pub notifications

* Simplify. Fix types.

* Log level
  • Loading branch information
corrideat authored Jan 29, 2024
1 parent fc4cc7c commit 921a611
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 44 deletions.
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

0 comments on commit 921a611

Please sign in to comment.