Last active
October 13, 2022 20:51
-
-
Save RaresAil/890bc467f08066a354f2b33b33768227 to your computer and use it in GitHub Desktop.
Yubico OTP Verification with No Cloud
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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