Skip to content

Commit

Permalink
Merge branch 'wes-no-encryption'
Browse files Browse the repository at this point in the history
  • Loading branch information
jadoc committed Nov 23, 2023
2 parents 8e03506 + 7dc968d commit 70144a0
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 139 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

This is a JS library that implements TCP network control for LG TVs manufactured
since 2018. It utilizes encryption rules based on a guide found on the internet.
A non-encrypted mode is provided for older models, but hasn't been tested.

This is not provided by LG, and it is not a complete implementation for every TV
model.
Expand Down Expand Up @@ -85,7 +86,8 @@ const lgtv = new LGTV(
'1a:2b:3c:4d:5e:6f',

/**
* Encryption Keycode, as generated during "Setting Up the TV" above
* Encryption Keycode, as generated during "Setting Up the TV" above.
* If not provided, uses clear text, but is required by most models.
*/
'KEY1C0DE',

Expand Down
Binary file added docs/LG_RS232_IP_legacy.pdf
Binary file not shown.
221 changes: 117 additions & 104 deletions src/classes/LGEncryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,107 +15,127 @@ export interface EncryptionSettings {
responseTerminator: string;
}

function assertSettings(settings: EncryptionSettings) {
assert(
typeof settings === 'object' && settings !== null,
'settings must be an object',
);

const {
encryptionIvLength,
encryptionKeyDigest,
encryptionKeyIterations,
encryptionKeyLength,
encryptionKeySalt,
keycodeFormat,
messageBlockSize,
messageTerminator,
responseTerminator,
} = settings;
assert(
typeof encryptionIvLength === 'number' && encryptionIvLength > 0,
'settings.encryptionIvLength must be a number greater than 0',
);
assert(
typeof encryptionKeyDigest === 'string' && encryptionKeyDigest.length > 0,
'settings.encryptionKeyDigest must be a string with length greater than 0',
);
assert(
typeof encryptionKeyIterations === 'number' && encryptionKeyIterations > 0,
'settings.encryptionKLeyIterations must be a number greater than 0',
);
assert(
typeof encryptionKeyLength === 'number' && encryptionKeyLength > 0,
'settings.encryptionKeyLength must be a number greater than 0',
);
assert(
Array.isArray(encryptionKeySalt) &&
encryptionKeySalt.some((data) => typeof data === 'number' && data > 0),
'settings.encryptionKeySalt must be an array of numbers with length greater than 0',
);
assert(
keycodeFormat instanceof RegExp,
'settings.keycodeFormat must be an instance of RegExp',
);
assert(
typeof messageBlockSize === 'number' && messageBlockSize > 0,
'settings.messageBlockSize must be a number greater than 0',
);
assert(
typeof messageTerminator === 'string' && messageTerminator.length > 0,
'settings.messageTerminator must be a string with length greater than 0',
);
assert(
typeof responseTerminator === 'string' && responseTerminator.length > 0,
'settings.responseTerminator must be a string with length greater than 0',
);
}
export class LGEncoder {
constructor(protected settings: EncryptionSettings = DefaultSettings) {
assert(
typeof settings === 'object' && settings !== null,
'settings must be an object',
);

function deriveKey(keycode: string, settings = DefaultSettings) {
assertSettings(settings);
assert(typeof keycode === 'string', 'keycode must be a string');
assert(settings.keycodeFormat.test(keycode), 'keycode format is invalid');

return pbkdf2Sync(
keycode,
Buffer.from(settings.encryptionKeySalt),
settings.encryptionKeyIterations,
settings.encryptionKeyLength,
settings.encryptionKeyDigest,
);
}
const { messageBlockSize, messageTerminator, responseTerminator } =
settings;
assert(
typeof messageBlockSize === 'number' && messageBlockSize > 0,
'settings.messageBlockSize must be a number greater than 0',
);
assert(
typeof messageTerminator === 'string' && messageTerminator.length > 0,
'settings.messageTerminator must be a string with length greater than 0',
);
assert(
typeof responseTerminator === 'string' && responseTerminator.length > 0,
'settings.responseTerminator must be a string with length greater than 0',
);
}

function generateRandomIv(length = DefaultSettings.encryptionIvLength) {
assert(typeof length === 'number', 'length must be a number');
assert(length > 0, 'length must be greater than 0');
protected terminateMessage(message: string): string {
const { messageTerminator } = this.settings;
assert(typeof message === 'string', 'message must be a string');
assert(message.length > 0, 'message must have a length greater than 0');
assert(
!message.includes(messageTerminator),
'message must not include the message terminator character',
);
return message + messageTerminator;
}

protected stripEnd(message: string): string {
const { responseTerminator } = this.settings;
return message.substring(0, message.indexOf(responseTerminator));
}

encode(message: string): Buffer {
return Buffer.from(this.terminateMessage(message), 'utf8');
}

const iv = Buffer.alloc(length, 0);
for (let i = 0; i < length; i++) {
iv[i] = Math.floor(Math.random() * 255);
decode(data: Buffer): string {
return this.stripEnd(data.toString());
}
return iv;
}

export class LGEncryption {
export class LGEncryption extends LGEncoder {
private derivedKey: Buffer;

constructor(keycode: string, private settings = DefaultSettings) {
assertSettings(settings);
this.derivedKey = deriveKey(keycode, settings);
}
constructor(keycode: string, settings: EncryptionSettings = DefaultSettings) {
super(settings);

const {
encryptionIvLength,
encryptionKeyDigest,
encryptionKeyIterations,
encryptionKeyLength,
encryptionKeySalt,
keycodeFormat,
} = settings;
assert(
typeof encryptionIvLength === 'number' && encryptionIvLength > 0,
'settings.encryptionIvLength must be a number greater than 0',
);
assert(
typeof encryptionKeyDigest === 'string' && encryptionKeyDigest.length > 0,
'settings.encryptionKeyDigest must be a string with length greater than 0',
);
assert(
typeof encryptionKeyIterations === 'number' &&
encryptionKeyIterations > 0,
'settings.encryptionKLeyIterations must be a number greater than 0',
);
assert(
typeof encryptionKeyLength === 'number' && encryptionKeyLength > 0,
'settings.encryptionKeyLength must be a number greater than 0',
);
assert(
Array.isArray(encryptionKeySalt) &&
encryptionKeySalt.some((data) => typeof data === 'number' && data > 0),
'settings.encryptionKeySalt must be an array of numbers with length greater than 0',
);
assert(
keycodeFormat instanceof RegExp,
'settings.keycodeFormat must be an instance of RegExp',
);

private prepareMessage(message: string) {
assert(typeof message === 'string', 'message must be a string');
assert(message.length > 0, 'message must have a length greater than 0');
this.derivedKey = this.deriveKey(keycode);
}

const { messageTerminator, messageBlockSize } = this.settings;
private deriveKey(keycode: string) {
assert(typeof keycode === 'string', 'keycode must be a string');
assert(
!message.includes(messageTerminator),
'message must not include the message terminator character',
this.settings.keycodeFormat.test(keycode),
'keycode format is invalid',
);

let newMessage = message + messageTerminator;
if (newMessage.length % messageBlockSize === 0) {
return pbkdf2Sync(
keycode,
Buffer.from(this.settings.encryptionKeySalt),
this.settings.encryptionKeyIterations,
this.settings.encryptionKeyLength,
this.settings.encryptionKeyDigest,
);
}

private generateRandomIv() {
const { encryptionIvLength } = this.settings;
const iv = Buffer.alloc(encryptionIvLength, 0);
for (let i = 0; i < encryptionIvLength; i++) {
iv[i] = Math.floor(Math.random() * 255);
}
return iv;
}

protected padMessage(message: string): string {
const { messageBlockSize } = this.settings;
let newMessage = message;
if (message.length % messageBlockSize === 0) {
newMessage += ' ';
}

Expand All @@ -124,13 +144,12 @@ export class LGEncryption {
const padding = messageBlockSize - remainder;
newMessage += String.fromCharCode(padding).repeat(padding);
}

return newMessage;
}

encrypt(message: string) {
const iv = generateRandomIv(this.settings.encryptionKeyLength);
const preparedMessage = this.prepareMessage(message);
encode(message: string): Buffer {
const iv = this.generateRandomIv();
const paddedMessage = this.padMessage(this.terminateMessage(message));

const ecbCypher = createCipheriv(
'aes-128-ecb',
Expand All @@ -140,30 +159,24 @@ export class LGEncryption {
const ivEnc = ecbCypher.update(iv);

const cbcCypher = createCipheriv('aes-128-cbc', this.derivedKey, iv);
const dataEnc = cbcCypher.update(preparedMessage, 'utf8');
const dataEnc = cbcCypher.update(paddedMessage);

return Buffer.concat([ivEnc, dataEnc]);
}

decrypt(cipher: Buffer) {
decode(cipher: Buffer): string {
const { encryptionKeyLength } = this.settings;
const ecbDecypher = createDecipheriv(
'aes-128-ecb',
this.derivedKey,
Buffer.alloc(0),
);
ecbDecypher.setAutoPadding(false);
const iv = ecbDecypher.update(
cipher.slice(0, this.settings.encryptionKeyLength),
);
const iv = ecbDecypher.update(cipher.slice(0, encryptionKeyLength));

const cbcDecypher = createDecipheriv('aes-128-cbc', this.derivedKey, iv);
cbcDecypher.setAutoPadding(false);
const decrypted = cbcDecypher
.update(cipher.slice(this.settings.encryptionKeyLength))
.toString();
return decrypted.substring(
0,
decrypted.indexOf(this.settings.responseTerminator),
);
const decrypted = cbcDecypher.update(cipher.slice(encryptionKeyLength));
return this.stripEnd(decrypted.toString());
}
}
16 changes: 9 additions & 7 deletions src/classes/LGTV.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
PictureModes,
ScreenMuteModes,
} from '../constants/TV.js';
import { LGEncryption } from './LGEncryption.js';
import { LGEncoder, LGEncryption } from './LGEncryption.js';
import { TinySocket } from './TinySocket.js';

export class ResponseParseError extends Error {}
Expand All @@ -20,23 +20,25 @@ function throwIfNotOK(response: string) {
}

export class LGTV {
encryption: LGEncryption;
encoder: LGEncoder;
socket: TinySocket;

constructor(
host: string,
macAddress: string | null,
keycode: string,
keycode: string | null,
settings = DefaultSettings,
) {
this.socket = new TinySocket(host, macAddress, settings);
this.encryption = new LGEncryption(keycode, settings);
this.encoder = keycode
? new LGEncryption(keycode, settings)
: new LGEncoder(settings);
}

private async sendCommand(command: string) {
const encryptedData = this.encryption.encrypt(command);
const encryptedResponse = await this.socket.sendReceive(encryptedData);
return this.encryption.decrypt(encryptedResponse);
const request = this.encoder.encode(command);
const response = await this.socket.sendReceive(request);
return this.encoder.decode(response);
}

async connect(): Promise<void> {
Expand Down
Loading

0 comments on commit 70144a0

Please sign in to comment.