Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions flottform/forms/src/default-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@ export const createDefaultFlottformComponent = ({
onSuccessText
}: {
flottformApi: string;
createClientUrl: (params: { endpointId: string }) => Promise<string>;
createClientUrl: (params: {
endpointId: string;
encryptionKey: string;
optionalData?: object;
}) => Promise<string>;
inputField: HTMLInputElement;
id?: string;
additionalItemClasses?: string;
Expand Down Expand Up @@ -147,7 +151,11 @@ export const createDefaultFlottformComponent = ({
onSuccessText
}: {
flottformApi: string;
createClientUrl: (params: { endpointId: string }) => Promise<string>;
createClientUrl: (params: {
endpointId: string;
encryptionKey: string;
optionalData?: object;
}) => Promise<string>;
inputField?: HTMLInputElement | HTMLTextAreaElement;
id?: string;
additionalItemClasses?: string;
Expand Down
106 changes: 106 additions & 0 deletions flottform/forms/src/encryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
export async function generateKey(): Promise<CryptoKey> {
return await crypto.subtle.generateKey(
{
name: 'AES-GCM',
length: 256
},
true, // extractable
['encrypt', 'decrypt']
);
}

export async function cryptoKeyToEncryptionKey(key: CryptoKey) {
// CryptoKey --> Exported bytes of the cryptoKey (i.e. the encryption key)
return (await crypto.subtle.exportKey('jwk', key)).k;
}

export async function encryptionKeyToCryptoKey(encryptionKey: string) {
// Create a complete JWK object structure
const jwk = {
kty: 'oct',
k: encryptionKey,
alg: 'A256GCM',
ext: true,
key_ops: ['encrypt', 'decrypt']
};

// Import the complete JWK
return await crypto.subtle.importKey(
'jwk',
jwk,
{
name: 'AES-GCM',
length: 256
},
true, // extractable
['encrypt', 'decrypt']
);
}

export async function encrypt(plaintext: string, cryptoKey: CryptoKey): Promise<string> {
const data = plaintextToTypedArray(plaintext);
const iv = getInitializationVector();

const encryptedData = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv
},
cryptoKey,
data
);

// Prepend the cyphertext with the initialization vector.
const combinedData = new Uint8Array(iv.length + encryptedData.byteLength);
combinedData.set(iv, 0);
combinedData.set(new Uint8Array(encryptedData), iv.length);

return typedArrayToBase64(combinedData);
}

export async function decrypt(ciphertext: string, cryptoKey: CryptoKey) {
const combinedData = base64ToTypedArray(ciphertext);

// Step 2: Extract IV and ciphertext
const iv = combinedData.slice(0, 12);
const data = combinedData.slice(12);

const decryptedData = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv
},
cryptoKey,
data
);

return typedArrayToPlaintext(new Uint8Array(decryptedData));
}

function plaintextToTypedArray(plainText: string): Uint8Array {
// Then encode to Uint8Array
const encoder = new TextEncoder();
return encoder.encode(plainText);
}

function getInitializationVector(): Uint8Array {
return crypto.getRandomValues(new Uint8Array(12));
}

function typedArrayToBase64(typedArray: Uint8Array): string {
// Uint8Array --> Base64
return btoa(String.fromCharCode(...new Uint8Array(typedArray)));
}

function base64ToTypedArray(messageAsBase64: string): Uint8Array {
// Base64 --> Uint8Array
const binaryString = atob(messageAsBase64);
return Uint8Array.from(binaryString, (char) => char.charCodeAt(0));
}

