mirror of
https://github.com/signalapp/libsignal.git
synced 2026-04-25 17:25:18 +02:00
230 lines
6.5 KiB
TypeScript
230 lines
6.5 KiB
TypeScript
//
|
|
// Copyright 2025 Signal Messenger, LLC.
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import { config, expect, use } from 'chai';
|
|
import chaiAsPromised from 'chai-as-promised';
|
|
import { Buffer } from 'node:buffer';
|
|
|
|
import * as Native from '../Native.js';
|
|
import * as util from './util.js';
|
|
import {
|
|
UnauthenticatedChatConnection,
|
|
Environment,
|
|
Net,
|
|
TokioAsyncContext,
|
|
} from '../net.js';
|
|
import { Aci } from '../Address.js';
|
|
import { PublicKey } from '../EcKeys.js';
|
|
import {
|
|
ErrorCode,
|
|
KeyTransparencyError,
|
|
KeyTransparencyVerificationFailed,
|
|
LibSignalErrorBase,
|
|
} from '../Errors.js';
|
|
import * as KT from '../net/KeyTransparency.js';
|
|
|
|
use(chaiAsPromised);
|
|
|
|
util.initLogger();
|
|
config.truncateThreshold = 0;
|
|
|
|
let chat: UnauthenticatedChatConnection;
|
|
let kt: KT.Client;
|
|
|
|
const userAgent = 'libsignal-kt-test';
|
|
const testAci = Aci.fromUuid('90c979fd-eab4-4a08-b6da-69dedeab9b29');
|
|
const testIdentityKey = PublicKey.deserialize(
|
|
Buffer.from(
|
|
'05cdcbb178067f0ddfd258bb21d006e0aa9c7ab132d9fb5e8b027de07d947f9d0c',
|
|
'hex'
|
|
)
|
|
);
|
|
const testE164 = '+18005550100';
|
|
const testUnidentifiedAccessKey = Buffer.from(
|
|
'108d84b71be307bdf101e380a1d7f2a2',
|
|
'hex'
|
|
);
|
|
|
|
const testUsernameHash = Buffer.from(
|
|
'dc711808c2cf66d5e6a33ce41f27d69d942d2e1ff4db22d39b42d2eff8d09746',
|
|
'hex'
|
|
);
|
|
|
|
const testRequest = {
|
|
aciInfo: { aci: testAci, identityKey: testIdentityKey },
|
|
e164Info: {
|
|
e164: testE164,
|
|
unidentifiedAccessKey: testUnidentifiedAccessKey,
|
|
},
|
|
usernameHash: testUsernameHash,
|
|
mode: 'contact' as const,
|
|
};
|
|
|
|
describe('KeyTransparency bridging', () => {
|
|
it('can bridge non fatal error', () => {
|
|
expect(() => Native.TESTING_KeyTransNonFatalVerificationFailure())
|
|
.to.throw(LibSignalErrorBase)
|
|
.that.satisfies(
|
|
(err: KeyTransparencyError) =>
|
|
err.code === ErrorCode.KeyTransparencyError
|
|
);
|
|
});
|
|
|
|
it('can bridge fatal error', () => {
|
|
expect(() => Native.TESTING_KeyTransFatalVerificationFailure())
|
|
.to.throw(LibSignalErrorBase)
|
|
.that.satisfies(
|
|
(err: KeyTransparencyVerificationFailed) =>
|
|
err.code === ErrorCode.KeyTransparencyVerificationFailed
|
|
);
|
|
});
|
|
|
|
it('can bridge chat send error', () => {
|
|
expect(() => Native.TESTING_KeyTransChatSendError())
|
|
.to.throw(LibSignalErrorBase)
|
|
.that.satisfies(
|
|
(err: LibSignalErrorBase) => err.code === ErrorCode.IoError
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('KeyTransparency network errors', () => {
|
|
it('can bridge network errors', async () => {
|
|
async function run(statusCode: number, headers: string[] = []) {
|
|
const tokio = new TokioAsyncContext(Native.TokioAsyncContext_new());
|
|
const [unauth, remote] = UnauthenticatedChatConnection.fakeConnect(
|
|
tokio,
|
|
{
|
|
onConnectionInterrupted: () => {},
|
|
onIncomingMessage: () => {},
|
|
onReceivedAlerts: () => {},
|
|
onQueueEmpty: () => {},
|
|
}
|
|
);
|
|
const client = new KT.ClientImpl(
|
|
tokio,
|
|
unauth._chatService,
|
|
Environment.Staging
|
|
);
|
|
const promise = client.check(testRequest, new InMemoryKtStore(), {});
|
|
|
|
const request = await remote.assertReceiveIncomingRequest();
|
|
|
|
remote.sendReplyTo(request, {
|
|
status: statusCode,
|
|
headers: headers,
|
|
});
|
|
return promise;
|
|
}
|
|
|
|
// 429 without a retry-after header is a generic error
|
|
await expect(run(429)).to.be.rejected.and.eventually.have.property(
|
|
'code',
|
|
ErrorCode.IoError
|
|
);
|
|
await expect(
|
|
run(429, ['retry-after: 42'])
|
|
).to.be.rejected.and.eventually.have.property(
|
|
'code',
|
|
ErrorCode.RateLimitedError
|
|
);
|
|
await expect(run(500)).to.be.rejected.and.eventually.have.property(
|
|
'code',
|
|
ErrorCode.IoError
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('KeyTransparency Integration', function (this: Mocha.Suite) {
|
|
// Avoid timing out due to slow network or KT environment
|
|
this.timeout(5000);
|
|
|
|
before(() => {
|
|
const ignoreKtTests =
|
|
typeof process.env.LIBSIGNAL_TESTING_IGNORE_KT_TESTS !== 'undefined';
|
|
if (!process.env.LIBSIGNAL_TESTING_RUN_NONHERMETIC_TESTS || ignoreKtTests) {
|
|
this.ctx.skip();
|
|
}
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
const network = new Net({
|
|
localTestServer: false,
|
|
env: Environment.Staging,
|
|
userAgent,
|
|
});
|
|
chat = await network.connectUnauthenticatedChat({
|
|
onConnectionInterrupted: (_cause) => {},
|
|
});
|
|
kt = chat.keyTransparencyClient();
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await chat.disconnect();
|
|
});
|
|
|
|
it('check account (initial search)', async () => {
|
|
const store = new InMemoryKtStore();
|
|
await kt.check(testRequest, store, {});
|
|
});
|
|
|
|
it('check account (monitor)', async () => {
|
|
const store = new InMemoryKtStore();
|
|
|
|
// Initial search first to populate the store with account data
|
|
await kt.check(testRequest, store, {});
|
|
|
|
const accountDataHistory = store.storage.get(testAci) ?? null;
|
|
if (accountDataHistory === null) {
|
|
expect.fail('accountDataHistory is null');
|
|
}
|
|
|
|
expect(accountDataHistory.length).to.equal(1);
|
|
expect(store.distinguished.length).to.equal(1);
|
|
|
|
await kt.check(testRequest, store, {});
|
|
expect(accountDataHistory.length).to.equal(2);
|
|
// Distinguished tree should not have been updated
|
|
expect(store.distinguished.length).to.equal(1);
|
|
});
|
|
});
|
|
|
|
class InMemoryKtStore implements KT.Store {
|
|
storage: Map<Readonly<Aci>, Array<Readonly<Uint8Array<ArrayBuffer>>>>;
|
|
distinguished: Array<Readonly<Uint8Array<ArrayBuffer>>>;
|
|
|
|
constructor() {
|
|
this.storage = new Map<Aci, Array<Readonly<Uint8Array<ArrayBuffer>>>>();
|
|
this.distinguished = new Array<Uint8Array<ArrayBuffer>>();
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
async getLastDistinguishedTreeHead(): Promise<Uint8Array<ArrayBuffer> | null> {
|
|
return this.distinguished.at(-1) ?? null;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
async setLastDistinguishedTreeHead(
|
|
bytes: Readonly<Uint8Array<ArrayBuffer>> | null
|
|
) {
|
|
if (bytes !== null) {
|
|
this.distinguished.push(bytes);
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
async getAccountData(aci: Aci): Promise<Uint8Array<ArrayBuffer> | null> {
|
|
const allVersions = this.storage.get(aci) ?? [];
|
|
return allVersions.at(-1) ?? null;
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/require-await
|
|
async setAccountData(aci: Aci, bytes: Readonly<Uint8Array<ArrayBuffer>>) {
|
|
const allVersions = this.storage.get(aci) ?? [];
|
|
allVersions.push(bytes);
|
|
this.storage.set(aci, allVersions);
|
|
}
|
|
}
|