Compare commits
No commits in common. "dd32a6eac7679e2563adeed6121c6982a7508d94" and "096051b49dd957cae1183a8d918cb56a6d7acd75" have entirely different histories.
dd32a6eac7
...
096051b49d
15
README.md
15
README.md
@ -1,17 +1,6 @@
|
|||||||
# TOTPFIT
|
# TOTPFIT
|
||||||
### Another 2FAuthenticator based on TOTP for Zepp Amazfit GTS 4
|
### Another 2FAuthenticator based on TOTP for Zepp Amazfit GTS 4
|
||||||
|
|
||||||

|
Features:
|
||||||
|
|
||||||
### Features:
|
|
||||||
- Supports of otpauth links with parameters "issuer", "algorithm", "digits", "period"
|
- Supports of otpauth links with parameters "issuer", "algorithm", "digits", "period"
|
||||||
- Addition/Edition/Deletion of TOTPs from mobile settings app
|
- Addition/Edition/Deletion of TOTPs from mobile settings app
|
||||||
|
|
||||||
### Google Migration Support:
|
|
||||||
- Support of google migration links formated: ```otpauth-migration://offline?data=...``` (BETA)
|
|
||||||
|
|
||||||
### Screenshots:
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||

|
|
2
app.json
2
app.json
@ -6,7 +6,7 @@
|
|||||||
"appType": "app",
|
"appType": "app",
|
||||||
"version": {
|
"version": {
|
||||||
"code": 1,
|
"code": 1,
|
||||||
"name": "1.1.3"
|
"name": "1.1.1"
|
||||||
},
|
},
|
||||||
"icon": "icon.png",
|
"icon": "icon.png",
|
||||||
"vender": "zepp",
|
"vender": "zepp",
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 27 KiB |
Binary file not shown.
Before Width: | Height: | Size: 18 KiB |
@ -1,36 +0,0 @@
|
|||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isHex(string) {
|
|
||||||
let result = true;
|
|
||||||
for (const char of string) {
|
|
||||||
if (!((char >= "a" && char <= "f") || (char >= "0" && char <= "9"))) {
|
|
||||||
result = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const bufferToPrettyHex = b => [...b].map(c => c.toString(16).padStart(2, '0')).join(' ');
|
|
@ -1,132 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
};
|
|
||||||
|
|
||||||
export function decodeProto(buffer) {
|
|
||||||
const reader = new BufferReader(buffer);
|
|
||||||
const parts = [];
|
|
||||||
reader.trySkipGrpcHeader();
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
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
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
reader.resetToCheckpoint();
|
|
||||||
console.log(err);
|
|
||||||
}
|
|
||||||
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
export function decodeVarint(buffer, offset) {
|
|
||||||
let res = this.BigInt(0);
|
|
||||||
let shift = 0;
|
|
||||||
let byte = 0;
|
|
||||||
|
|
||||||
do {
|
|
||||||
if (offset >= buffer.length) {
|
|
||||||
throw new RangeError("Index out of bound decoding varint");
|
|
||||||
}
|
|
||||||
|
|
||||||
byte = buffer[offset++];
|
|
||||||
|
|
||||||
const multiplier = this.BigInt(2) ** this.BigInt(shift);
|
|
||||||
const thisByteValue = this.BigInt(byte & 0x7f) * multiplier;
|
|
||||||
shift += 7;
|
|
||||||
res = res + thisByteValue;
|
|
||||||
} while (byte >= 0x80);
|
|
||||||
|
|
||||||
return {
|
|
||||||
value: res,
|
|
||||||
length: shift / 7
|
|
||||||
};
|
|
||||||
}
|
|
@ -23,58 +23,4 @@ function leftpad(str, len, pad) {
|
|||||||
(str = new Array(len + 1 - str.length).join(pad) + str),
|
(str = new Array(len + 1 - str.length).join(pad) + str),
|
||||||
str
|
str
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
export function encode(bytes) {
|
|
||||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
||||||
let bits = 0;
|
|
||||||
let value = 0;
|
|
||||||
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];
|
|
||||||
bits -= 5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bits > 0) {
|
|
||||||
output += alphabet[(value << (5 - bits)) & 0x1F];
|
|
||||||
}
|
|
||||||
|
|
||||||
const paddingLength = (8 - (output.length % 8)) % 8;
|
|
||||||
output += '='.repeat(paddingLength);
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function base64decode(base64) {
|
|
||||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
||||||
let result = [];
|
|
||||||
let i = 0, j = 0;
|
|
||||||
let b1, b2, b3, b4;
|
|
||||||
|
|
||||||
while (i < base64.length) {
|
|
||||||
b1 = chars.indexOf(base64.charAt(i++));
|
|
||||||
b2 = chars.indexOf(base64.charAt(i++));
|
|
||||||
b3 = chars.indexOf(base64.charAt(i++));
|
|
||||||
b4 = chars.indexOf(base64.charAt(i++));
|
|
||||||
|
|
||||||
if (b1 === -1 || b2 === -1) break;
|
|
||||||
|
|
||||||
result[j++] = (b1 << 2) | (b2 >> 4);
|
|
||||||
|
|
||||||
if (b3 !== -1) {
|
|
||||||
result[j++] = ((b2 & 15) << 4) | (b3 >> 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (b4 !== -1) {
|
|
||||||
result[j++] = ((b3 & 3) << 6) | b4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result.slice(0, j);
|
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "totpfit",
|
"name": "totpfit",
|
||||||
"version": "1.1.3",
|
"version": "1.1.1",
|
||||||
"description": "Another 2FAuthenticator based on TOTP for Zepp Amazfit GTS 4",
|
"description": "Another 2FAuthenticator based on TOTP for Zepp Amazfit GTS 4",
|
||||||
"main": "app.js",
|
"main": "app.js",
|
||||||
"author": "Lisoveliy",
|
"author": "Lisoveliy",
|
||||||
|
@ -6,6 +6,7 @@ import { LocalStorage } from "@zos/storage";
|
|||||||
const app = getApp();
|
const app = getApp();
|
||||||
|
|
||||||
let waitForFetch = true;
|
let waitForFetch = true;
|
||||||
|
let localStorage = new LocalStorage();
|
||||||
|
|
||||||
Page(
|
Page(
|
||||||
BasePage({
|
BasePage({
|
||||||
@ -13,8 +14,6 @@ Page(
|
|||||||
this.getTOTPData()
|
this.getTOTPData()
|
||||||
.then((x) => {
|
.then((x) => {
|
||||||
app._options.globalData.TOTPS = JSON.parse(x) ?? [];
|
app._options.globalData.TOTPS = JSON.parse(x) ?? [];
|
||||||
|
|
||||||
let localStorage = new LocalStorage();
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"TOTPs",
|
"TOTPs",
|
||||||
JSON.stringify(app._options.globalData.TOTPS)
|
JSON.stringify(app._options.globalData.TOTPS)
|
||||||
@ -23,7 +22,6 @@ Page(
|
|||||||
})
|
})
|
||||||
.catch((x) => {
|
.catch((x) => {
|
||||||
console.log(`Init failed: ${x}`);
|
console.log(`Init failed: ${x}`);
|
||||||
let localStorage = new LocalStorage();
|
|
||||||
app._options.globalData.TOTPS = JSON.parse(
|
app._options.globalData.TOTPS = JSON.parse(
|
||||||
localStorage.getItem("TOTPs", null) ?? []
|
localStorage.getItem("TOTPs", null) ?? []
|
||||||
);
|
);
|
||||||
|
@ -5,6 +5,7 @@ let _props = null;
|
|||||||
AppSettingsPage({
|
AppSettingsPage({
|
||||||
build(props) {
|
build(props) {
|
||||||
_props = props;
|
_props = props;
|
||||||
|
|
||||||
const storage = JSON.parse(
|
const storage = JSON.parse(
|
||||||
props.settingsStorage.getItem("TOTPs") ?? "[]"
|
props.settingsStorage.getItem("TOTPs") ?? "[]"
|
||||||
);
|
);
|
||||||
@ -13,16 +14,12 @@ AppSettingsPage({
|
|||||||
placeholder: "otpauth://",
|
placeholder: "otpauth://",
|
||||||
label: "Add new OTP Link",
|
label: "Add new OTP Link",
|
||||||
onChange: (changes) => {
|
onChange: (changes) => {
|
||||||
let link = getTOTPByLink(changes);
|
var link = getTOTPByLink(changes);
|
||||||
if (link == null) {
|
if (link == null) {
|
||||||
console.log("link is invalid");
|
console.log("link is invalid");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
storage.push(link);
|
||||||
if(Array.isArray(link))
|
|
||||||
storage.push(...link);
|
|
||||||
else storage.push(link);
|
|
||||||
|
|
||||||
updateStorage(storage);
|
updateStorage(storage);
|
||||||
},
|
},
|
||||||
labelStyle: {
|
labelStyle: {
|
||||||
@ -84,11 +81,7 @@ function GetTOTPList(storage) {
|
|||||||
label: "Change OTP link",
|
label: "Change OTP link",
|
||||||
onChange: (changes) => {
|
onChange: (changes) => {
|
||||||
try {
|
try {
|
||||||
let link = getTOTPByLink(changes);
|
storage[elementId] = getTOTPByLink(changes);
|
||||||
if(Array.isArray(link))
|
|
||||||
return;
|
|
||||||
|
|
||||||
storage[elementId] = link;
|
|
||||||
updateStorage(storage);
|
updateStorage(storage);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
|
@ -1,29 +1,10 @@
|
|||||||
import { decodeProto, TYPES } from "../../lib/protobuf-decoder/protobufDecoder";
|
|
||||||
import { TOTP } from "../../lib/totp-quickjs";
|
import { TOTP } from "../../lib/totp-quickjs";
|
||||||
import { base64decode, encode } from "../../lib/totp-quickjs/base32decoder";
|
|
||||||
|
|
||||||
const otpauthScheme = "otpauth:/";
|
const otpScheme = "otpauth:/";
|
||||||
const googleMigrationScheme = "otpauth-migration:/";
|
|
||||||
|
|
||||||
export function getTOTPByLink(link) {
|
export function getTOTPByLink(link) {
|
||||||
if(link.includes(otpauthScheme))
|
|
||||||
return getByOtpauthScheme(link)
|
|
||||||
if(link.includes(googleMigrationScheme))
|
|
||||||
return getByGoogleMigrationScheme(link)
|
|
||||||
|
|
||||||
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";
|
|
||||||
}
|
|
||||||
|
|
||||||
function getByOtpauthScheme(link){
|
|
||||||
try {
|
try {
|
||||||
let args = link.split("/", otpauthScheme.length);
|
let args = link.split("/", otpScheme.length);
|
||||||
let type = args[2]; //Returns 'hotp' or 'totp'
|
let type = args[2]; //Returns 'hotp' or 'totp'
|
||||||
let issuer = args[3].split(":")[0]?.split("?")[0]; //Returns issuer
|
let issuer = args[3].split(":")[0]?.split("?")[0]; //Returns issuer
|
||||||
let client =
|
let client =
|
||||||
@ -39,12 +20,8 @@ function getByOtpauthScheme(link){
|
|||||||
|
|
||||||
if (secret === undefined) throw new Error("Secret not defined");
|
if (secret === undefined) throw new Error("Secret not defined");
|
||||||
|
|
||||||
if(issuer == client){
|
issuer = issuer.replace("%20", " ");
|
||||||
issuer = args[3].split("issuer=")[1]?.split("&")[0];
|
client = client.replace("%20", " ");
|
||||||
}
|
|
||||||
|
|
||||||
issuer = decodeURIComponent(issuer);
|
|
||||||
client = decodeURIComponent(client);
|
|
||||||
|
|
||||||
return new TOTP(
|
return new TOTP(
|
||||||
secret,
|
secret,
|
||||||
@ -56,58 +33,13 @@ function getByOtpauthScheme(link){
|
|||||||
getHashType(algorithm)
|
getHashType(algorithm)
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(err)
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getByGoogleMigrationScheme(link){
|
function getHashType(algorithm) {
|
||||||
|
if (algorithm == "SHA1") return "SHA-1";
|
||||||
let data = link.split("data=")[1]; //Returns base64 encoded data
|
if (algorithm == "SHA256") return "SHA-256";
|
||||||
data = decodeURIComponent(data);
|
if (algorithm == "SHA512") return "SHA-512";
|
||||||
let decode = base64decode(data);
|
else return "SHA-1";
|
||||||
let proto = decodeProto(decode);
|
|
||||||
|
|
||||||
let protoTotps = [];
|
|
||||||
|
|
||||||
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 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;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function bytesToString(bytes) {
|
|
||||||
let str = '';
|
|
||||||
for (let i = 0; i < bytes.length; i++) {
|
|
||||||
str += String.fromCharCode(bytes[i]);
|
|
||||||
}
|
|
||||||
return str;
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user