mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
108 lines
3.6 KiB
TypeScript
108 lines
3.6 KiB
TypeScript
import { createHash } from 'node:crypto'
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
encodeMobileE2EEV2Transcript,
|
|
validateMobileE2EEV2Handshake
|
|
} from './mobile-e2ee-v2-contract'
|
|
import { createMobileE2EEV2Fixture, MOBILE_E2EE_V2_VECTOR } from './mobile-e2ee-v2-fixtures'
|
|
|
|
describe('mobile E2EE v2 contract', () => {
|
|
it('validates the exact relay handshake and canonical encodings', () => {
|
|
const { hello, ready } = createMobileE2EEV2Fixture()
|
|
expect(validateMobileE2EEV2Handshake(hello, ready)).not.toBeNull()
|
|
})
|
|
|
|
it('rejects unknown fields, noncanonical base64, and an unechoed nonce', () => {
|
|
const { hello, ready } = createMobileE2EEV2Fixture()
|
|
expect(validateMobileE2EEV2Handshake({ ...hello, extra: true }, ready)).toBeNull()
|
|
expect(
|
|
validateMobileE2EEV2Handshake(
|
|
{ ...hello, clientPublicKeyB64: hello.clientPublicKeyB64.replace(/=$/, '') },
|
|
ready
|
|
)
|
|
).toBeNull()
|
|
expect(
|
|
validateMobileE2EEV2Handshake(hello, {
|
|
...ready,
|
|
clientNonceB64: btoa(String.fromCharCode(...new Uint8Array(32).fill(9)))
|
|
})
|
|
).toBeNull()
|
|
})
|
|
|
|
it('rejects each oversized handshake field without decoding it', () => {
|
|
const { hello, ready } = createMobileE2EEV2Fixture()
|
|
const oversized = 'A'.repeat(48)
|
|
const decode = vi.spyOn(globalThis, 'atob')
|
|
try {
|
|
expect(validateMobileE2EEV2Handshake(hello, ready)).not.toBeNull()
|
|
for (const [invalidHello, invalidReady] of [
|
|
[{ ...hello, clientPublicKeyB64: oversized }, ready],
|
|
[hello, { ...ready, desktopPublicKeyB64: oversized }],
|
|
[
|
|
{ ...hello, clientNonceB64: oversized },
|
|
{ ...ready, clientNonceB64: oversized }
|
|
],
|
|
[hello, { ...ready, desktopNonceB64: oversized }]
|
|
]) {
|
|
decode.mockClear()
|
|
expect(validateMobileE2EEV2Handshake(invalidHello, invalidReady)).toBeNull()
|
|
expect(decode).not.toHaveBeenCalledWith(oversized)
|
|
}
|
|
} finally {
|
|
decode.mockRestore()
|
|
}
|
|
})
|
|
|
|
it('rejects context and capability-selection changes', () => {
|
|
const { hello, ready } = createMobileE2EEV2Fixture()
|
|
expect(
|
|
validateMobileE2EEV2Handshake(hello, {
|
|
...ready,
|
|
context: { ...ready.context, relayHostId: 'ZbCdEf0123_-xyZ9' }
|
|
})
|
|
).toBeNull()
|
|
expect(
|
|
validateMobileE2EEV2Handshake(
|
|
{ ...hello, capabilities: { framing: [2], payloadKinds: ['binary', 'text'] } },
|
|
ready
|
|
)
|
|
).toBeNull()
|
|
})
|
|
|
|
it('requires relayHostId only for relay context', () => {
|
|
const { hello, ready } = createMobileE2EEV2Fixture()
|
|
const { relayHostId: _relayHostId, ...relayWithoutId } = hello.context
|
|
expect(
|
|
validateMobileE2EEV2Handshake(
|
|
{ ...hello, context: relayWithoutId },
|
|
{ ...ready, context: relayWithoutId }
|
|
)
|
|
).toBeNull()
|
|
|
|
const directContext = {
|
|
protocol: 'orca-mobile-e2ee' as const,
|
|
initiator: 'mobile' as const,
|
|
responder: 'desktop' as const,
|
|
transport: 'direct' as const
|
|
}
|
|
expect(
|
|
validateMobileE2EEV2Handshake(
|
|
{ ...hello, context: directContext },
|
|
{ ...ready, context: directContext }
|
|
)
|
|
).not.toBeNull()
|
|
})
|
|
|
|
it('locks the canonical length-prefixed transcript bytes', () => {
|
|
const { hello, ready } = createMobileE2EEV2Fixture()
|
|
const handshake = validateMobileE2EEV2Handshake(hello, ready)
|
|
expect(handshake).not.toBeNull()
|
|
const transcript = encodeMobileE2EEV2Transcript(handshake!)
|
|
|
|
expect(transcript).toHaveLength(MOBILE_E2EE_V2_VECTOR.transcriptLength)
|
|
expect(createHash('sha256').update(transcript).digest('hex')).toBe(
|
|
MOBILE_E2EE_V2_VECTOR.transcriptHashHex
|
|
)
|
|
})
|
|
})
|