function typedArrayToPlaintext(typedArray: Uint8Array, isOriginalDataJson = true) {
// Uint8Array --> data (string, number, object, array..)
const decoder = new TextDecoder();
const plaintext = decoder.decode(typedArray);
return isOriginalDataJson ? JSON.parse(plaintext) : plaintext;
}
53 changes: 46 additions & 7 deletions flottform/forms/src/flottform-channel-client.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { decrypt, encrypt, encryptionKeyToCryptoKey } from './encryption';
import {
ClientState,
EventEmitter,
Expand Down Expand Up @@ -28,6 +29,8 @@ export class FlottformChannelClient extends EventEmitter<Listeners> {
private pollTimeForIceInMs: number;
private logger: Logger;

private cryptoKey: CryptoKey | null = null;
private encryptionKey: string;
private state: ClientState = 'init';
private openPeerConnection: RTCPeerConnection | null = null;
private dataChannel: RTCDataChannel | null = null;
Expand All @@ -38,19 +41,22 @@ export class FlottformChannelClient extends EventEmitter<Listeners> {
endpointId,
flottformApi,
rtcConfiguration,
encryptionKey,
pollTimeForIceInMs = POLL_TIME_IN_MS,
logger = console
}: {
endpointId: string;
flottformApi: string | URL;
rtcConfiguration: RTCConfiguration;
encryptionKey: string;
pollTimeForIceInMs?: number;
logger?: Logger;
}) {
super();
this.endpointId = endpointId;
this.flottformApi = flottformApi;
this.rtcConfiguration = rtcConfiguration;
this.encryptionKey = encryptionKey;
this.pollTimeForIceInMs = pollTimeForIceInMs;
this.logger = logger;
}
Expand All @@ -66,14 +72,17 @@ export class FlottformChannelClient extends EventEmitter<Listeners> {
if (this.openPeerConnection) {
this.close();
}
// Import cryptoKey from encryptionKey
this.cryptoKey = await encryptionKeyToCryptoKey(this.encryptionKey);

const baseApi = (
this.flottformApi instanceof URL ? this.flottformApi : new URL(this.flottformApi)
)
.toString()
.replace(/\/$/, '');

// For now the fetching can be done outside of these classes and should be passed as an argument.

/* try {
this.rtcConfiguration.iceServers = await this.fetchIceServers(baseApi);
} catch (error) {
Expand All @@ -88,8 +97,16 @@ export class FlottformChannelClient extends EventEmitter<Listeners> {
const putClientInfoUrl = `${this.flottformApi}/${this.endpointId}/client`;

this.changeState('retrieving-info-from-endpoint');
const { hostInfo } = await retrieveEndpointInfo(getEndpointInfoUrl);
await this.openPeerConnection.setRemoteDescription(hostInfo.session);
const hostInfoCipherText = await retrieveEndpointInfo(getEndpointInfoUrl);

if (!this.cryptoKey) {
throw new Error('CryptoKey is null! Decryption is not possible!!');
}
const hostInfo = await decrypt(hostInfoCipherText.hostInfo, this.cryptoKey);

const hostSession: RTCSessionDescriptionInit = JSON.parse(hostInfo.session);

await this.openPeerConnection.setRemoteDescription(hostSession);
const session = await this.openPeerConnection.createAnswer();
await this.openPeerConnection.setLocalDescription(session);

Expand Down Expand Up @@ -179,6 +196,7 @@ export class FlottformChannelClient extends EventEmitter<Listeners> {
this.logger.error(`onicecandidateerror - ${this.openPeerConnection!.connectionState}`, e);
};
};

private setUpConnectionStateGathering = (getEndpointInfoUrl: string) => {
if (this.openPeerConnection === null) {
this.changeState(
Expand Down Expand Up @@ -216,12 +234,14 @@ export class FlottformChannelClient extends EventEmitter<Listeners> {
}
};
};

private stopPollingForIceCandidates = async () => {
if (this.pollForIceTimer) {
clearTimeout(this.pollForIceTimer);
}
this.pollForIceTimer = null;
};

private startPollingForIceCandidates = async (getEndpointInfoUrl: string) => {
if (this.pollForIceTimer) {
clearTimeout(this.pollForIceTimer);
Expand All @@ -231,32 +251,51 @@ export class FlottformChannelClient extends EventEmitter<Listeners> {

this.pollForIceTimer = setTimeout(this.startPollingForIceCandidates, this.pollTimeForIceInMs);
};

private pollForConnection = async (getEndpointInfoUrl: string) => {
if (this.openPeerConnection === null) {
this.changeState('error', "openPeerConnection is null. Unable to retrieve Host's details");
return;
}

this.logger.log('polling for host ice candidates', this.openPeerConnection.iceGatheringState);
const { hostInfo } = await retrieveEndpointInfo(getEndpointInfoUrl);
for (const iceCandidate of hostInfo.iceCandidates) {
const hostInfoCipherText = await retrieveEndpointInfo(getEndpointInfoUrl);
if (!this.cryptoKey) {
throw new Error('CryptoKey is null! Decryption is not possible!!');
}
const hostInfo = await decrypt(hostInfoCipherText.hostInfo, this.cryptoKey);

const hostIceCandidates: RTCIceCandidateInit[] = JSON.parse(hostInfo.iceCandidates);

for (const iceCandidate of hostIceCandidates) {
await this.openPeerConnection.addIceCandidate(iceCandidate);
}
};

private putClientInfo = async (
putClientInfoUrl: string,
clientKey: string,
clientIceCandidates: Set<RTCIceCandidateInit>,
session: RTCSessionDescriptionInit
) => {
this.logger.log('Updating client info with new list of ice candidates');
if (!this.cryptoKey) {
throw new Error('CryptoKey is null! Encryption is not possible!!');
}
const encryptedClientInfo = await encrypt(
JSON.stringify({
session: JSON.stringify(session),
iceCandidates: JSON.stringify([...clientIceCandidates])
}),
this.cryptoKey
);

const response = await fetch(putClientInfoUrl, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
clientKey,
iceCandidates: [...clientIceCandidates],
session
clientInfo: encryptedClientInfo
})
});
if (!response.ok) {
Expand Down
Loading