Skip to content

Instantly share code, notes, and snippets.

@RaresAil
Last active October 13, 2022 20:51
Show Gist options
  • Save RaresAil/890bc467f08066a354f2b33b33768227 to your computer and use it in GitHub Desktop.
Save RaresAil/890bc467f08066a354f2b33b33768227 to your computer and use it in GitHub Desktop.
Yubico OTP Verification with No Cloud
const crypto = require('crypto');
const MAX_TIMESTAMP = 0xffffff;
const MAX_SESSION_CTR = 0xff;
const MAX_USE_CTR = 0x7fff;
class Token {
#privateId = null;
#secretKey = null;
#publicId = null;
#sessionTimestamp = 0;
#sessionCTR = 0;
#usageCTR = 0;
constructor(secretKey, privateId, publicId) {
this.#secretKey = secretKey;
this.#privateId = privateId;
this.#publicId = publicId;
}
validate(code) {
if (this.#publicId !== code.slice(0, 12)) {
return false;
}
const hex = this.#toHex(code.slice(12));
const decipher = crypto.createDecipheriv(
'aes-128-ecb',
this.#secretKey,
null
);
decipher.setAutoPadding(false);
const buffer = Buffer.concat([decipher.update(Buffer.from(hex, 'hex'))]);
if (!this.#verifyChecksum(buffer)) {
return false;
}
if (this.#privateId !== buffer.subarray(0, 6).toString('hex')) {
return false;
}
const usageCtr = parseInt(
Buffer.concat([buffer.subarray(7, 8), buffer.subarray(6, 7)]).toString(
'hex'
),
16
);
if (this.#usageCTR > usageCtr && this.#usageCTR !== MAX_USE_CTR) {
return false;
}
const sessionCtr = parseInt(buffer.subarray(11, 12).toString('hex'), 16);
if (sessionCtr >= MAX_SESSION_CTR) {
return false;
}
if (this.#usageCTR === usageCtr && this.#sessionCTR >= sessionCtr) {
return false;
}
const timestamp = parseInt(
Buffer.concat([buffer.subarray(10, 11), buffer.subarray(8, 10)]).toString(
'hex'
),
16
);
if (timestamp >= MAX_TIMESTAMP) {
return false;
}
if (this.#sessionCTR > 1 && this.#sessionTimestamp >= timestamp) {
return false;
}
this.#sessionTimestamp = timestamp;
this.#sessionCTR = sessionCtr;
this.#usageCTR = usageCtr;
return true;
}
get toJSON() {
return {
privateId: this.#privateId,
publicId: this.#publicId,
sessionTimestamp: this.#sessionTimestamp,
sessionCTR: this.#sessionCTR,
usageCTR: this.#usageCTR
};
}
#toHex(value) {
return value
.split('')
.reduce(
(acc, char) =>
acc + this.#hexArray[this.#modeHexArray.findIndex((c) => c === char)],
''
);
}
#verifyChecksum(buffer) {
let crc = 0x7fff;
let isNeg = true;
let i;
for (let j = 0; j < buffer.length; j++) {
crc ^= buffer[j] & 0xff;
for (i = 0; i < 8; i++) {
if ((crc & 1) == 0) {
crc >>= 1;
if (isNeg) {
isNeg = false;
crc |= 0x4000;
}
} else {
crc >>= 1;
if (isNeg) {
crc ^= 0x4408;
} else {
crc ^= 0x0408;
isNeg = true;
}
}
}
}
return (isNeg ? crc | 0x8000 : crc) === 0xf0b8;
}
#hexArray = [
'a',
'b',
'c',
'd',
'e',
'f',
'0',
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9'
];
#modeHexArray = [
'l',
'n',
'r',
't',
'u',
'v',
'c',
'b',
'd',
'e',
'f',
'g',
'h',
'i',
'j',
'k'
];
}
const token = new Token(Buffer.from('KEY', 'hex'), 'PRIVATE', 'PUBLIC');
console.log(token.validate('vv'));
console.log(token.validate('vv'));
console.log(token.validate('vv'));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment