mirror of
https://gitlab.com/futo-org/fcast.git
synced 2025-08-07 09:42:50 +00:00
Added encryption.
This commit is contained in:
parent
b8bd78d90d
commit
9599c1931e
29 changed files with 1016 additions and 1069 deletions
|
@ -1,7 +1,6 @@
|
|||
import net = require('net');
|
||||
import * as crypto from 'crypto';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { DecryptedMessage, EncryptedMessage, KeyExchangeMessage, PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VersionMessage, VolumeUpdateMessage } from './Packets';
|
||||
import { PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VersionMessage, VolumeUpdateMessage } from './Packets';
|
||||
import { WebSocket } from 'ws';
|
||||
|
||||
enum SessionState {
|
||||
|
@ -24,11 +23,8 @@ export enum Opcode {
|
|||
PlaybackError = 9,
|
||||
SetSpeed = 10,
|
||||
Version = 11,
|
||||
KeyExchange = 12,
|
||||
Encrypted = 13,
|
||||
Ping = 14,
|
||||
Pong = 15,
|
||||
StartEncryption = 16
|
||||
Ping = 12,
|
||||
Pong = 13
|
||||
};
|
||||
|
||||
const LENGTH_BYTES = 4;
|
||||
|
@ -42,52 +38,17 @@ export class FCastSession {
|
|||
writer: (data: Buffer) => void;
|
||||
state: SessionState;
|
||||
emitter = new EventEmitter();
|
||||
encryptionStarted = false;
|
||||
|
||||
private aesKey: Buffer;
|
||||
private dh: crypto.DiffieHellman;
|
||||
private queuedEncryptedMessages: EncryptedMessage[] = [];
|
||||
|
||||
constructor(socket: net.Socket | WebSocket, writer: (data: Buffer) => void) {
|
||||
this.socket = socket;
|
||||
this.writer = writer;
|
||||
this.state = SessionState.WaitingForLength;
|
||||
|
||||
this.dh = generateKeyPair();
|
||||
|
||||
const keyExchangeMessage = getKeyExchangeMessage(this.dh);
|
||||
console.log(`Sending KeyExchangeMessage: ${keyExchangeMessage}`);
|
||||
this.send(Opcode.KeyExchange, keyExchangeMessage);
|
||||
}
|
||||
|
||||
sendVersion(value: VersionMessage) {
|
||||
this.send(Opcode.Version, value);
|
||||
}
|
||||
|
||||
sendPlaybackError(value: PlaybackErrorMessage) {
|
||||
this.send(Opcode.PlaybackError, value);
|
||||
}
|
||||
|
||||
sendPlaybackUpdate(value: PlaybackUpdateMessage) {
|
||||
this.send(Opcode.PlaybackUpdate, value);
|
||||
}
|
||||
|
||||
sendVolumeUpdate(value: VolumeUpdateMessage) {
|
||||
this.send(Opcode.VolumeUpdate, value);
|
||||
}
|
||||
|
||||
private send(opcode: number, message = null) {
|
||||
if (this.encryptionStarted && opcode != Opcode.Encrypted && opcode != Opcode.KeyExchange && opcode != Opcode.StartEncryption) {
|
||||
const decryptedMessage: DecryptedMessage = {
|
||||
opcode,
|
||||
message
|
||||
};
|
||||
|
||||
this.send(Opcode.Encrypted, encryptMessage(this.aesKey, decryptedMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
send(opcode: number, message = null) {
|
||||
const json = message ? JSON.stringify(message) : null;
|
||||
console.log(`send (opcode: ${opcode}, body: ${json})`);
|
||||
|
||||
let data: Uint8Array;
|
||||
if (json) {
|
||||
const utf8Encode = new TextEncoder();
|
||||
|
@ -215,35 +176,9 @@ export class FCastSession {
|
|||
case Opcode.SetSpeed:
|
||||
this.emitter.emit("setspeed", JSON.parse(body) as SetSpeedMessage);
|
||||
break;
|
||||
case Opcode.KeyExchange:
|
||||
const keyExchangeMessage = JSON.parse(body) as KeyExchangeMessage;
|
||||
this.aesKey = computeSharedSecret(this.dh, keyExchangeMessage);
|
||||
this.send(Opcode.StartEncryption);
|
||||
|
||||
for (const encryptedMessage of this.queuedEncryptedMessages) {
|
||||
const decryptedMessage = decryptMessage(this.aesKey, encryptedMessage);
|
||||
this.handlePacket(decryptedMessage.opcode, decryptedMessage.message);
|
||||
}
|
||||
|
||||
this.queuedEncryptedMessages = [];
|
||||
break;
|
||||
case Opcode.Ping:
|
||||
this.send(Opcode.Pong);
|
||||
break;
|
||||
case Opcode.Encrypted:
|
||||
const encryptedMessage = JSON.parse(body) as EncryptedMessage;
|
||||
|
||||
if (this.aesKey) {
|
||||
const decryptedMessage = decryptMessage(this.aesKey, encryptedMessage);
|
||||
this.handlePacket(decryptedMessage.opcode, decryptedMessage.message);
|
||||
} else {
|
||||
if (this.queuedEncryptedMessages.length === 15) {
|
||||
this.queuedEncryptedMessages.shift();
|
||||
}
|
||||
|
||||
this.queuedEncryptedMessages.push(encryptedMessage);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(`Error handling packet from.`, e);
|
||||
|
@ -258,52 +193,14 @@ export class FCastSession {
|
|||
console.log('body', body);
|
||||
this.handlePacket(opcode, body);
|
||||
}
|
||||
}
|
||||
|
||||
export function getKeyExchangeMessage(dh: crypto.DiffieHellman): KeyExchangeMessage {
|
||||
return { version: 1, publicKey: dh.getPublicKey().toString('base64') };
|
||||
}
|
||||
|
||||
export function computeSharedSecret(dh: crypto.DiffieHellman, keyExchangeMessage: KeyExchangeMessage): Buffer {
|
||||
console.log("private", dh.getPrivateKey().toString('base64'));
|
||||
|
||||
const theirPublicKey = Buffer.from(keyExchangeMessage.publicKey, 'base64');
|
||||
console.log("theirPublicKey", theirPublicKey.toString('base64'));
|
||||
const secret = dh.computeSecret(theirPublicKey);
|
||||
console.log("secret", secret.toString('base64'));
|
||||
const digest = crypto.createHash('sha256').update(secret).digest();
|
||||
console.log("digest", digest.toString('base64'));
|
||||
return digest;
|
||||
}
|
||||
|
||||
export function encryptMessage(aesKey: Buffer, decryptedMessage: DecryptedMessage): EncryptedMessage {
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv('aes-256-cbc', aesKey, iv);
|
||||
let encrypted = cipher.update(JSON.stringify(decryptedMessage), 'utf8', 'base64');
|
||||
encrypted += cipher.final('base64');
|
||||
return {
|
||||
version: 1,
|
||||
iv: iv.toString('base64'),
|
||||
blob: encrypted
|
||||
};
|
||||
}
|
||||
|
||||
export function decryptMessage(aesKey: Buffer, encryptedMessage: EncryptedMessage): DecryptedMessage {
|
||||
const iv = Buffer.from(encryptedMessage.iv, 'base64');
|
||||
const decipher = crypto.createDecipheriv('aes-256-cbc', aesKey, iv);
|
||||
let decrypted = decipher.update(encryptedMessage.blob, 'base64', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
return JSON.parse(decrypted) as DecryptedMessage;
|
||||
}
|
||||
|
||||
export function generateKeyPair() {
|
||||
const dh = createDiffieHellman();
|
||||
dh.generateKeys();
|
||||
return dh;
|
||||
}
|
||||
|
||||
export function createDiffieHellman(): crypto.DiffieHellman {
|
||||
const p = Buffer.from('ffffffffffffffffc90fdaa22168c234c4c6628b80dc1cd129024e088a67cc74020bbea63b139b22514a08798e3404ddef9519b3cd3a431b302b0a6df25f14374fe1356d6d51c245e485b576625e7ec6f44c42e9a637ed6b0bff5cb6f406b7edee386bfb5a899fa5ae9f24117c4b1fe649286651ece45b3dc2007cb8a163bf0598da48361c55d39a69163fa8fd24cf5f83655d23dca3ad961c62f356208552bb9ed529077096966d670c354e4abc9804f1746c08ca18217c32905e462e36ce3be39e772c180e86039b2783a2ec07a28fb5c55df06f4c52c9de2bcbf6955817183995497cea956ae515d2261898fa051015728e5a8aacaa68ffffffffffffffff', 'hex');
|
||||
const g = Buffer.from('02', 'hex');
|
||||
return crypto.createDiffieHellman(p, g);
|
||||
bindEvents(emitter: EventEmitter) {
|
||||
this.emitter.on("play", (body: PlayMessage) => { emitter.emit("play", body) });
|
||||
this.emitter.on("pause", () => { emitter.emit("pause") });
|
||||
this.emitter.on("resume", () => { emitter.emit("resume") });
|
||||
this.emitter.on("stop", () => { emitter.emit("stop") });
|
||||
this.emitter.on("seek", (body: SeekMessage) => { emitter.emit("seek", body) });
|
||||
this.emitter.on("setvolume", (body: SetVolumeMessage) => { emitter.emit("setvolume", body) });
|
||||
this.emitter.on("setspeed", (body: SetSpeedMessage) => { emitter.emit("setspeed", body) });
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue