feat: changed TOTP and base32 library to self-writed special for quick-js, fixed BigInt incompatibility
This commit is contained in:
parent
ab13957cb0
commit
f16d7374f4
@ -1,19 +0,0 @@
|
||||
Copyright (c) 2023, Chris Umbel, Lisoveliy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
@ -1,10 +0,0 @@
|
||||
# base32
|
||||
|
||||
Implementation of RFC 3548 Base32 encoding/decoding for zepp quickjs.
|
||||
|
||||
## Usage
|
||||
import {encode, decode} from 'base32/index.js'
|
||||
base32.encode('node');
|
||||
// output: NZXWIZI=
|
||||
base32.decode('NZXWIZI=');
|
||||
//output: node
|
@ -1,23 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2023, Chris Umbel, Lisoveliy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
|
||||
export { encode, decode } from './lib/base32/base32.js'
|
@ -1,128 +0,0 @@
|
||||
/*
|
||||
Copyright (c) 2023, Chris Umbel, Lisoveliy
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
var charTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
var byteTable = [
|
||||
0xff, 0xff, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
|
||||
0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
|
||||
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
|
||||
0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06,
|
||||
0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
|
||||
0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,
|
||||
0x17, 0x18, 0x19, 0xff, 0xff, 0xff, 0xff, 0xff
|
||||
];
|
||||
|
||||
function quintetCount(buff) {
|
||||
var quintets = Math.floor(buff.length / 5);
|
||||
return buff.length % 5 === 0 ? quintets : quintets + 1;
|
||||
}
|
||||
|
||||
export function encode(plain) {
|
||||
if (!Buffer.isBuffer(plain)) {
|
||||
plain = new Buffer(plain);
|
||||
}
|
||||
var i = 0;
|
||||
var j = 0;
|
||||
var shiftIndex = 0;
|
||||
var digit = 0;
|
||||
var encoded = new Buffer(quintetCount(plain) * 8);
|
||||
|
||||
/* byte by byte isn't as pretty as quintet by quintet but tests a bit
|
||||
faster. will have to revisit. */
|
||||
while (i < plain.length) {
|
||||
var current = plain[i];
|
||||
|
||||
if (shiftIndex > 3) {
|
||||
digit = current & (0xff >> shiftIndex);
|
||||
shiftIndex = (shiftIndex + 5) % 8;
|
||||
digit = (digit << shiftIndex) | ((i + 1 < plain.length) ?
|
||||
plain[i + 1] : 0) >> (8 - shiftIndex);
|
||||
i++;
|
||||
} else {
|
||||
digit = (current >> (8 - (shiftIndex + 5))) & 0x1f;
|
||||
shiftIndex = (shiftIndex + 5) % 8;
|
||||
if (shiftIndex === 0) i++;
|
||||
}
|
||||
|
||||
encoded[j] = charTable.charCodeAt(digit);
|
||||
j++;
|
||||
}
|
||||
|
||||
for (i = j; i < encoded.length; i++) {
|
||||
encoded[i] = 0x3d; //'='.charCodeAt(0)
|
||||
}
|
||||
|
||||
return encoded;
|
||||
};
|
||||
|
||||
export function decode(encoded) {
|
||||
var shiftIndex = 0;
|
||||
var plainDigit = 0;
|
||||
var plainChar;
|
||||
var plainPos = 0;
|
||||
if (!Buffer.isBuffer(encoded)) {
|
||||
encoded = new Buffer(encoded);
|
||||
}
|
||||
var decoded = new Buffer(Math.ceil(encoded.length * 5 / 8));
|
||||
|
||||
/* byte by byte isn't as pretty as octet by octet but tests a bit
|
||||
faster. will have to revisit. */
|
||||
for (var i = 0; i < encoded.length; i++) {
|
||||
if (encoded[i] === 0x3d) { //'='
|
||||
break;
|
||||
}
|
||||
|
||||
var encodedByte = encoded[i] - 0x30;
|
||||
|
||||
if (encodedByte < byteTable.length) {
|
||||
plainDigit = byteTable[encodedByte];
|
||||
|
||||
if (shiftIndex <= 3) {
|
||||
shiftIndex = (shiftIndex + 5) % 8;
|
||||
|
||||
if (shiftIndex === 0) {
|
||||
plainChar |= plainDigit;
|
||||
decoded[plainPos] = plainChar;
|
||||
plainPos++;
|
||||
plainChar = 0;
|
||||
} else {
|
||||
plainChar |= 0xff & (plainDigit << (8 - shiftIndex));
|
||||
}
|
||||
} else {
|
||||
shiftIndex = (shiftIndex + 5) % 8;
|
||||
plainChar |= 0xff & (plainDigit >>> shiftIndex);
|
||||
decoded[plainPos] = plainChar;
|
||||
plainPos++;
|
||||
|
||||
plainChar = 0xff & (plainDigit << (8 - shiftIndex));
|
||||
}
|
||||
} else {
|
||||
throw new Error('Invalid input - it is not base32 encoded string');
|
||||
}
|
||||
}
|
||||
|
||||
return decoded.slice(0, plainPos);
|
||||
};
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"name": "base32",
|
||||
"description": "Implementation RFC 3548 Base32 encoding/decoding for node.",
|
||||
"version": "1.0.2-zepp",
|
||||
"author": "Chris Umbel <chris@chrisumbel.com>",
|
||||
"keywords": ["base32", "encoding"],
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/chrisumbel/thirty-two.git"
|
||||
}
|
||||
}
|
54
lib/totp-quickjs/OTPGenerator.js
Normal file
54
lib/totp-quickjs/OTPGenerator.js
Normal file
@ -0,0 +1,54 @@
|
||||
import { decode } from "./base32decoder.js";
|
||||
import jsSHA from "jssha";
|
||||
"use bigint"
|
||||
/**
|
||||
* get HOTP based on counter
|
||||
* @param {BigInt} counter BigInt counter of HOTP
|
||||
* @param {string} secret base32 encoded string
|
||||
* @param {number} [digits=6] number of digits in OTP token
|
||||
* @param {string} [hashType='SHA-1'] type of hash (more in jsSHA documentation)
|
||||
* @returns HOTP string
|
||||
*/
|
||||
export function getHOTP(counter, secret, digits = 6, hashType = 'SHA-1'){
|
||||
|
||||
//Stage 1: Prepare data
|
||||
const rawDataCounter = new DataView(new ArrayBuffer(8))
|
||||
rawDataCounter.setUint32(0, counter >> 32)
|
||||
rawDataCounter.setUint32(4, counter)
|
||||
const bCounter = new Uint8Array(rawDataCounter.buffer)
|
||||
|
||||
const bSecret = new Uint8Array(decode(secret).match(/.{1,2}/g).map(chunk => parseInt(chunk, 16))); //confirmed
|
||||
|
||||
//Stage 2: Hash data
|
||||
const jssha = new jsSHA(hashType, 'UINT8ARRAY')
|
||||
jssha.setHMACKey(bSecret, 'UINT8ARRAY')
|
||||
jssha.update(bCounter)
|
||||
const hmacResult = jssha.getHMAC('UINT8ARRAY') //confirmed
|
||||
|
||||
//Stage 3: Dynamic truncate
|
||||
const offsetB = hmacResult[19] & 0xf;
|
||||
const P = hmacResult.slice(offsetB, offsetB + 4)
|
||||
P[0] = P[0] & 0x7f;
|
||||
|
||||
//Stage 4: Format string
|
||||
let res = (new DataView(P.buffer).getInt32(0) % Math.pow(10, digits)).toString()
|
||||
while(res.length < digits)
|
||||
res = '0' + res;
|
||||
return res;
|
||||
}
|
||||
|
||||
/**
|
||||
* get OTP based on current time
|
||||
* @param {string} secret base32 encoded string
|
||||
* @param {number} [digits=6] digits in OTP
|
||||
* @param {number} [time=Date.now()] time for counter (default unix time epoch)
|
||||
* @param {number} [fetchTime=30] period of token in seconds
|
||||
* @param {number} [timeOffset=0] time offset for token in seconds
|
||||
* @param {string} [hashType='SHA-1'] type of hash (more in jsSHA documentation)
|
||||
* @returns TOTP string
|
||||
*/
|
||||
export function getTOTP(secret, digits = 6, time = Date.now(), fetchTime = 30, timeOffset = 0, hashType = 'SHA-1')
|
||||
{
|
||||
const unixTime = Math.round((time / 1000 + timeOffset) / fetchTime)
|
||||
return getHOTP(BigInt(unixTime), secret, digits)
|
||||
}
|
26
lib/totp-quickjs/base32decoder.js
Normal file
26
lib/totp-quickjs/base32decoder.js
Normal file
@ -0,0 +1,26 @@
|
||||
export function decode(base32) {
|
||||
for (
|
||||
var base32chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567",
|
||||
bits = "",
|
||||
hex = "",
|
||||
i = 0;
|
||||
i < base32.length;
|
||||
i++
|
||||
) {
|
||||
var val = base32chars.indexOf(base32.charAt(i).toUpperCase());
|
||||
bits += leftpad(val.toString(2), 5, "0");
|
||||
}
|
||||
for (i = 0; i + 4 <= bits.length; i += 4) {
|
||||
var chunk = bits.substr(i, 4);
|
||||
hex += parseInt(chunk, 2).toString(16);
|
||||
}
|
||||
return hex;
|
||||
}
|
||||
|
||||
function leftpad(str, len, pad) {
|
||||
return (
|
||||
len + 1 >= str.length &&
|
||||
(str = new Array(len + 1 - str.length).join(pad) + str),
|
||||
str
|
||||
);
|
||||
}
|
61
lib/totp-quickjs/index.js
Normal file
61
lib/totp-quickjs/index.js
Normal file
@ -0,0 +1,61 @@
|
||||
import { getHOTP } from "./OTPGenerator.js"
|
||||
"use bigint"
|
||||
/**
|
||||
* TOTP instance
|
||||
*/
|
||||
export class TOTP{
|
||||
/**
|
||||
*
|
||||
* @param {string} secret base32 encoded string
|
||||
* @param {string} issuer issuer of TOTP
|
||||
* @param {number} [digits=6] number of digits in OTP token
|
||||
* @param {number} [fetchTime=30] period of token in seconds
|
||||
* @param {number} [timeOffset=0] time offset for token in seconds
|
||||
* @param {string} [hashType='SHA-1'] type of hash (more in jsSHA documentation)
|
||||
*/
|
||||
constructor(secret,
|
||||
issuer,
|
||||
digits = 6,
|
||||
fetchTime = 30,
|
||||
timeOffset = 0,
|
||||
hashType = 'SHA-1')
|
||||
{
|
||||
this.secret = secret
|
||||
this.issuer = issuer
|
||||
this.digits = digits
|
||||
this.fetchTime = fetchTime
|
||||
this.timeOffset = timeOffset
|
||||
this.hashType = hashType
|
||||
}
|
||||
/**
|
||||
*
|
||||
* @param {number} time time for counter (default unix time epoch)
|
||||
* @returns OTP instance
|
||||
*/
|
||||
getOTP(time = Date.now()){
|
||||
const unixTime = (time / 1000 + this.timeOffset) / this.fetchTime
|
||||
const otp = getHOTP(Math.floor(unixTime), this.secret, this.digits)
|
||||
const expireTime = time +
|
||||
(this.fetchTime -
|
||||
(time / 1000 + this.timeOffset) %
|
||||
this.fetchTime) * 1000
|
||||
|
||||
return new OTP(otp, expireTime)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Class for TOTP.getOTP result
|
||||
*/
|
||||
export class OTP{
|
||||
/**
|
||||
*
|
||||
* @param {string} otp OTP string
|
||||
* @param {number} expireTime time in seconds to reset OTP
|
||||
*/
|
||||
constructor(otp, expireTime)
|
||||
{
|
||||
this.otp = otp
|
||||
this.expireTime = expireTime
|
||||
}
|
||||
}
|
@ -1,2 +0,0 @@
|
||||
export { HOTP } from './lib/hotp.js'
|
||||
export { TOTP } from './lib/totp.js'
|
@ -1,72 +0,0 @@
|
||||
import { encode, decode } from '../../base32/index.js'
|
||||
import jsSHA from 'jssha'
|
||||
|
||||
export class HOTP {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} key secret key
|
||||
* @param {*} digit lenth of otp code
|
||||
*/
|
||||
constructor(key, digit = 6) {
|
||||
this.key = key;
|
||||
this.digit = digit;
|
||||
}
|
||||
static encode(data){
|
||||
return encode(data)
|
||||
}
|
||||
/**
|
||||
* generate secret key
|
||||
* @param {int} len
|
||||
*/
|
||||
static randomKey(len = 16) {
|
||||
const str = Math.random().toString(36);
|
||||
return encode(str).toString().substr(0, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* generate a OTP base on HMAC-SHA-1
|
||||
* @param {int} movingFactor counter
|
||||
*/
|
||||
genOTP(movingFactor) {
|
||||
const hmacSha = new jsSHA('SHA-1', 'BYTES');
|
||||
hmacSha.setHMACKey(decode(this.key).toString(), 'BYTES');
|
||||
|
||||
const factorByte = this._factor2ByteText(movingFactor);
|
||||
hmacSha.update(factorByte);
|
||||
|
||||
const hmac_result = hmacSha.getHMAC('BYTES');
|
||||
return this._truncat(hmac_result);
|
||||
}
|
||||
|
||||
/**
|
||||
* verify a OPT code
|
||||
* @param {string} opt opt code
|
||||
* @param {int} movingFactor counter
|
||||
*/
|
||||
verify(opt, movingFactor) {
|
||||
return opt === this.genOTP(movingFactor);
|
||||
}
|
||||
|
||||
_truncat(hmac_result) {
|
||||
const offset = hmac_result[19].charCodeAt() & 0xf;
|
||||
const bin_code = (hmac_result[offset].charCodeAt() & 0x7f) << 24
|
||||
| (hmac_result[offset + 1].charCodeAt() & 0xff) << 16
|
||||
| (hmac_result[offset + 2].charCodeAt() & 0xff) << 8
|
||||
| (hmac_result[offset + 3].charCodeAt() & 0xff);
|
||||
let otp = (bin_code % 10 ** this.digit).toString();
|
||||
while (otp.length < this.digit) {
|
||||
otp = '0' + otp;
|
||||
}
|
||||
return otp;
|
||||
}
|
||||
|
||||
_factor2ByteText(movingFactor) {
|
||||
const text = new Array(8);
|
||||
for (let i = text.length - 1; i >= 0; i--) {
|
||||
text[i] = String.fromCharCode(movingFactor & 0xFF);
|
||||
movingFactor >>= 8;
|
||||
}
|
||||
return text.join('');
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { HOTP } from "./hotp.js";
|
||||
|
||||
export class TOTP extends HOTP {
|
||||
constructor(key) {
|
||||
super(key)
|
||||
}
|
||||
|
||||
genOTP(timeStep = 30, t0 = 0) {
|
||||
const T = Math.floor((Date.now() / 1000 - t0) / timeStep);
|
||||
return super.genOTP(T);
|
||||
}
|
||||
|
||||
verify(otp, timeStep = 30, t0 = 0) {
|
||||
return otp === this.genOTP(timeStep, t0);
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
{
|
||||
"name": "totp.js",
|
||||
"version": "1.0.1-zepp",
|
||||
"description": "Two-factor authentication implementation in pure javascript. One-time password generator (HOTP/TOTP) with support for Google Authenticator. ",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "./node_modules/.bin/mocha"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wuyanxin/totp.js.git"
|
||||
},
|
||||
"keywords": [
|
||||
"otp",
|
||||
"hotp",
|
||||
"totp",
|
||||
"Two-factor authentication",
|
||||
"Google authenticator"
|
||||
],
|
||||
"author": "wuyanxin",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wuyanxin/totp.js/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wuyanxin/totp.js#readme",
|
||||
"dependencies": {
|
||||
"jssha": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^5.0.4"
|
||||
}
|
||||
}
|
@ -35,6 +35,7 @@ Page({
|
||||
let totpHeight = margin;
|
||||
|
||||
for(let i = 0; i < buffer.length; i++){
|
||||
console.log(buffer[i])
|
||||
createWidget(widget.FILL_RECT, {
|
||||
x: width / 2 - buttonWidth / 2,
|
||||
y: totpHeight,
|
||||
@ -53,7 +54,7 @@ Page({
|
||||
align_h: align.CENTER_H,
|
||||
align_v: align.CENTER_V,
|
||||
text_style: text_style.NONE,
|
||||
text: buffer[i].name
|
||||
text: (buffer[i].expireTime - Date.now()) / 1000
|
||||
})
|
||||
createWidget(widget.TEXT, {
|
||||
x: 0,
|
||||
@ -65,7 +66,7 @@ Page({
|
||||
align_h: align.CENTER_H,
|
||||
align_v: align.CENTER_V,
|
||||
text_style: text_style.NONE,
|
||||
text: buffer[i].data
|
||||
text: buffer[i].otp
|
||||
})
|
||||
|
||||
totpHeight += margin + buttonHeight;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { TOTP } from "../../lib/totp.js/lib/totp";
|
||||
import { TOTP } from "../../lib/totp-quickjs";
|
||||
|
||||
export class TOTPBuffer{
|
||||
constructor(){
|
||||
@ -6,15 +6,6 @@ export class TOTPBuffer{
|
||||
}
|
||||
|
||||
getTOTPs(){
|
||||
return [new TOTPData("Contabo-Customer-Control-Panel-11755808: my.contabo.com", new TOTP(TOTP.encode('UU5WIIWKDNAHUNNL')).genOTP()),
|
||||
new TOTPData("OTPData", new TOTP(TOTP.encode('LCHJZO23LT3Z2QYNYAYAJXH5HFDZ5YI2')).genOTP()),
|
||||
new TOTPData("test", new TOTP(TOTP.encode('GAXHMZDEGJVG64LP')).genOTP())]
|
||||
}
|
||||
}
|
||||
|
||||
export class TOTPData{
|
||||
constructor(name, data){
|
||||
this.name = name;
|
||||
this.data = data;
|
||||
return [new TOTP('JBSWY3DPEHPK3PXP').getOTP()]
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user