From bd490d46426e032b651b0d25cda5d16c1771ebb5 Mon Sep 17 00:00:00 2001 From: Savely Savianok <1986developer@gmail.com> Date: Fri, 28 Feb 2025 23:32:54 +0300 Subject: [PATCH] feat: adding protobuf decoder for parse of google auth migrations --- lib/protobuf-decoder/hexUtils.js | 36 +++++++ lib/protobuf-decoder/intUtils.js | 27 +++++ lib/protobuf-decoder/protobufDecoder.js | 132 ++++++++++++++++++++++++ lib/protobuf-decoder/varintUtils.js | 24 +++++ lib/totp-quickjs/base32decoder.js | 20 ++++ setting/utils/queryParser.js | 35 +++++-- 6 files changed, 266 insertions(+), 8 deletions(-) create mode 100644 lib/protobuf-decoder/hexUtils.js create mode 100644 lib/protobuf-decoder/intUtils.js create mode 100644 lib/protobuf-decoder/protobufDecoder.js create mode 100644 lib/protobuf-decoder/varintUtils.js diff --git a/lib/protobuf-decoder/hexUtils.js b/lib/protobuf-decoder/hexUtils.js new file mode 100644 index 0000000..6f6ff52 --- /dev/null +++ b/lib/protobuf-decoder/hexUtils.js @@ -0,0 +1,36 @@ +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(' '); diff --git a/lib/protobuf-decoder/intUtils.js b/lib/protobuf-decoder/intUtils.js new file mode 100644 index 0000000..3e16ea4 --- /dev/null +++ b/lib/protobuf-decoder/intUtils.js @@ -0,0 +1,27 @@ +import JSBI from "jsbi"; + +export function interpretAsSignedType(n) { + // see https://github.com/protocolbuffers/protobuf/blob/master/src/google/protobuf/wire_format_lite.h#L857-L876 + // however, this is a simpler equivalent formula + const isEven = JSBI.equal(JSBI.bitwiseAnd(n, JSBI.BigInt(1)), JSBI.BigInt(0)); + if (isEven) { + return JSBI.divide(n, JSBI.BigInt(2)); + } else { + return JSBI.multiply( + JSBI.BigInt(-1), + JSBI.divide(JSBI.add(n, JSBI.BigInt(1)), JSBI.BigInt(2)) + ); + } +} + +export function interpretAsTwosComplement(n, bits) { + const isTwosComplement = JSBI.equal( + JSBI.signedRightShift(n, JSBI.BigInt(bits - 1)), + JSBI.BigInt(1) + ); + if (isTwosComplement) { + return JSBI.subtract(n, JSBI.leftShift(JSBI.BigInt(1), JSBI.BigInt(bits))); + } else { + return n; + } +} diff --git a/lib/protobuf-decoder/protobufDecoder.js b/lib/protobuf-decoder/protobufDecoder.js new file mode 100644 index 0000000..f9a11af --- /dev/null +++ b/lib/protobuf-decoder/protobufDecoder.js @@ -0,0 +1,132 @@ +import { decodeVarint } from "./varintUtils"; + +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(); + } + + 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"; + } +} diff --git a/lib/protobuf-decoder/varintUtils.js b/lib/protobuf-decoder/varintUtils.js new file mode 100644 index 0000000..ceae6f5 --- /dev/null +++ b/lib/protobuf-decoder/varintUtils.js @@ -0,0 +1,24 @@ +"use bigint" +export function decodeVarint(buffer, offset) { + let res = 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 = exponentiate(BigInt(2), BigInt(shift)); + const thisByteValue = multiply(BigInt(byte & 0x7f), multiplier); + shift += 7; + res = add(res, thisByteValue); + } while (byte >= 0x80); + + return { + value: res, + length: shift / 7 + }; +} diff --git a/lib/totp-quickjs/base32decoder.js b/lib/totp-quickjs/base32decoder.js index b74d251..7c0e514 100644 --- a/lib/totp-quickjs/base32decoder.js +++ b/lib/totp-quickjs/base32decoder.js @@ -23,4 +23,24 @@ function leftpad(str, len, pad) { (str = new Array(len + 1 - str.length).join(pad) + str), str ); +} + +export function base64decode(base64String) { + var sliceSize = 1024; + var byteCharacters = window.atob(base64String); + var bytesLength = byteCharacters.length; + var slicesCount = Math.ceil(bytesLength / sliceSize); + var byteArrays = new Array(slicesCount); + + for (var sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) { + var begin = sliceIndex * sliceSize; + var end = Math.min(begin + sliceSize, bytesLength); + + var bytes = new Array(end - begin); + for (var offset = begin, i = 0; offset < end; ++i, ++offset) { + bytes[i] = byteCharacters[offset].charCodeAt(0); + } + byteArrays[sliceIndex] = new Uint8Array(bytes); + } + return byteArrays } \ No newline at end of file diff --git a/setting/utils/queryParser.js b/setting/utils/queryParser.js index 985dcde..8bb2365 100644 --- a/setting/utils/queryParser.js +++ b/setting/utils/queryParser.js @@ -1,10 +1,29 @@ +import { decodeProto } from "../../lib/protobuf-decoder/protobufDecoder"; import { TOTP } from "../../lib/totp-quickjs"; +import { base64decode } from "../../lib/totp-quickjs/base32decoder"; -const otpScheme = "otpauth:/"; +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) + + 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 { - let args = link.split("/", otpScheme.length); + 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 = @@ -42,9 +61,9 @@ export function getTOTPByLink(link) { } } -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 getByGoogleMigrationScheme(link){ + console.log("Hello") + let data = link.split("data=")[1]; //Returns secret + let decodedProto = decodeProto(base64decode(data)); + console.log(decodedProto) +} \ No newline at end of file