Skip to content

Commit

Permalink
fix: allow specific topics when getting HMAC keys
Browse files Browse the repository at this point in the history
  • Loading branch information
rygine committed Mar 12, 2024
1 parent c50c81b commit bbd7010
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 5 deletions.
19 changes: 16 additions & 3 deletions src/keystore/InMemoryKeystore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ import {
userPreferencesEncrypt,
generateUserPreferencesTopic,
} from '../crypto/selfEncryption'
import type { KeystoreInterface } from '..'
import {
exportHmacKey,
generateHmacSignature,
hkdfHmacKey,
} from '../crypto/encryption'
import type { KeystoreInterface } from './rpcDefinitions'

const { ErrorCode } = keystore

Expand Down Expand Up @@ -595,15 +595,28 @@ export default class InMemoryKeystore implements KeystoreInterface {
return this.v2Store.lookup(topic)
}

async getV2ConversationHmacKeys(): Promise<keystore.GetConversationHmacKeysResponse> {
async getV2ConversationHmacKeys(
req?: keystore.GetConversationHmacKeysRequest
): Promise<keystore.GetConversationHmacKeysResponse> {
const thirtyDayPeriodsSinceEpoch = Math.floor(
Date.now() / 1000 / 60 / 60 / 24 / 30
)

const hmacKeys: keystore.GetConversationHmacKeysResponse['hmacKeys'] = {}

let topics = this.v2Store.topics

// if specific topics are requested, only include those topics
if (req?.topics) {
topics = topics.filter(
(topicData) =>
topicData.invitation !== undefined &&
req.topics.includes(topicData.invitation.topic)
)
}

await Promise.all(
this.v2Store.topics.map(async (topicData) => {
topics.map(async (topicData) => {
if (topicData.invitation?.topic) {
const keyMaterial = getKeyMaterial(topicData.invitation)
const values = await Promise.all(
Expand Down
6 changes: 5 additions & 1 deletion src/keystore/rpcDefinitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,12 @@ export const apiDefs = {
req: null,
res: keystore.GetPrivatePreferencesTopicIdentifierResponse,
},
/**
* Returns the conversation HMAC keys for the current, previous, and next
* 30 day periods since the epoch
*/
getV2ConversationHmacKeys: {
req: null,
req: keystore.GetConversationHmacKeysRequest,
res: keystore.GetConversationHmacKeysResponse,
},
}
Expand Down
107 changes: 106 additions & 1 deletion test/keystore/InMemoryKeystore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -868,7 +868,7 @@ describe('InMemoryKeystore', () => {
})

describe('getV2ConversationHmacKeys', () => {
it('returns conversation HMAC keys', async () => {
it('returns all conversation HMAC keys', async () => {
const baseTime = new Date()
const timestamps = Array.from(
{ length: 5 },
Expand Down Expand Up @@ -967,5 +967,110 @@ describe('InMemoryKeystore', () => {
})
)
})

it('returns specific conversation HMAC keys', async () => {
const baseTime = new Date()
const timestamps = Array.from(
{ length: 10 },
(_, i) => new Date(baseTime.getTime() + i)
)

const invites = await Promise.all(
[...timestamps].map(async (createdAt) => {
let keys = await PrivateKeyBundleV1.generate(newWallet())

const recipient = SignedPublicKeyBundle.fromLegacyBundle(
keys.getPublicKeyBundle()
)

return aliceKeystore.createInvite({
recipient,
createdNs: dateToNs(createdAt),
context: undefined,
})
})
)

const thirtyDayPeriodsSinceEpoch = Math.floor(
Date.now() / 1000 / 60 / 60 / 24 / 30
)

const periods = [
thirtyDayPeriodsSinceEpoch - 1,
thirtyDayPeriodsSinceEpoch,
thirtyDayPeriodsSinceEpoch + 1,
]

const randomInvites = invites.slice(3, 8)

const { hmacKeys } = await aliceKeystore.getV2ConversationHmacKeys({
topics: randomInvites.map((invite) => invite.conversation!.topic),
})

const topics = Object.keys(hmacKeys)
expect(topics.length).toBe(randomInvites.length)
randomInvites.forEach((invite) => {
expect(topics.includes(invite.conversation!.topic)).toBeTruthy()
})

const topicHmacs: {
[topic: string]: Uint8Array
} = {}
const headerBytes = new Uint8Array(10)

await Promise.all(
randomInvites.map(async (invite) => {
const topic = invite.conversation!.topic
const payload = new TextEncoder().encode('Hello, world!')

const {
responses: [encrypted],
} = await aliceKeystore.encryptV2({
requests: [
{
contentTopic: topic,
payload,
headerBytes,
},
],
})

if (encrypted.error) {
throw encrypted.error
}

const topicData = aliceKeystore.lookupTopic(topic)
const keyMaterial = getKeyMaterial(topicData!.invitation)
const info = `${thirtyDayPeriodsSinceEpoch}-${aliceKeystore.walletAddress}`
const hmac = await generateHmacSignature(
keyMaterial,
new TextEncoder().encode(info),
headerBytes
)

topicHmacs[topic] = hmac
})
)

await Promise.all(
Object.keys(hmacKeys).map(async (topic) => {
const hmacData = hmacKeys[topic]

await Promise.all(
hmacData.values.map(
async ({ hmacKey, thirtyDayPeriodsSinceEpoch }, idx) => {
expect(thirtyDayPeriodsSinceEpoch).toBe(periods[idx])
const valid = await verifyHmacSignature(
await importHmacKey(hmacKey),
topicHmacs[topic],
headerBytes
)
expect(valid).toBe(idx === 1 ? true : false)
}
)
)
})
)
})
})
})

0 comments on commit bbd7010

Please sign in to comment.