TTP -> MTP, A lot of other stuff #21
4 changed files with 264 additions and 53 deletions
(wip): call migration stuff
commit
27f08e92e5
|
|
@ -4,7 +4,13 @@ import { useLocation, useNavigate } from "@tanstack/react-router";
|
||||||
import { useMTP } from "@tensamin/mtp";
|
import { useMTP } from "@tensamin/mtp";
|
||||||
import { log, toast } from "@tensamin/shared/log";
|
import { log, toast } from "@tensamin/shared/log";
|
||||||
import { mtp } from "@tensamin/shared/data";
|
import { mtp } from "@tensamin/shared/data";
|
||||||
import { useCrypto } from "@tensamin/crypto/context";
|
import { bytesToBase64 } from "mtp";
|
||||||
|
import {
|
||||||
|
deriveCallSecretId,
|
||||||
|
kemPublicKeyFromPublicKeyBundle,
|
||||||
|
unwrapCallSecret,
|
||||||
|
wrapCallSecret,
|
||||||
|
} from "@tensamin/crypto/callSecret";
|
||||||
import { useStorage } from "@tensamin/storage/context";
|
import { useStorage } from "@tensamin/storage/context";
|
||||||
import { useSession } from "@tensamin/storage/session";
|
import { useSession } from "@tensamin/storage/session";
|
||||||
import { useUser } from "@tensamin/user/context";
|
import { useUser } from "@tensamin/user/context";
|
||||||
|
|
@ -46,9 +52,19 @@ setLogExtension(
|
||||||
|
|
||||||
type CallState = "closed" | "closing" | "connecting" | "open" | "encrypting";
|
type CallState = "closed" | "closing" | "connecting" | "open" | "encrypting";
|
||||||
type CallView = "preview" | "focused" | "grid";
|
type CallView = "preview" | "focused" | "grid";
|
||||||
|
type ProtocolCallSecret = NonNullable<
|
||||||
|
z.infer<typeof mtp.CallInvite.response>["CallSecret"]
|
||||||
|
>;
|
||||||
|
type WrappedCallSecret = {
|
||||||
|
secretId: string;
|
||||||
|
versionNumber: number;
|
||||||
|
encryptedSecret: Uint8Array;
|
||||||
|
kemCiphertext: Uint8Array;
|
||||||
|
wrappingScheme: string;
|
||||||
|
};
|
||||||
type IncomingCallInvite = {
|
type IncomingCallInvite = {
|
||||||
callId: string;
|
callId: string;
|
||||||
callSecret: string;
|
callSecret: WrappedCallSecret;
|
||||||
senderId: number;
|
senderId: number;
|
||||||
};
|
};
|
||||||
type CurrentCallData =
|
type CurrentCallData =
|
||||||
|
|
@ -62,13 +78,6 @@ type SendFn = (
|
||||||
type: string,
|
type: string,
|
||||||
data: Record<string, unknown>,
|
data: Record<string, unknown>,
|
||||||
) => Promise<{ data: unknown }>;
|
) => Promise<{ data: unknown }>;
|
||||||
type GetSharedSecretFn = (
|
|
||||||
privateKey: unknown,
|
|
||||||
ownPublicKey: string,
|
|
||||||
remotePublicKey: string,
|
|
||||||
) => Promise<string>;
|
|
||||||
type DecryptTextFn = (sharedSecret: string, text: string) => Promise<string>;
|
|
||||||
type EncryptTextFn = (sharedSecret: string, text: string) => Promise<string>;
|
|
||||||
type LoadFn = (key: string) => Promise<unknown>;
|
type LoadFn = (key: string) => Promise<unknown>;
|
||||||
type GetUserFn = (userId: number) => Promise<{ PublicKey: string }>;
|
type GetUserFn = (userId: number) => Promise<{ PublicKey: string }>;
|
||||||
type RemoteVideoTrackSelector = Track.Kind | Track.Source;
|
type RemoteVideoTrackSelector = Track.Kind | Track.Source;
|
||||||
|
|
@ -76,9 +85,6 @@ type RemoteVideoTrackSelector = Track.Kind | Track.Source;
|
||||||
type Runtime = {
|
type Runtime = {
|
||||||
navigate: NavigateFn;
|
navigate: NavigateFn;
|
||||||
send: SendFn;
|
send: SendFn;
|
||||||
getSharedSecret: GetSharedSecretFn;
|
|
||||||
decryptText: DecryptTextFn;
|
|
||||||
encryptText: EncryptTextFn;
|
|
||||||
load: LoadFn;
|
load: LoadFn;
|
||||||
getUser: GetUserFn;
|
getUser: GetUserFn;
|
||||||
};
|
};
|
||||||
|
|
@ -103,6 +109,7 @@ type CallStore = {
|
||||||
pendingWatchedParticipantIds: number[];
|
pendingWatchedParticipantIds: number[];
|
||||||
activeScreenShareParticipantIds: number[];
|
activeScreenShareParticipantIds: number[];
|
||||||
isEncrypted: boolean;
|
isEncrypted: boolean;
|
||||||
|
ownCallSecretInvitePending: boolean;
|
||||||
callIsFullscreen: boolean;
|
callIsFullscreen: boolean;
|
||||||
callIsPopout: boolean;
|
callIsPopout: boolean;
|
||||||
layoutVersion: number;
|
layoutVersion: number;
|
||||||
|
|
@ -148,11 +155,46 @@ export function getRoom(): Room {
|
||||||
|
|
||||||
const remoteAudioElements = new Map<string, HTMLMediaElement>();
|
const remoteAudioElements = new Map<string, HTMLMediaElement>();
|
||||||
|
|
||||||
|
const CALL_SECRET_VERSION = 1;
|
||||||
const SCREEN_SHARE_PREVIEW_MAX_WIDTH = 320;
|
const SCREEN_SHARE_PREVIEW_MAX_WIDTH = 320;
|
||||||
const SCREEN_SHARE_PREVIEW_MAX_HEIGHT = 180;
|
const SCREEN_SHARE_PREVIEW_MAX_HEIGHT = 180;
|
||||||
const SCREEN_SHARE_PREVIEW_QUALITY = 0.7;
|
const SCREEN_SHARE_PREVIEW_QUALITY = 0.7;
|
||||||
const SCREEN_SHARE_PREVIEW_TIMEOUT_MS = 5000;
|
const SCREEN_SHARE_PREVIEW_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
|
function protocolBytes(bytes: Uint8Array): Uint8Array<ArrayBuffer> {
|
||||||
|
return new Uint8Array(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWrappedCallSecret(
|
||||||
|
callSecret: WrappedCallSecret | ProtocolCallSecret,
|
||||||
|
): WrappedCallSecret {
|
||||||
|
if ("secretId" in callSecret) {
|
||||||
|
return callSecret;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
secretId: callSecret.SecretId,
|
||||||
|
versionNumber: callSecret.VersionNumber,
|
||||||
|
encryptedSecret: callSecret.EncryptedSecret,
|
||||||
|
kemCiphertext: callSecret.KemCiphertext,
|
||||||
|
wrappingScheme: callSecret.WrappingScheme,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function protocolCallSecret(callSecret: WrappedCallSecret): ProtocolCallSecret {
|
||||||
|
return {
|
||||||
|
SecretId: callSecret.secretId,
|
||||||
|
VersionNumber: callSecret.versionNumber,
|
||||||
|
EncryptedSecret: protocolBytes(callSecret.encryptedSecret),
|
||||||
|
KemCiphertext: protocolBytes(callSecret.kemCiphertext),
|
||||||
|
WrappingScheme: callSecret.wrappingScheme,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomCallSecret(): string {
|
||||||
|
return bytesToBase64(globalThis.crypto.getRandomValues(new Uint8Array(32)));
|
||||||
|
}
|
||||||
|
|
||||||
// audio helpers
|
// audio helpers
|
||||||
function attachRemoteAudio(trackSid: string, element: HTMLMediaElement) {
|
function attachRemoteAudio(trackSid: string, element: HTMLMediaElement) {
|
||||||
const existingElement = remoteAudioElements.get(trackSid);
|
const existingElement = remoteAudioElements.get(trackSid);
|
||||||
|
|
@ -625,28 +667,28 @@ export async function sendCallInvite(userId: number) {
|
||||||
throw new Error("Cannot send call invite without an active call.");
|
throw new Error("Cannot send call invite without an active call.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const ownUserId = (await runtime.load("user_id")) as number;
|
|
||||||
const privateKey = await runtime.load("mtp_keyring");
|
|
||||||
const ownPublicKey = await runtime
|
|
||||||
.getUser(ownUserId)
|
|
||||||
.then((data) => data.PublicKey);
|
|
||||||
const remotePublicKey = await runtime
|
const remotePublicKey = await runtime
|
||||||
.getUser(userId)
|
.getUser(userId)
|
||||||
.then((data) => data.PublicKey);
|
.then((data) => data.PublicKey);
|
||||||
const sharedSecret = await runtime.getSharedSecret(
|
const secretId = deriveCallSecretId(callId);
|
||||||
privateKey,
|
const wrapped = await wrapCallSecret({
|
||||||
ownPublicKey,
|
|
||||||
remotePublicKey,
|
|
||||||
);
|
|
||||||
const encryptedCallSecret = await runtime.encryptText(
|
|
||||||
sharedSecret,
|
|
||||||
callSecret,
|
callSecret,
|
||||||
);
|
recipientKemPublicKey: kemPublicKeyFromPublicKeyBundle(remotePublicKey),
|
||||||
|
callId,
|
||||||
|
secretId,
|
||||||
|
version: CALL_SECRET_VERSION,
|
||||||
|
});
|
||||||
|
|
||||||
await runtime.send("call_invite", {
|
await runtime.send("call_invite", {
|
||||||
ReceiverId: userId,
|
ReceiverId: userId,
|
||||||
CallId: callId,
|
CallId: callId,
|
||||||
CallSecret: encryptedCallSecret,
|
CallSecret: {
|
||||||
|
SecretId: secretId,
|
||||||
|
VersionNumber: CALL_SECRET_VERSION,
|
||||||
|
EncryptedSecret: protocolBytes(wrapped.encryptedSecret),
|
||||||
|
KemCiphertext: protocolBytes(wrapped.kemCiphertext),
|
||||||
|
WrappingScheme: wrapped.wrappingScheme,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -856,6 +898,7 @@ export async function disconnect() {
|
||||||
watchedStreamParticipantIds: [],
|
watchedStreamParticipantIds: [],
|
||||||
pendingWatchedParticipantIds: [],
|
pendingWatchedParticipantIds: [],
|
||||||
activeScreenShareParticipantIds: [],
|
activeScreenShareParticipantIds: [],
|
||||||
|
ownCallSecretInvitePending: false,
|
||||||
callIsFullscreen: false,
|
callIsFullscreen: false,
|
||||||
lastFocusedParticipantId: null,
|
lastFocusedParticipantId: null,
|
||||||
});
|
});
|
||||||
|
|
@ -877,7 +920,7 @@ export async function disconnect() {
|
||||||
// Prepare encryption and join or create a call with another user.
|
// Prepare encryption and join or create a call with another user.
|
||||||
export async function joinCall(
|
export async function joinCall(
|
||||||
userId: number,
|
userId: number,
|
||||||
callSecret?: string,
|
callSecret?: WrappedCallSecret | ProtocolCallSecret,
|
||||||
existingCallId?: string,
|
existingCallId?: string,
|
||||||
sendInvite = true,
|
sendInvite = true,
|
||||||
) {
|
) {
|
||||||
|
|
@ -889,24 +932,29 @@ export async function joinCall(
|
||||||
}
|
}
|
||||||
|
|
||||||
log(2, "call", "purple", "Call creation initialised");
|
log(2, "call", "purple", "Call creation initialised");
|
||||||
|
const isNewCall = !callSecret && !existingCallId;
|
||||||
useCall.setState({
|
useCall.setState({
|
||||||
state: "encrypting",
|
state: "encrypting",
|
||||||
invitedUserId: sendInvite && !existingCallId ? userId : null,
|
invitedUserId: sendInvite && !existingCallId ? userId : null,
|
||||||
|
ownCallSecretInvitePending: isNewCall,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (callSecret) {
|
if (callSecret) {
|
||||||
try {
|
try {
|
||||||
const sharedSecret = await runtime.getSharedSecret(
|
if (!existingCallId) {
|
||||||
await runtime.load("mtp_keyring"),
|
throw new Error("Cannot unwrap call secret without a call id");
|
||||||
await runtime
|
}
|
||||||
.getUser((await runtime.load("user_id")) as number)
|
|
||||||
.then((res) => res.PublicKey),
|
const wrappedCallSecret = normalizeWrappedCallSecret(callSecret);
|
||||||
await runtime.getUser(userId).then((res) => res.PublicKey),
|
const decryptedSecret = await unwrapCallSecret({
|
||||||
);
|
encryptedSecret: wrappedCallSecret.encryptedSecret,
|
||||||
const decryptedSecret = await runtime.decryptText(
|
kemCiphertext: wrappedCallSecret.kemCiphertext,
|
||||||
sharedSecret,
|
keyring: String(await runtime.load("mtp_keyring")),
|
||||||
callSecret,
|
callId: existingCallId,
|
||||||
);
|
secretId: wrappedCallSecret.secretId,
|
||||||
|
version: wrappedCallSecret.versionNumber,
|
||||||
|
wrappingScheme: wrappedCallSecret.wrappingScheme,
|
||||||
|
});
|
||||||
|
|
||||||
await getKeyProvider().setKey(decryptedSecret);
|
await getKeyProvider().setKey(decryptedSecret);
|
||||||
await getRoom().setE2EEEnabled(true);
|
await getRoom().setE2EEEnabled(true);
|
||||||
|
|
@ -917,7 +965,7 @@ export async function joinCall(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const random = crypto.randomUUID();
|
const random = randomCallSecret();
|
||||||
|
|
||||||
await getKeyProvider().setKey(random);
|
await getKeyProvider().setKey(random);
|
||||||
await getRoom().setE2EEEnabled(true);
|
await getRoom().setE2EEEnabled(true);
|
||||||
|
|
@ -1075,6 +1123,7 @@ export const useCall = create<CallStore>(() => ({
|
||||||
pendingWatchedParticipantIds: [],
|
pendingWatchedParticipantIds: [],
|
||||||
activeScreenShareParticipantIds: [],
|
activeScreenShareParticipantIds: [],
|
||||||
isEncrypted: false,
|
isEncrypted: false,
|
||||||
|
ownCallSecretInvitePending: false,
|
||||||
callIsFullscreen: false,
|
callIsFullscreen: false,
|
||||||
callIsPopout: false,
|
callIsPopout: false,
|
||||||
layoutVersion: 0,
|
layoutVersion: 0,
|
||||||
|
|
@ -1088,7 +1137,6 @@ export function useInitializeCall() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { send, subscribePush } = useMTP();
|
const { send, subscribePush } = useMTP();
|
||||||
const { getSharedSecret, decryptText, encryptText } = useCrypto();
|
|
||||||
const { load } = useStorage();
|
const { load } = useStorage();
|
||||||
const { insertCall } = useSession();
|
const { insertCall } = useSession();
|
||||||
const { get } = useUser();
|
const { get } = useUser();
|
||||||
|
|
@ -1116,16 +1164,13 @@ export function useInitializeCall() {
|
||||||
setCallRuntime({
|
setCallRuntime({
|
||||||
navigate,
|
navigate,
|
||||||
send: send as SendFn,
|
send: send as SendFn,
|
||||||
getSharedSecret: getSharedSecret as GetSharedSecretFn,
|
|
||||||
decryptText: decryptText as DecryptTextFn,
|
|
||||||
encryptText: encryptText as EncryptTextFn,
|
|
||||||
load: load as LoadFn,
|
load: load as LoadFn,
|
||||||
getUser: get as GetUserFn,
|
getUser: get as GetUserFn,
|
||||||
});
|
});
|
||||||
}, [decryptText, encryptText, get, getSharedSecret, load, navigate, send]);
|
}, [get, load, navigate, send]);
|
||||||
|
|
||||||
const showCallingScreen = useCallback(
|
const showCallingScreen = useCallback(
|
||||||
(callId: string, callSecret: string, senderId: number) => {
|
(callId: string, callSecret: WrappedCallSecret, senderId: number) => {
|
||||||
useCall.setState({
|
useCall.setState({
|
||||||
incomingCallInvite: { callId, callSecret, senderId },
|
incomingCallInvite: { callId, callSecret, senderId },
|
||||||
});
|
});
|
||||||
|
|
@ -1151,7 +1196,7 @@ export function useInitializeCall() {
|
||||||
|
|
||||||
insertCall({
|
insertCall({
|
||||||
CallId: invite.callId,
|
CallId: invite.callId,
|
||||||
CallSecret: invite.callSecret,
|
CallSecret: protocolCallSecret(invite.callSecret),
|
||||||
CallMembers: [invite.senderId],
|
CallMembers: [invite.senderId],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -1182,13 +1227,21 @@ export function useInitializeCall() {
|
||||||
|
|
||||||
const { CallId, CallSecret, SenderId } = message.data as {
|
const { CallId, CallSecret, SenderId } = message.data as {
|
||||||
CallId: string;
|
CallId: string;
|
||||||
CallSecret: string;
|
CallSecret: ProtocolCallSecret;
|
||||||
SenderId: number;
|
SenderId: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
showCallingScreen(CallId, CallSecret, SenderId);
|
if (SenderId === Number(await load("user_id"))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showCallingScreen(
|
||||||
|
CallId,
|
||||||
|
normalizeWrappedCallSecret(CallSecret),
|
||||||
|
SenderId,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}, [subscribePush, showCallingScreen]);
|
}, [load, subscribePush, showCallingScreen]);
|
||||||
|
|
||||||
// get callId from url
|
// get callId from url
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -1289,6 +1342,23 @@ export function useInitializeCall() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const invitedUserId = useCall.getState().invitedUserId;
|
const invitedUserId = useCall.getState().invitedUserId;
|
||||||
|
const ownCallSecretInvitePending =
|
||||||
|
useCall.getState().ownCallSecretInvitePending;
|
||||||
|
|
||||||
|
if (ownCallSecretInvitePending) {
|
||||||
|
useCall.setState({ ownCallSecretInvitePending: false });
|
||||||
|
void load("user_id")
|
||||||
|
.then((ownUserId) => sendCallInvite(Number(ownUserId)))
|
||||||
|
.catch((error) => {
|
||||||
|
log(
|
||||||
|
1,
|
||||||
|
"call",
|
||||||
|
"red",
|
||||||
|
"Failed to send own call secret invite",
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (invitedUserId != null) {
|
if (invitedUserId != null) {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
"./context": "./src/context.tsx",
|
"./context": "./src/context.tsx",
|
||||||
"./chatSecret": "./src/chatSecret.ts"
|
"./chatSecret": "./src/chatSecret.ts",
|
||||||
|
"./callSecret": "./src/callSecret.ts"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "pnpm exec prettier --write .",
|
"format": "pnpm exec prettier --write .",
|
||||||
|
|
|
||||||
112
packages/crypto/src/callSecret.ts
Normal file
112
packages/crypto/src/callSecret.ts
Normal file
|
|
@ -0,0 +1,112 @@
|
||||||
|
import { crypto } from "mtp";
|
||||||
|
|
||||||
|
const textEncoder = new TextEncoder();
|
||||||
|
const textDecoder = new TextDecoder();
|
||||||
|
|
||||||
|
export const CALL_SECRET_WRAPPING_SCHEME =
|
||||||
|
"mtp-call-secret-kem-chacha20poly1305-hkdf-sha256-v1";
|
||||||
|
|
||||||
|
const CALL_SECRET_SALT = textEncoder.encode("tensamin-call-secret-v1");
|
||||||
|
|
||||||
|
export function deriveCallSecretId(callId: string): string {
|
||||||
|
return `call:${callId}:main`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ownKemPublicKeyFromKeyring(keyring: string): Uint8Array {
|
||||||
|
return crypto.keyringToKeys(keyring).kemPublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function kemPublicKeyFromPublicKeyBundle(publicKey: string): Uint8Array {
|
||||||
|
return crypto.publicKeyBundleToKeys(publicKey).kemPublicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function wrapCallSecret(args: {
|
||||||
|
callSecret: string;
|
||||||
|
recipientKemPublicKey: Uint8Array;
|
||||||
|
callId: string;
|
||||||
|
secretId: string;
|
||||||
|
version: number;
|
||||||
|
}): Promise<{
|
||||||
|
encryptedSecret: Uint8Array;
|
||||||
|
kemCiphertext: Uint8Array;
|
||||||
|
wrappingScheme: string;
|
||||||
|
}> {
|
||||||
|
const enc = crypto.encapsulate(args.recipientKemPublicKey);
|
||||||
|
try {
|
||||||
|
const wrappingKey = deriveWrappingKey({
|
||||||
|
sharedSecret: enc.shared_secret,
|
||||||
|
callId: args.callId,
|
||||||
|
secretId: args.secretId,
|
||||||
|
version: args.version,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
encryptedSecret: await crypto.encrypt(
|
||||||
|
wrappingKey,
|
||||||
|
textEncoder.encode(args.callSecret),
|
||||||
|
),
|
||||||
|
kemCiphertext: enc.ciphertext,
|
||||||
|
wrappingScheme: CALL_SECRET_WRAPPING_SCHEME,
|
||||||
|
};
|
||||||
|
} finally {
|
||||||
|
wrappingKey.fill(0);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
enc.shared_secret.fill(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function unwrapCallSecret(args: {
|
||||||
|
encryptedSecret: Uint8Array;
|
||||||
|
kemCiphertext: Uint8Array;
|
||||||
|
keyring: string;
|
||||||
|
callId: string;
|
||||||
|
secretId: string;
|
||||||
|
version: number;
|
||||||
|
wrappingScheme: string;
|
||||||
|
}): Promise<string> {
|
||||||
|
if (args.wrappingScheme !== CALL_SECRET_WRAPPING_SCHEME) {
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported call secret wrapping scheme: ${args.wrappingScheme}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ownKeys = crypto.keyringToKeys(args.keyring);
|
||||||
|
const sharedSecret = crypto.decapsulate(
|
||||||
|
ownKeys.kemSecretKey,
|
||||||
|
args.kemCiphertext,
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const wrappingKey = deriveWrappingKey({
|
||||||
|
sharedSecret,
|
||||||
|
callId: args.callId,
|
||||||
|
secretId: args.secretId,
|
||||||
|
version: args.version,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
return textDecoder.decode(
|
||||||
|
await crypto.decrypt(wrappingKey, args.encryptedSecret),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
wrappingKey.fill(0);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
sharedSecret.fill(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function deriveWrappingKey(args: {
|
||||||
|
sharedSecret: Uint8Array;
|
||||||
|
callId: string;
|
||||||
|
secretId: string;
|
||||||
|
version: number;
|
||||||
|
}): Uint8Array {
|
||||||
|
return crypto.deriveEncryptionKey(
|
||||||
|
args.sharedSecret,
|
||||||
|
CALL_SECRET_SALT,
|
||||||
|
textEncoder.encode(`${args.callId}:${args.secretId}:${args.version}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,18 @@ const bytesLike = z.union([
|
||||||
|
|
||||||
const protocolBytes = z.instanceof(Uint8Array);
|
const protocolBytes = z.instanceof(Uint8Array);
|
||||||
|
|
||||||
|
function bytesFromProtocol(value: z.infer<typeof bytesLike>): Uint8Array {
|
||||||
|
if (value instanceof Uint8Array) return value;
|
||||||
|
if (Array.isArray(value)) return new Uint8Array(value);
|
||||||
|
|
||||||
|
const bin = atob(value);
|
||||||
|
const out = new Uint8Array(bin.length);
|
||||||
|
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
const protocolBytesResponse = bytesLike.transform(bytesFromProtocol);
|
||||||
|
|
||||||
const chatSecretResponse = z.object({
|
const chatSecretResponse = z.object({
|
||||||
UserId: z.string(),
|
UserId: z.string(),
|
||||||
ChatId: z.string(),
|
ChatId: z.string(),
|
||||||
|
|
@ -38,6 +50,22 @@ const chatSecretRecipient = z.object({
|
||||||
KemCiphertext: protocolBytes,
|
KemCiphertext: protocolBytes,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const callSecretEnvelopeResponse = z.object({
|
||||||
|
SecretId: z.string(),
|
||||||
|
VersionNumber: z.number(),
|
||||||
|
EncryptedSecret: protocolBytesResponse,
|
||||||
|
KemCiphertext: protocolBytesResponse,
|
||||||
|
WrappingScheme: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const callSecretEnvelopeRequest = z.object({
|
||||||
|
SecretId: z.string(),
|
||||||
|
VersionNumber: z.number(),
|
||||||
|
EncryptedSecret: protocolBytes,
|
||||||
|
KemCiphertext: protocolBytes,
|
||||||
|
WrappingScheme: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
export const Message = z.object({
|
export const Message = z.object({
|
||||||
NotEncrypted: z.boolean().optional(),
|
NotEncrypted: z.boolean().optional(),
|
||||||
SentBySelf: z.boolean().optional(),
|
SentBySelf: z.boolean().optional(),
|
||||||
|
|
@ -85,7 +113,7 @@ const authPayload = z.object({
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
CallId: z.string(),
|
CallId: z.string(),
|
||||||
CallSecret: z.base64().optional(),
|
CallSecret: callSecretEnvelopeResponse.optional(),
|
||||||
CallMembers: z.array(z.number()),
|
CallMembers: z.array(z.number()),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
@ -262,12 +290,12 @@ export const mtp = {
|
||||||
CallInvite: {
|
CallInvite: {
|
||||||
request: z.object({
|
request: z.object({
|
||||||
CallId: z.string(),
|
CallId: z.string(),
|
||||||
CallSecret: z.base64(),
|
CallSecret: callSecretEnvelopeRequest,
|
||||||
ReceiverId: z.number(),
|
ReceiverId: z.number(),
|
||||||
}),
|
}),
|
||||||
response: z.object({
|
response: z.object({
|
||||||
CallId: z.string().optional(),
|
CallId: z.string().optional(),
|
||||||
CallSecret: z.base64().optional(),
|
CallSecret: callSecretEnvelopeResponse.optional(),
|
||||||
SenderId: z.number().optional(),
|
SenderId: z.number().optional(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue