From 69dddcebb5e1b05edc2d251ededeb9c7ffb33bc7 Mon Sep 17 00:00:00 2001 From: Lisoveliy Date: Thu, 24 Jul 2025 14:39:02 +0300 Subject: [PATCH] chore: prettier cleanup --- .prettierrc.yaml | 2 +- README.md | 10 +- app-side/index.js | 26 ++- app.js | 2 +- app.json | 98 +++++------ docs/guides/how-to-add-totps/README.md | 8 +- jsconfig.json | 14 +- lib/protobuf-decoder/hexUtils.js | 49 +++--- lib/protobuf-decoder/protobufDecoder.js | 214 ++++++++++++------------ lib/protobuf-decoder/varintUtils.js | 34 ++-- lib/totp-quickjs/OTPGenerator.js | 64 ++++--- lib/totp-quickjs/base32decoder.js | 60 +++---- lib/totp-quickjs/index.js | 81 ++++----- package.json | 26 +-- page/index.js | 13 +- page/render/index/renderer.js | 4 +- page/render/totpRenderer.js | 2 +- page/tip.js | 2 +- setting/index.js | 108 ++++++------ setting/utils/queryParser.js | 155 ++++++++--------- 20 files changed, 488 insertions(+), 484 deletions(-) diff --git a/.prettierrc.yaml b/.prettierrc.yaml index 68ea04d..f1d7f90 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -1 +1 @@ -tabWidth: 4 \ No newline at end of file +tabWidth: 4 diff --git a/README.md b/README.md index 97dd8f2..40b4efb 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@ -# TOTPFIT +# TOTPFIT + ### Another 2FAuthenticator based on TOTP for Zepp Amazfit GTS 4 with Google Authenticator migration support ![alt text](docs/assets/image2.png) ### Features: -- Supports of ```otpauth://``` links with parameters "client", "issuer", "algorithm", "digits", "period", "offset" + +- Supports of `otpauth://` links with parameters "client", "issuer", "algorithm", "digits", "period", "offset" - Addition/Edition/Deletion of TOTPs from mobile app -- Support of Google Authenticator migration links formated: ```otpauth-migration://offline?data=...``` (At this stage with only 6 digits and only 30 seconds period) +- Support of Google Authenticator migration links formated: `otpauth-migration://offline?data=...` (At this stage with only 6 digits and only 30 seconds period) ### Guides: [How to add 2FA TOTP records (keys) on app](/docs/guides/how-to-add-totps/README.md) -#### This repo has mirror for issues on [GitHub](https://github.com/Lisoveliy/totpfit) \ No newline at end of file +#### This repo has mirror for issues on [GitHub](https://github.com/Lisoveliy/totpfit) diff --git a/app-side/index.js b/app-side/index.js index b1cc4bf..17f4896 100644 --- a/app-side/index.js +++ b/app-side/index.js @@ -1,17 +1,13 @@ -import { BaseSideService } from "@zeppos/zml/base-side" +import { BaseSideService } from "@zeppos/zml/base-side"; AppSideService( - BaseSideService( - { - onInit(){ - - }, - onRequest(request, response){ - if(request.method === 'totps'){ - response(null, settings.settingsStorage.getItem('TOTPs')) - } - }, - onSettingsChange(){ } - } - ) -) \ No newline at end of file + BaseSideService({ + onInit() {}, + onRequest(request, response) { + if (request.method === "totps") { + response(null, settings.settingsStorage.getItem("TOTPs")); + } + }, + onSettingsChange() {}, + }), +); diff --git a/app.js b/app.js index 49d65b6..188d3b2 100644 --- a/app.js +++ b/app.js @@ -7,5 +7,5 @@ App( }, onCreate() {}, onDestroy() {}, - }) + }), ); diff --git a/app.json b/app.json index 0fffc6b..550d452 100644 --- a/app.json +++ b/app.json @@ -1,56 +1,50 @@ { - "configVersion": "v3", - "app": { - "appId": 25087, - "appName": "totpfit", - "appType": "app", - "version": { - "code": 1, - "name": "1.3.0" + "configVersion": "v3", + "app": { + "appId": 25087, + "appName": "totpfit", + "appType": "app", + "version": { + "code": 1, + "name": "1.3.0" + }, + "icon": "icon.png", + "vender": "zepp", + "description": "Another 2FAuthenticator based on TOTP for Zepp Amazfit GTS 4" }, - "icon": "icon.png", - "vender": "zepp", - "description": "Another 2FAuthenticator based on TOTP for Zepp Amazfit GTS 4" - }, - "permissions": [ - "data:os.device.info", - "device:os.local_storage" - ], - "runtime": { - "apiVersion": { - "compatible": "3.0.0", - "target": "3.0.0", - "minVersion": "3.0" - } - }, - "targets": { - "default": { - "module": { - "page": { - "pages": [ - "page/index", - "page/tip" - ] - }, - "app-side": { - "path": "app-side/index" - }, - "setting": { - "path": "setting/index" + "permissions": ["data:os.device.info", "device:os.local_storage"], + "runtime": { + "apiVersion": { + "compatible": "3.0.0", + "target": "3.0.0", + "minVersion": "3.0" } - }, - "platforms": [ - { - "st": "s", - "dw": 390 + }, + "targets": { + "default": { + "module": { + "page": { + "pages": ["page/index", "page/tip"] + }, + "app-side": { + "path": "app-side/index" + }, + "setting": { + "path": "setting/index" + } + }, + "platforms": [ + { + "st": "s", + "dw": 390 + } + ] } - ] - } - }, - "i18n": { - "en-US": { - "appName": "TOTPFit" - } - }, - "defaultLanguage": "en-US" -} \ No newline at end of file + }, + "i18n": { + "en-US": { + "appName": "TOTPFit" + } + }, + "defaultLanguage": "en-US" +} diff --git a/docs/guides/how-to-add-totps/README.md b/docs/guides/how-to-add-totps/README.md index 9311a59..f78b736 100644 --- a/docs/guides/how-to-add-totps/README.md +++ b/docs/guides/how-to-add-totps/README.md @@ -6,7 +6,7 @@ To add 2FA TOTP records using 2FA TOTP QR-Codes, you must scan QR-Code of servic ![QR Code with URI](image.png) -Copy this URI string and paste it to app using button *"Add new TOTP record"*: +Copy this URI string and paste it to app using button _"Add new TOTP record"_: ![Add new TOTP record popup](image-2.png) @@ -18,7 +18,7 @@ You can edit your otpauth:// records using button "Change TOTP link". Your previ ### If you use google migrations (otpauth-migration:// links) -To add 2FA TOTP recods using migration from Google Authenticator app, you must go to menu, select "Transfer accounts" -> "Export accounts" +To add 2FA TOTP recods using migration from Google Authenticator app, you must go to menu, select "Transfer accounts" -> "Export accounts" Select codes then screenshot QR code and scan (decode) it to a URI. Use any app providing scan from image, ex: "Search screen" function (Google Lens) on Google Assistant. @@ -26,7 +26,7 @@ For example, this QR-Code will represent next URI string: ![Google lens scan from Google Authenticator](image-5.png) -After scaning copy this URI string and paste it to app using button *"Add new TOTP record"*: +After scaning copy this URI string and paste it to app using button _"Add new TOTP record"_: ![Add new TOTP record using otpauth-migration](image-6.png) @@ -34,4 +34,4 @@ Then press OK, all selected records from Google Authenticator will appear on pag ![Added records from otpauth-migration](image-7.png) -You can edit your records using button "Change TOTP link". Your previous record will be replaced with a new otpauth:// link from text field (otpauth-migration:// will not work), and previous link will not be shown on field. \ No newline at end of file +You can edit your records using button "Change TOTP link". Your previous record will be replaced with a new otpauth:// link from text field (otpauth-migration:// will not work), and previous link will not be shown on field. diff --git a/jsconfig.json b/jsconfig.json index 1bd80d8..c8e1fa6 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,9 +1,9 @@ { - "compilerOptions": { - "module": "commonjs", - "target": "es6", - "checkJs": true - }, - "exclude": ["node_modules", "**/node_modules/*"], - "files": ["node_modules/@zeppos/device-types/dist/index.d.ts"] + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "checkJs": true + }, + "exclude": ["node_modules", "**/node_modules/*"], + "files": ["node_modules/@zeppos/device-types/dist/index.d.ts"] } diff --git a/lib/protobuf-decoder/hexUtils.js b/lib/protobuf-decoder/hexUtils.js index 6f6ff52..636c027 100644 --- a/lib/protobuf-decoder/hexUtils.js +++ b/lib/protobuf-decoder/hexUtils.js @@ -1,36 +1,37 @@ -import { Buffer } from 'buffer' +import { Buffer } from "buffer"; export function parseInput(input) { - const normalizedInput = input.replace(/\s/g, ""); - const normalizedHexInput = normalizedInput.replace(/0x/g, "").toLowerCase(); - if (isHex(normalizedHexInput)) { - return Buffer.from(normalizedHexInput, "hex"); - } else { - return Buffer.from(normalizedInput, "base64"); - } + const normalizedInput = input.replace(/\s/g, ""); + const normalizedHexInput = normalizedInput.replace(/0x/g, "").toLowerCase(); + if (isHex(normalizedHexInput)) { + return Buffer.from(normalizedHexInput, "hex"); + } else { + return Buffer.from(normalizedInput, "base64"); + } } export function isHex(string) { - let result = true; - for (const char of string) { - if (!((char >= "a" && char <= "f") || (char >= "0" && char <= "9"))) { - result = false; + let result = true; + for (const char of string) { + if (!((char >= "a" && char <= "f") || (char >= "0" && char <= "9"))) { + result = false; + } } - } - return result; + return result; } export function bufferLeToBeHex(buffer) { - let output = ""; - for (const v of buffer) { - const hex = v.toString(16); - if (hex.length === 1) { - output = "0" + hex + output; - } else { - output = hex + output; + let output = ""; + for (const v of buffer) { + const hex = v.toString(16); + if (hex.length === 1) { + output = "0" + hex + output; + } else { + output = hex + output; + } } - } - return output; + return output; } -export const bufferToPrettyHex = b => [...b].map(c => c.toString(16).padStart(2, '0')).join(' '); +export const bufferToPrettyHex = (b) => + [...b].map((c) => c.toString(16).padStart(2, "0")).join(" "); diff --git a/lib/protobuf-decoder/protobufDecoder.js b/lib/protobuf-decoder/protobufDecoder.js index f99a573..e8ad50e 100644 --- a/lib/protobuf-decoder/protobufDecoder.js +++ b/lib/protobuf-decoder/protobufDecoder.js @@ -1,132 +1,132 @@ import { decodeVarint } from "./varintUtils"; export class BufferReader { - constructor(buffer) { - this.buffer = buffer; - this.offset = 0; - } - - readVarInt() { - const result = decodeVarint(this.buffer, this.offset); - this.offset += result.length; - - return result.value; - } - - readBuffer(length) { - this.checkByte(length); - const result = this.buffer.slice(this.offset, this.offset + length); - this.offset += length; - - return result; - } - - // gRPC has some additional header - remove it - trySkipGrpcHeader() { - const backupOffset = this.offset; - - if (this.buffer[this.offset] === 0 && this.leftBytes() >= 5) { - this.offset++; - const length = this.buffer.readInt32BE(this.offset); - this.offset += 4; - - if (length > this.leftBytes()) { - // Something is wrong, revert - this.offset = backupOffset; - } + constructor(buffer) { + this.buffer = buffer; + this.offset = 0; } - } - leftBytes() { - return this.buffer.length - this.offset; - } + readVarInt() { + const result = decodeVarint(this.buffer, this.offset); + this.offset += result.length; - checkByte(length) { - const bytesAvailable = this.leftBytes(); - if (length > bytesAvailable) { - throw new Error( - "Not enough bytes left. Requested: " + - length + - " left: " + - bytesAvailable - ); + return result.value; } - } - checkpoint() { - this.savedOffset = this.offset; - } + readBuffer(length) { + this.checkByte(length); + const result = this.buffer.slice(this.offset, this.offset + length); + this.offset += length; - resetToCheckpoint() { - this.offset = this.savedOffset; - } + return result; + } + + // gRPC has some additional header - remove it + trySkipGrpcHeader() { + const backupOffset = this.offset; + + if (this.buffer[this.offset] === 0 && this.leftBytes() >= 5) { + this.offset++; + const length = this.buffer.readInt32BE(this.offset); + this.offset += 4; + + if (length > this.leftBytes()) { + // Something is wrong, revert + this.offset = backupOffset; + } + } + } + + leftBytes() { + return this.buffer.length - this.offset; + } + + checkByte(length) { + const bytesAvailable = this.leftBytes(); + if (length > bytesAvailable) { + throw new Error( + "Not enough bytes left. Requested: " + + length + + " left: " + + bytesAvailable, + ); + } + } + + checkpoint() { + this.savedOffset = this.offset; + } + + resetToCheckpoint() { + this.offset = this.savedOffset; + } } export const TYPES = { - VARINT: 0, - FIXED64: 1, - LENDELIM: 2, - FIXED32: 5 + VARINT: 0, + FIXED64: 1, + LENDELIM: 2, + FIXED32: 5, }; export function decodeProto(buffer) { - const reader = new BufferReader(buffer); - const parts = []; - reader.trySkipGrpcHeader(); + const reader = new BufferReader(buffer); + const parts = []; + reader.trySkipGrpcHeader(); - try { - while (reader.leftBytes() > 0) { - reader.checkpoint(); + try { + while (reader.leftBytes() > 0) { + reader.checkpoint(); - const byteRange = [reader.offset]; - const indexType = parseInt(reader.readVarInt().toString()); - const type = indexType & 0b111; - const index = indexType >> 3; + const byteRange = [reader.offset]; + const indexType = parseInt(reader.readVarInt().toString()); + const type = indexType & 0b111; + const index = indexType >> 3; - let value; - if (type === TYPES.VARINT) { - value = reader.readVarInt().toString(); - } else if (type === TYPES.LENDELIM) { - const length = parseInt(reader.readVarInt().toString()); - value = reader.readBuffer(length); - } else if (type === TYPES.FIXED32) { - value = reader.readBuffer(4); - } else if (type === TYPES.FIXED64) { - value = reader.readBuffer(8); - } else { - throw new Error("Unknown type: " + type); - } - byteRange.push(reader.offset); + let value; + if (type === TYPES.VARINT) { + value = reader.readVarInt().toString(); + } else if (type === TYPES.LENDELIM) { + const length = parseInt(reader.readVarInt().toString()); + value = reader.readBuffer(length); + } else if (type === TYPES.FIXED32) { + value = reader.readBuffer(4); + } else if (type === TYPES.FIXED64) { + value = reader.readBuffer(8); + } else { + throw new Error("Unknown type: " + type); + } + byteRange.push(reader.offset); - parts.push({ - byteRange, - index, - type, - value - }); + parts.push({ + byteRange, + index, + type, + value, + }); + } + } catch (err) { + reader.resetToCheckpoint(); + console.log(err); } - } catch (err) { - reader.resetToCheckpoint(); - console.log(err); - } - return { - parts, - leftOver: reader.readBuffer(reader.leftBytes()) - }; + return { + parts, + leftOver: reader.readBuffer(reader.leftBytes()), + }; } export function typeToString(type, subType) { - switch (type) { - case TYPES.VARINT: - return "varint"; - case TYPES.LENDELIM: - return subType || "len_delim"; - case TYPES.FIXED32: - return "fixed32"; - case TYPES.FIXED64: - return "fixed64"; - default: - return "unknown"; - } + switch (type) { + case TYPES.VARINT: + return "varint"; + case TYPES.LENDELIM: + return subType || "len_delim"; + case TYPES.FIXED32: + return "fixed32"; + case TYPES.FIXED64: + return "fixed64"; + default: + return "unknown"; + } } diff --git a/lib/protobuf-decoder/varintUtils.js b/lib/protobuf-decoder/varintUtils.js index b3a8a98..3d540e0 100644 --- a/lib/protobuf-decoder/varintUtils.js +++ b/lib/protobuf-decoder/varintUtils.js @@ -1,23 +1,23 @@ export function decodeVarint(buffer, offset) { - let res = 0; - let shift = 0; - let byte = 0; + let res = 0; + let shift = 0; + let byte = 0; - do { - if (offset >= buffer.length) { - throw new RangeError("Index out of bound decoding varint"); - } + do { + if (offset >= buffer.length) { + throw new RangeError("Index out of bound decoding varint"); + } - byte = buffer[offset++]; + byte = buffer[offset++]; - const multiplier = 2 ** shift; - const thisByteValue = (byte & 0x7f) * multiplier; - shift += 7; - res = res + thisByteValue; - } while (byte >= 0x80); + const multiplier = 2 ** shift; + const thisByteValue = (byte & 0x7f) * multiplier; + shift += 7; + res = res + thisByteValue; + } while (byte >= 0x80); - return { - value: res, - length: shift / 7 - }; + return { + value: res, + length: shift / 7, + }; } diff --git a/lib/totp-quickjs/OTPGenerator.js b/lib/totp-quickjs/OTPGenerator.js index cbf9a29..a4fb9e0 100644 --- a/lib/totp-quickjs/OTPGenerator.js +++ b/lib/totp-quickjs/OTPGenerator.js @@ -1,53 +1,63 @@ import { decode } from "./base32decoder.js"; import jsSHA from "jssha"; -"use bigint" +("use bigint"); /** * get HOTP based on counter * @param {BigInt} counter BigInt counter of HOTP - * @param {string} secret base32 encoded string + * @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'){ - +export function getHOTP(counter, secret, digits = 6, hashType = "SHA-1") { //Stage 1: Prepare data - const rawDataCounter = new DataView(new ArrayBuffer(8)) - 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 - + const rawDataCounter = new DataView(new ArrayBuffer(8)); + 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 - + 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) + 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; + 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 {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 {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) -} \ No newline at end of file +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); +} diff --git a/lib/totp-quickjs/base32decoder.js b/lib/totp-quickjs/base32decoder.js index 784e2b1..705dcf9 100644 --- a/lib/totp-quickjs/base32decoder.js +++ b/lib/totp-quickjs/base32decoder.js @@ -1,60 +1,62 @@ 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; + 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 - ); + return ( + len + 1 >= str.length && + (str = new Array(len + 1 - str.length).join(pad) + str), + str + ); } export function encode(bytes) { - const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; + const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; let bits = 0; let value = 0; - let output = ''; + let output = ""; for (let i = 0; i < bytes.length; i++) { value = (value << 8) | bytes[i]; bits += 8; while (bits >= 5) { - output += alphabet[(value >>> (bits - 5)) & 0x1F]; + output += alphabet[(value >>> (bits - 5)) & 0x1f]; bits -= 5; } } if (bits > 0) { - output += alphabet[(value << (5 - bits)) & 0x1F]; + output += alphabet[(value << (5 - bits)) & 0x1f]; } const paddingLength = (8 - (output.length % 8)) % 8; - output += '='.repeat(paddingLength); + output += "=".repeat(paddingLength); return output; } export function base64decode(base64) { - const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + const chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; let result = []; - let i = 0, j = 0; + let i = 0, + j = 0; let b1, b2, b3, b4; while (i < base64.length) { @@ -77,4 +79,4 @@ export function base64decode(base64) { } return result.slice(0, j); -} \ No newline at end of file +} diff --git a/lib/totp-quickjs/index.js b/lib/totp-quickjs/index.js index f2453b8..d77715f 100644 --- a/lib/totp-quickjs/index.js +++ b/lib/totp-quickjs/index.js @@ -1,60 +1,63 @@ -import { getHOTP } from "./OTPGenerator.js" +import { getHOTP } from "./OTPGenerator.js"; /** * TOTP instance */ export class TOTP { /** - * - * @param {string} secret base32 encoded string + * + * @param {string} secret base32 encoded string * @param {string} issuer issuer of TOTP * @param {string} client client 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 {number} [timeOffset=0] time offset for token in seconds * @param {string} [hashType='SHA-1'] type of hash (more in jsSHA documentation) */ - constructor(secret, + constructor( + secret, issuer, client, digits = 6, fetchTime = 30, timeOffset = 0, - hashType = 'SHA-1') { - this.secret = secret - this.issuer = issuer - this.client = client - this.digits = digits - this.fetchTime = fetchTime - this.timeOffset = timeOffset - this.hashType = hashType + hashType = "SHA-1", + ) { + this.secret = secret; + this.issuer = issuer; + this.client = client; + this.digits = digits; + this.fetchTime = fetchTime; + this.timeOffset = timeOffset; + this.hashType = hashType; } - static copy(totp){ + static copy(totp) { return new TOTP( - secret = totp.secret, - issuer = totp.TOTPissuer, - client = totp.client, - digits = totp.digits, - fetchTime = totp.fetchTime, - timeOffset = totp.timeOffset, - hashType = totp.hashType - ) + (secret = totp.secret), + (issuer = totp.TOTPissuer), + (client = totp.client), + (digits = totp.digits), + (fetchTime = totp.fetchTime), + (timeOffset = totp.timeOffset), + (hashType = totp.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 + + 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 - const createdTime = time - (((time / 1000 + this.timeOffset) % - this.fetchTime) * 1000) - - return new OTP(otp, createdTime, expireTime) + ((time / 1000 + this.timeOffset) % this.fetchTime)) * + 1000; + const createdTime = + time - ((time / 1000 + this.timeOffset) % this.fetchTime) * 1000; + + return new OTP(otp, createdTime, expireTime); } } @@ -63,14 +66,14 @@ export class TOTP { */ export class OTP { /** - * + * * @param {string} otp OTP string - * @param {number} createdTime time in unix epoch created OTP - * @param {number} expireTime time in unix epoch to expire OTP + * @param {number} createdTime time in unix epoch created OTP + * @param {number} expireTime time in unix epoch to expire OTP */ constructor(otp, createdTime, expireTime) { - this.otp = otp - this.createdTime = createdTime - this.expireTime = expireTime + this.otp = otp; + this.createdTime = createdTime; + this.expireTime = expireTime; } -} \ No newline at end of file +} diff --git a/package.json b/package.json index afd0025..f9f29ba 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,15 @@ { - "name": "totpfit", - "version": "1.3.0", - "description": "Another 2FAuthenticator based on TOTP for Zepp Amazfit GTS 4", - "main": "app.js", - "author": "Lisoveliy", - "license": "MIT", - "devDependencies": { - "@zeppos/device-types": "^3.0.0" - }, - "dependencies": { - "@zeppos/zml": "^0.0.27", - "jssha": "^3.3.1" - } + "name": "totpfit", + "version": "1.3.0", + "description": "Another 2FAuthenticator based on TOTP for Zepp Amazfit GTS 4", + "main": "app.js", + "author": "Lisoveliy", + "license": "MIT", + "devDependencies": { + "@zeppos/device-types": "^3.0.0" + }, + "dependencies": { + "@zeppos/zml": "^0.0.27", + "jssha": "^3.3.1" + } } diff --git a/page/index.js b/page/index.js index 2f72254..07fe98f 100644 --- a/page/index.js +++ b/page/index.js @@ -13,23 +13,22 @@ Page( this.getTOTPData() .then((x) => { app._options.globalData.TOTPS = JSON.parse(x) ?? []; - + let localStorage = new LocalStorage(); localStorage.setItem( "TOTPs", - JSON.stringify(app._options.globalData.TOTPS) + JSON.stringify(app._options.globalData.TOTPS), ); this.initPage(); }) .catch((x) => { console.log(`Init failed: ${x}`); - try{ + try { let localStorage = new LocalStorage(); app._options.globalData.TOTPS = JSON.parse( - localStorage.getItem("TOTPs", []) + localStorage.getItem("TOTPs", []), ); - } - catch{ + } catch { app._options.globalData.TOTPS = []; } this.initPage(); @@ -56,5 +55,5 @@ Page( method: "totps", }); }, - }) + }), ); diff --git a/page/render/index/renderer.js b/page/render/index/renderer.js index 7301b3b..fa73620 100644 --- a/page/render/index/renderer.js +++ b/page/render/index/renderer.js @@ -30,7 +30,7 @@ function renderTOTPs(buffer) { expireBar: RenderExpireBar( i, otpData.createdTime, - buffer[i].fetchTime + buffer[i].fetchTime, ), }; setInterval(() => { @@ -38,7 +38,7 @@ function renderTOTPs(buffer) { (Date.now() - otpData.createdTime) / 1000 / buffer[i].fetchTime - - 1 + 1, ); renderData[i].expireBar.setProperty(prop.MORE, { diff --git a/page/render/totpRenderer.js b/page/render/totpRenderer.js index 714d6ed..996622b 100644 --- a/page/render/totpRenderer.js +++ b/page/render/totpRenderer.js @@ -77,7 +77,7 @@ export function RenderOTPValue(position, otpValue) { export function RenderExpireBar(position, createdTime, fetchTime) { const yPos = getYPos(position); const expireDif = Math.abs( - (Date.now() - createdTime) / 1000 / fetchTime - 1 + (Date.now() - createdTime) / 1000 / fetchTime - 1, ); return createWidget(widget.ARC, { x: buttonWidth - 50, diff --git a/page/tip.js b/page/tip.js index 94a42e0..7a20cc8 100644 --- a/page/tip.js +++ b/page/tip.js @@ -27,5 +27,5 @@ Page( text: "To add TOTP record open\n settings on Zepp app", }); }, - }) + }), ); diff --git a/setting/index.js b/setting/index.js index fb0641b..e8c1c47 100644 --- a/setting/index.js +++ b/setting/index.js @@ -9,30 +9,33 @@ const colors = { text: "#fafafa", alert: "#ad3c23", notify: "#555555", - bigText: "#fafafa" + bigText: "#fafafa", }; AppSettingsPage({ build(props) { _props = props; const storage = JSON.parse( - props.settingsStorage.getItem("TOTPs") ?? "[]" + props.settingsStorage.getItem("TOTPs") ?? "[]", ); const totpEntrys = GetTOTPList(storage); - const addTOTPsHint = storage.length < 1 ? - Text({ - paragraph: true, - align: "center", - style: { - paddingTop: "10px", - marginBottom: "10px", - color: colors.text, - fontSize: 16, - verticalAlign: "middle", - }, - }, - "For add a 2FA TOTP record you must have otpauth:// link or otpauth-migration:// link from Google Authenticator Migration QR-Code" - ) : null; + const addTOTPsHint = + storage.length < 1 + ? Text( + { + paragraph: true, + align: "center", + style: { + paddingTop: "10px", + marginBottom: "10px", + color: colors.text, + fontSize: 16, + verticalAlign: "middle", + }, + }, + "For add a 2FA TOTP record you must have otpauth:// link or otpauth-migration:// link from Google Authenticator Migration QR-Code", + ) + : null; const createButton = TextInput({ placeholder: "otpauth(-migration)://", label: "Add new TOTP record", @@ -43,8 +46,7 @@ AppSettingsPage({ return; } - if (Array.isArray(link)) - storage.push(...link); + if (Array.isArray(link)) storage.push(...link); else storage.push(link); updateStorage(storage); @@ -61,7 +63,7 @@ AppSettingsPage({ position: storage.length < 1 ? "absolute" : null, //TODO: Сделать что-то с этим кошмаром bottom: storage.length < 1 ? "0px" : null, left: storage.length < 1 ? "0px" : null, - right: storage.length < 1 ? "0px" : null + right: storage.length < 1 ? "0px" : null, }, }); @@ -79,35 +81,40 @@ AppSettingsPage({ textAlign: "center", }, }, - storage.length < 1 ? addTOTPsHint : Text( - { - align: "center", - paragraph: true, - style: { - marginBottom: "10px", - color: colors.bigText, - fontSize: 23, - fontWeight: "500", - verticalAlign: "middle", - }, - }, - "TOTP records:" - ) + storage.length < 1 + ? addTOTPsHint + : Text( + { + align: "center", + paragraph: true, + style: { + marginBottom: "10px", + color: colors.bigText, + fontSize: 23, + fontWeight: "500", + verticalAlign: "middle", + }, + }, + "TOTP records:", + ), ), ...totpEntrys, createButton, - View({ - style: { - display: "flex", - justifyContent: "center" - } - }, - Link({ - source: "https://github.com/Lisoveliy/totpfit/blob/main/docs/guides/how-to-add-totps/README.md" + View( + { + style: { + display: "flex", + justifyContent: "center", + }, }, - "Instruction | Report issue (GitHub)") + Link( + { + source: "https://github.com/Lisoveliy/totpfit/blob/main/docs/guides/how-to-add-totps/README.md", + }, + "Instruction | Report issue (GitHub)", + ), ), - ] + ], ); return body; }, @@ -124,8 +131,7 @@ function GetTOTPList(storage) { onChange: (changes) => { try { let link = getTOTPByLink(changes); - if (Array.isArray(link)) - return; + if (Array.isArray(link)) return; storage[elementId] = link; updateStorage(storage); @@ -152,16 +158,16 @@ function GetTOTPList(storage) { style: { color: colors.text, fontSize: "18px", - fontWeight: "500" + fontWeight: "500", }, paragraph: true, }, - `${element.issuer}: ${element.client}` + `${element.issuer}: ${element.client}`, ); const delButton = Button({ onClick: () => { storage = storage.filter( - (x) => storage.indexOf(x) != elementId + (x) => storage.indexOf(x) != elementId, ); updateStorage(storage); }, @@ -182,7 +188,7 @@ function GetTOTPList(storage) { }, align: "center", }, - `${element.hashType} | ${element.digits} digits | ${element.fetchTime} seconds | ${element.timeOffset} sec offset` + `${element.hashType} | ${element.digits} digits | ${element.fetchTime} seconds | ${element.timeOffset} sec offset`, ); const view = View( { @@ -204,9 +210,9 @@ function GetTOTPList(storage) { gridTemplateColumns: "1fr 100px", }, }, - [textInput, delButton] + [textInput, delButton], ), - ] + ], ); totpEntrys.push({ text: text, view: view }); counter++; diff --git a/setting/utils/queryParser.js b/setting/utils/queryParser.js index ae94456..1c7fc13 100644 --- a/setting/utils/queryParser.js +++ b/setting/utils/queryParser.js @@ -6,109 +6,100 @@ const otpauthScheme = "otpauth:/"; const googleMigrationScheme = "otpauth-migration:/"; export function getTOTPByLink(link) { - if (link.includes(otpauthScheme)) - return getByOtpauthScheme(link) - if (link.includes(googleMigrationScheme)) - return getByGoogleMigrationScheme(link) + if (link.includes(otpauthScheme)) return getByOtpauthScheme(link); + if (link.includes(googleMigrationScheme)) + return getByGoogleMigrationScheme(link); - return null; + return null; } function getHashType(algorithm) { - if (algorithm == "SHA1") return "SHA-1"; - if (algorithm == "SHA256") return "SHA-256"; - if (algorithm == "SHA512") return "SHA-512"; - else return "SHA-1"; + if (algorithm == "SHA1") return "SHA-1"; + if (algorithm == "SHA256") return "SHA-256"; + if (algorithm == "SHA512") return "SHA-512"; + else return "SHA-1"; } function getByOtpauthScheme(link) { - try { - let args = link.split("/", otpauthScheme.length); - let type = args[2]; //Returns 'hotp' or 'totp' - let issuer = args[3].split(":")[0]?.split("?")[0]; //Returns issuer - let client = - args[3].split(":")[1]?.split("?")[0] ?? - args[3].split(":")[0]?.split("?")[0]; //Returns client - let secret = args[3].split("secret=")[1]?.split("&")[0]; //Returns secret - let period = args[3].split("period=")[1]?.split("&")[0]; //Returns period - let digits = args[3].split("digit=")[1]?.split("&")[0]; //Returns digits - let algorithm = args[3].split("algorithm=")[1]?.split("&")[0]; //Returns algorithm - let offset = args[3].split("offset=")[1]?.split("&")[0] ?? 0; //Returns offset + try { + let args = link.split("/", otpauthScheme.length); + let type = args[2]; //Returns 'hotp' or 'totp' + let issuer = args[3].split(":")[0]?.split("?")[0]; //Returns issuer + let client = + args[3].split(":")[1]?.split("?")[0] ?? + args[3].split(":")[0]?.split("?")[0]; //Returns client + let secret = args[3].split("secret=")[1]?.split("&")[0]; //Returns secret + let period = args[3].split("period=")[1]?.split("&")[0]; //Returns period + let digits = args[3].split("digit=")[1]?.split("&")[0]; //Returns digits + let algorithm = args[3].split("algorithm=")[1]?.split("&")[0]; //Returns algorithm + let offset = args[3].split("offset=")[1]?.split("&")[0] ?? 0; //Returns offset - if (type.toLowerCase() != "totp") - throw new Error("Type is not valid, requires 'TOTP'"); + if (type.toLowerCase() != "totp") + throw new Error("Type is not valid, requires 'TOTP'"); - if (secret === undefined) throw new Error("Secret not defined"); + if (secret === undefined) throw new Error("Secret not defined"); - if (issuer == client) { - issuer = args[3].split("issuer=")[1]?.split("&")[0]; - } + if (issuer == client) { + issuer = args[3].split("issuer=")[1]?.split("&")[0]; + } - issuer = decodeURIComponent(issuer); - client = decodeURIComponent(client); + issuer = decodeURIComponent(issuer); + client = decodeURIComponent(client); - return new TOTP( - secret, - issuer, - client, - Number(digits), - Number(period), - Number(offset), - getHashType(algorithm) - ); - } catch (err) { - console.log(err) - return null; - } + return new TOTP( + secret, + issuer, + client, + Number(digits), + Number(period), + Number(offset), + getHashType(algorithm), + ); + } catch (err) { + console.log(err); + return null; + } } function getByGoogleMigrationScheme(link) { + let data = link.split("data=")[1]; //Returns base64 encoded data + data = decodeURIComponent(data); + let decode = base64decode(data); + let proto = decodeProto(decode); - let data = link.split("data=")[1]; //Returns base64 encoded data - data = decodeURIComponent(data); - let decode = base64decode(data); - let proto = decodeProto(decode); + let protoTotps = []; - let protoTotps = []; + proto.parts.forEach((part) => { + if (part.type == TYPES.LENDELIM) { + protoTotps.push(decodeProto(part.value)); + } + }); - proto.parts.forEach(part => { - if (part.type == TYPES.LENDELIM) { - protoTotps.push(decodeProto(part.value)); - } - }); + let totps = []; + protoTotps.forEach((x) => { + let type = x.parts.filter((x) => x.index == 6)[0]; //find type of OTP + if (type.value !== "2") { + console.log("ERR: it's a not TOTP record"); + return; + } + let secret = x.parts.filter((x) => x.index == 1)[0].value; + secret = encode(secret); - let totps = []; - protoTotps.forEach(x => { - let type = x.parts.filter(x => x.index == 6)[0]; //find type of OTP - if (type.value !== '2') { - console.log("ERR: it's a not TOTP record") - return; - } - let secret = x.parts.filter(x => x.index == 1)[0].value; - secret = encode(secret); + let name = bytesToString(x.parts.filter((x) => x.index == 2)[0].value); + let issuer = bytesToString( + x.parts.filter((x) => x.index == 3)[0].value, + ); - let name = bytesToString(x.parts.filter(x => x.index == 2)[0].value); - let issuer = bytesToString(x.parts.filter(x => x.index == 3)[0].value); - - totps.push(new TOTP( - secret, - issuer, - name, - 6, - 30, - 0, - "SHA-1" - )); - }); - - return totps; + totps.push(new TOTP(secret, issuer, name, 6, 30, 0, "SHA-1")); + }); + return totps; } function bytesToString(bytes) { - let str = ''; - for (let i = 0; i < bytes.length; i++) { - str += String.fromCharCode(bytes[i]); - } - return str; -} \ No newline at end of file + let str = ""; + for (let i = 0; i < bytes.length; i++) { + str += String.fromCharCode(bytes[i]); + } + return str; +}