Skip to content

Commit

Permalink
postUpgradeVerification improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
corrideat committed Oct 10, 2024
1 parent 713f8e3 commit 330624d
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 73 deletions.
6 changes: 4 additions & 2 deletions Gruntfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ const {
MAX_EVENTS_AFTER = '',
NODE_ENV = 'development',
EXPOSE_SBP = '',
ENABLE_UNSAFE_NULL_CRYPTO = 'false'
ENABLE_UNSAFE_NULL_CRYPTO = 'false',
UNSAFE_TRUST_ALL_MANIFEST_SIGNING_KEYS = 'false'
} = process.env

if (!['development', 'production'].includes(NODE_ENV)) {
Expand Down Expand Up @@ -223,7 +224,8 @@ module.exports = (grunt) => {
'process.env.MAX_EVENTS_AFTER': `'${MAX_EVENTS_AFTER}'`,
'process.env.NODE_ENV': `'${NODE_ENV}'`,
'process.env.EXPOSE_SBP': `'${EXPOSE_SBP}'`,
'process.env.ENABLE_UNSAFE_NULL_CRYPTO': `'${ENABLE_UNSAFE_NULL_CRYPTO}'`
'process.env.ENABLE_UNSAFE_NULL_CRYPTO': `'${ENABLE_UNSAFE_NULL_CRYPTO}'`,
'process.env.UNSAFE_TRUST_ALL_MANIFEST_SIGNING_KEYS': UNSAFE_TRUST_ALL_MANIFEST_SIGNING_KEYS
},
external: ['crypto', '*.eot', '*.ttf', '*.woff', '*.woff2'],
format: 'esm',
Expand Down
112 changes: 58 additions & 54 deletions frontend/controller/actions/group.js
Original file line number Diff line number Diff line change
Expand Up @@ -912,71 +912,75 @@ export default (sbp('sbp/selectors/register', {
sbp('okTurtles.events/emit', REPLACE_MODAL, 'IncomeDetails')
}
},
'gi.actions/group/fixAnyoneCanJoinLink': async function ({ contractID }) {
const now = await sbp('chelonia/time') * 1000
const state = await sbp('chelonia/contract/state', contractID)

// Add up all all used 'anyone can join' invite links
// Then, we take the difference and, if the number is less than
// MAX_GROUP_MEMBER_COUNT, we create a new invite.
const usedInvites = Object.keys(state.invites)
.filter(invite => state.invites[invite].creatorID === INVITE_INITIAL_CREATOR)
.reduce((acc, cv) => acc +
'gi.actions/group/fixAnyoneCanJoinLink': function ({ contractID }) {
// Queue ensures that the update happens as atomically as possible
return sbp('chelonia/queueInvocation', `${contractID}-FIX-ANYONE-CAN-JOIN`, async () => {
const now = await sbp('chelonia/time') * 1000
const state = await sbp('chelonia/contract/wait', contractID).then(() => sbp('chelonia/contract/state', contractID))

// Add up all all used 'anyone can join' invite links
// Then, we take the difference and, if the number is less than
// MAX_GROUP_MEMBER_COUNT, we create a new invite.
const usedInvites = Object.keys(state.invites)
.filter(invite => state.invites[invite].creatorID === INVITE_INITIAL_CREATOR)
.reduce((acc, cv) => acc +
((state._vm.invites[cv].initialQuantity - state._vm.invites[cv].quantity
) || 0), 0)

const quantity = MAX_GROUP_MEMBER_COUNT - usedInvites
const quantity = MAX_GROUP_MEMBER_COUNT - usedInvites

if (quantity <= 0) {
console.warn('[gi.actions/group/fixAnyoneCanJoinLink] Already used MAX_GROUP_MEMBER_COUNT invites for group', contractID, MAX_GROUP_MEMBER_COUNT)
return
}
if (quantity <= 0) {
console.warn('[gi.actions/group/fixAnyoneCanJoinLink] Already used MAX_GROUP_MEMBER_COUNT invites for group', contractID, MAX_GROUP_MEMBER_COUNT)
return
}

const CEKid = findKeyIdByName(state, 'cek')
const CSKid = findKeyIdByName(state, 'csk')
const CEKid = findKeyIdByName(state, 'cek')
const CSKid = findKeyIdByName(state, 'csk')

if (!CEKid || !CSKid) {
throw new Error('Contract is missing a CEK or CSK')
}
if (!CEKid || !CSKid) {
throw new Error('Contract is missing a CEK or CSK')
}

const inviteKey = keygen(EDWARDS25519SHA512BATCH)
const inviteKeyId = keyId(inviteKey)
const inviteKeyP = serializeKey(inviteKey, false)
const inviteKeyS = encryptedOutgoingData(state, CEKid, serializeKey(inviteKey, true))

const ik = {
id: inviteKeyId,
name: '#inviteKey-' + inviteKeyId,
purpose: ['sig'],
ringLevel: Number.MAX_SAFE_INTEGER,
permissions: [GIMessage.OP_KEY_REQUEST],
meta: {
quantity,
...(INVITE_EXPIRES_IN_DAYS.ON_BOARDING && {
expires:
await sbp('chelonia/time') * 1000 + DAYS_MILLIS * INVITE_EXPIRES_IN_DAYS.ON_BOARDING
}),
private: {
content: inviteKeyS
}
},
data: inviteKeyP
}
const inviteKey = keygen(EDWARDS25519SHA512BATCH)
const inviteKeyId = keyId(inviteKey)
const inviteKeyP = serializeKey(inviteKey, false)
const inviteKeyS = encryptedOutgoingData(state, CEKid, serializeKey(inviteKey, true))

const ik = {
id: inviteKeyId,
name: '#inviteKey-' + inviteKeyId,
purpose: ['sig'],
ringLevel: Number.MAX_SAFE_INTEGER,
permissions: [GIMessage.OP_KEY_REQUEST],
meta: {
quantity,
...(INVITE_EXPIRES_IN_DAYS.ON_BOARDING && {
expires:
now + DAYS_MILLIS * INVITE_EXPIRES_IN_DAYS.ON_BOARDING
}),
private: {
content: inviteKeyS
}
},
data: inviteKeyP
}

// Replace all existing anyone-can-join invite links with the new one
const activeInvites = Object.keys(state.invites)
.filter(invite => state.invites[invite].creatorID === INVITE_INITIAL_CREATOR && !(state._vm.invites[invite].expires >= now))
// Replace all existing anyone-can-join invite links with the new one
const activeInvites = Object.keys(state.invites)
.filter(invite => state.invites[invite].creatorID === INVITE_INITIAL_CREATOR && !(state._vm.invites[invite].expires >= now))

await Promise.all(activeInvites.map(inviteKeyId => sbp('gi.actions/group/inviteRevoke', { contractID, data: { inviteKeyId } })))
await sbp('chelonia/out/keyAdd', {
contractName: 'gi.contracts/group',
contractID,
data: [ik],
signingKeyId: CSKid
})

await sbp('chelonia/out/keyAdd', {
contractName: 'gi.contracts/group',
contractID,
data: [ik],
signingKeyId: CSKid
})
await sbp('gi.actions/group/invite', { contractID, data: { inviteKeyId, creatorID: INVITE_INITIAL_CREATOR } })

await sbp('gi.actions/group/invite', { contractID, data: { inviteKeyId, creatorID: 'invite-initial-creator' } })
// Revoke at the end
await Promise.all(activeInvites.map(inviteKeyId => sbp('gi.actions/group/inviteRevoke', { contractID, data: { inviteKeyId } })))
})
},
...encryptedAction('gi.actions/group/leaveChatRoom', L('Failed to leave chat channel.'), async (sendMessage, params) => {
const state = await sbp('chelonia/contract/state', params.contractID)
Expand Down
72 changes: 64 additions & 8 deletions frontend/model/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import sbp from '@sbp/sbp'
import { L } from '@common/common.js'
import { EVENT_HANDLED, CONTRACT_REGISTERED } from '~/shared/domains/chelonia/events.js'
import { LOGOUT } from '~/frontend/utils/events.js'
import { LOGIN_COMPLETE, LOGIN_ERROR, LOGOUT } from '~/frontend/utils/events.js'
import Vue from 'vue'
import Vuex from 'vuex'
import { MAX_GROUP_MEMBER_COUNT, PROFILE_STATUS, INVITE_INITIAL_CREATOR } from '@model/contracts/shared/constants.js'
Expand All @@ -22,6 +22,39 @@ import identityGetters from './contracts/shared/getters/identity.js'
import notificationModule from '~/frontend/model/notifications/vuexModule.js'
import settingsModule from '~/frontend/model/settings/vuexModule.js'
import chatroomModule from '~/frontend/model/chatroom/vuexModule.js'
import { CONTRACTS_MODIFIED } from '../../shared/domains/chelonia/events.js'

// Wrapper function for performing contract upgrades and migrations
const contractUpdate = (updateFn: (contractIDHints: ?string[]) => any) => {
const loginErrorHandler = () => {
sbp('okTurtles.events/off', CONTRACTS_MODIFIED, modifiedHandled)
sbp('okTurtles.events/off', LOGIN_COMPLETE, loginCompleteHandler)
sbp('okTurtles.events/off', LOGOUT, logoutHandler)
}
const logoutHandler = () => {
sbp('okTurtles.events/off', CONTRACTS_MODIFIED, modifiedHandled)
}
const loginCompleteHandler = () => {
sbp('okTurtles.events/off', LOGIN_ERROR, loginErrorHandler)
}
// This function is called when the set of subscribed contracts is modified
const modifiedHandled = (_, { added }) => {
// Wait for the added contracts to be ready, then call the update function
sbp('chelonia/contract/wait', added).then(() => {
updateFn(added)
})
}

// Add event listeners for CONTRACTS_MODIFIED, LOGOUT, LOGIN_COMPLETE and
// LOGIN_ERROR
sbp('okTurtles.events/on', CONTRACTS_MODIFIED, modifiedHandled)
sbp('okTurtles.events/once', LOGOUT, logoutHandler)
sbp('okTurtles.events/once', LOGIN_COMPLETE, loginCompleteHandler)
sbp('okTurtles.events/once', LOGIN_ERROR, loginErrorHandler)

// Call the update function in the next tick
setTimeout(updateFn, 0)
}

Vue.use(Vuex)

Expand Down Expand Up @@ -90,7 +123,13 @@ sbp('sbp/selectors/register', {
// $FlowFixMe[incompatible-call]
Vue.set(state, 'reverseNamespaceLookups', Object.fromEntries(Object.entries(state.namespaceLookups).map(([k, v]: [string, string]) => [v, k])))
}
(() => {
contractUpdate((contractIDHints: ?string[]) => {
const state = sbp('state/vuex/state')
if (Array.isArray(contractIDHints)) {
if (!contractIDHints.reduce((acc, contractID) => {
return (acc || state.contracts[contractID]?.type === 'gi.contracts/group')
}, false)) return
}
// Upgrade from version 1.0.7 to a newer version
// The new group contract introduces a breaking change: the
// `state[groupID].chatRooms[chatRoomID].members[memberID].joinedHeight`
Expand All @@ -101,6 +140,7 @@ sbp('sbp/selectors/register', {
if (!ourIdentityContractId || !state[ourIdentityContractId]?.groups) return
Object.entries(state[ourIdentityContractId].groups).map(([groupID, { hasLeft }]: [string, Object]) => {
if (hasLeft || !state[groupID]?.chatRooms) return undefined
if (Array.isArray(contractIDHints) && !contractIDHints.includes(groupID)) return undefined
// $FlowFixMe[incompatible-use]
return Object.values((state[groupID].chatRooms: { [string]: Object })).flatMap(({ members }) => {
return Object.values(members)
Expand All @@ -117,13 +157,22 @@ sbp('sbp/selectors/register', {
console.error('[state/vuex/postUpgradeVerification] Error during gi.actions/group/upgradeFrom1.0.7', contractID, e)
})
})
})();
(() => {
})
contractUpdate((contractIDHints: ?string[]) => {
const state = sbp('state/vuex/state')
if (Array.isArray(contractIDHints)) {
if (!contractIDHints.reduce((acc, contractID) => {
return (acc || state.contracts[contractID]?.type === 'gi.contracts/chatroom')
}, false)) return
}
// Upgrade from version 1.0.8 to a newer version
// The new chatroom contracts have an admin IDs list
// This code checks if the attribute is missing, and if so, issues the
// corresponing upgrade action.
const needsUpgrade = (chatroomID) => !Array.isArray(state[chatroomID]?.attributes?.adminIDs)
const needsUpgrade = (chatroomID) => {
if (Array.isArray(contractIDHints) && !contractIDHints.includes(chatroomID)) return false
return !Array.isArray(state[chatroomID]?.attributes?.adminIDs)
}

const upgradeAction = async (contractID: string, data?: Object) => {
try {
Expand Down Expand Up @@ -157,8 +206,14 @@ sbp('sbp/selectors/register', {
upgradeAction(contractID)
})
}
})();
(() => {
})
contractUpdate((contractIDHints: ?string[]) => {
const state = sbp('state/vuex/state')
if (Array.isArray(contractIDHints)) {
if (!contractIDHints.reduce((acc, contractID) => {
return (acc || state.contracts[contractID]?.type === 'gi.contracts/group')
}, false)) return
}
// Update expired invites
// If fewer than MAX_GROUP_MEMBER_COUNT 'anyone can join' have been used,
// create a new 'anyone can join' link up to MAX_GROUP_MEMBER_COUNT invites
Expand All @@ -167,6 +222,7 @@ sbp('sbp/selectors/register', {
Object.entries(state[ourIdentityContractId].groups).map(([groupID, { hasLeft }]: [string, Object]) => {
const groupState = state[groupID]
if (hasLeft || !groupState?.invites) return undefined
if (Array.isArray(contractIDHints) && !contractIDHints.includes(groupID)) return undefined
const hasBeenUpdated = Object.keys(groupState.invites).some(inviteId => {
return groupState.invites[inviteId].creatorID === INVITE_INITIAL_CREATOR &&
groupState._vm.invites[inviteId].expires == null
Expand All @@ -180,7 +236,7 @@ sbp('sbp/selectors/register', {
}).filter(Boolean).forEach((contractID) => {
sbp('gi.actions/group/fixAnyoneCanJoinLink', { contractID }).catch(e => console.error(`[state/vuex/postUpgradeVerification] Error during gi.actions/group/fixAnyoneCanJoinLink for ${contractID}:`, e))
})
})()
})
},
'state/vuex/save': (encrypted: ?boolean, state: ?Object) => {
return sbp('okTurtles.eventQueue/queueEvent', 'state/vuex/save', async function () {
Expand Down
10 changes: 6 additions & 4 deletions frontend/views/containers/group-settings/InvitationsTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -238,11 +238,13 @@ export default ({
else return isInviteExpired || isInviteRevoked ? L('Not used') : L('Not used yet')
},
readableExpiryInfo (expiryTime) {
if (expiryTime == null) return
const { years, months, days, hours, minutes } = timeLeft(expiryTime)
if (expiryTime == null) return L("Doesn't expire")
const { expired, years, months, days, hours, minutes } = timeLeft(expiryTime)
if (expired) L('Expired')
// In the cases when displaying years/months, count the remainer hours/mins as +1 day eg) 3days 15hrs 25mins -> 4days.
if (years) return L('{years}y {months}mo {days}d left', { years, months, days })
if (months) return L('{months}mo {days}d left', { months, days })
if (years) return L('{years}y {months}mo {days}d left', { years, months, days: days + ((hours || minutes) ? 1 : 0) })
if (months) return L('{months}mo {days}d left', { months, days: days + ((hours || minutes) ? 1 : 0) })
if (days) return L('{days}d {hours}h {minutes}m left', { days, hours, minutes })
if (hours) return L('{hours}h {minutes}m left', { hours, minutes })
Expand Down
3 changes: 2 additions & 1 deletion shared/domains/chelonia/chelonia.js
Original file line number Diff line number Diff line change
Expand Up @@ -368,10 +368,11 @@ export default (sbp('sbp/selectors/register', {
clearObject(this.currentSyncs)
clearObject(this.postSyncOperations)
clearObject(this.sideEffectStacks)
const removedContractIDs = Array.from(this.subscriptionSet)
this.subscriptionSet.clear()
sbp('chelonia/clearTransientSecretKeys')
sbp('okTurtles.events/emit', CHELONIA_RESET)
sbp('okTurtles.events/emit', CONTRACTS_MODIFIED, Array.from(this.subscriptionSet))
sbp('okTurtles.events/emit', CONTRACTS_MODIFIED, Array.from(this.subscriptionSet), { added: [], removed: removedContractIDs })
sbp('chelonia/private/startClockSync')
if (newState) {
Object.entries(newState).forEach(([key, value]) => {
Expand Down
8 changes: 4 additions & 4 deletions shared/domains/chelonia/internals.js
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export default (sbp('sbp/selectors/register', {
const contractNameLookupKey = `name:${contractName}`
// If the contract name has been seen before, validate its signature now
let signatureValidated = false
if (has(rootState.contractSigningKeys, contractNameLookupKey)) {
if (!process.env.UNSAFE_TRUST_ALL_MANIFEST_SIGNING_KEYS && has(rootState.contractSigningKeys, contractNameLookupKey)) {
console.info(`[chelonia] verifying signature for ${manifestHash} with an existing key`)
if (!has(rootState.contractSigningKeys[contractNameLookupKey], manifest.signature.keyId)) {
console.error(`The manifest with ${manifestHash} (named ${contractName}) claims to be signed with a key with ID ${manifest.signature.keyId}, which is not trusted. The trusted key IDs for this name are:`, Object.keys(rootState.contractSigningKeys[contractNameLookupKey]))
Expand Down Expand Up @@ -431,7 +431,7 @@ export default (sbp('sbp/selectors/register', {

this.subscriptionSet.delete(contractID)
// calling this will make pubsub unsubscribe for events on `contractID`
sbp('okTurtles.events/emit', CONTRACTS_MODIFIED, Array.from(this.subscriptionSet))
sbp('okTurtles.events/emit', CONTRACTS_MODIFIED, Array.from(this.subscriptionSet), { added: [], removed: [contractID] })
},
// used by, e.g. 'chelonia/contract/wait'
'chelonia/private/noop': function () {},
Expand Down Expand Up @@ -1296,7 +1296,7 @@ export default (sbp('sbp/selectors/register', {
}
} else if (!isSubcribed) {
this.subscriptionSet.add(contractID)
sbp('okTurtles.events/emit', CONTRACTS_MODIFIED, Array.from(this.subscriptionSet))
sbp('okTurtles.events/emit', CONTRACTS_MODIFIED, Array.from(this.subscriptionSet), { added: [contractID], removed: [] })
const entryIndex = this.pending.findIndex((entry) => entry?.contractID === contractID)
if (entryIndex !== -1) {
this.pending.splice(entryIndex, 1)
Expand Down Expand Up @@ -2093,7 +2093,7 @@ const handleEvent = {
}
}
this.subscriptionSet.add(contractID)
sbp('okTurtles.events/emit', CONTRACTS_MODIFIED, Array.from(this.subscriptionSet))
sbp('okTurtles.events/emit', CONTRACTS_MODIFIED, Array.from(this.subscriptionSet), { added: [contractID], removed: [] })
}

if (!processingErrored) {
Expand Down

0 comments on commit 330624d

Please sign in to comment.