Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
TOTP 100% qb64 Yup - sorta LOL
#1
OK I cant do it any more - months of testing and I can no longer see the forest for the trees as they say.

I've been working on adding full TOTP generation natively in QB64 — no DLLs, no Python, just raw QB64. This is for a personal PWM it ahve been working on for years, I just keep adding to it LOL, and now I want an internal 2FA.  I’ve made serious progress, but I’m hitting a brick wall with final output not matching the expected 6-digit codes (e.g., not matching 287082 with known test vectors like GEZDGNBVGY3TQOJQ... at counter 0).
https://totp.danhersam.com/

Here’s what I’ve already implemented:

- Full Base32 decoding
- SHA1 hashing (based on FIPS PUB 180-1, all QB64)
- HMAC-SHA1 (manual, with XOR'd opad/ipad)
- 8-byte big-endian counter
- All code modularized in a totp.bi file
- Verified Base32 decoded output = 20 bytes (correct)
- Confirmed HMAC length = 20 bytes (SHA1 digest)
- Offset and truncation logic implemented using RFC 4226 spec
- Constants like 30 sec interval, 6-digit truncation, etc.

I’ve tested this against Google Authenticator keys and various online generators — still getting invalid results. We even tried printing hashes in hex and stepping through the logic. Everything looks structurally sound.
I used this page for help.
https://datatracker.ietf.org/doc/html/rfc4226#section-7

I’m guessing the fault is here:

    Either QB64’s string handling is off for binary bytes

    Or something’s fishy in bit shifts / unsigned math in QB64

I’ve included the .BI file if anyone wants to tinker. I’m all ears if someone knows a way QB64 internally misbehaves with binary string math or if any fixes exist I missed. Would love to see this baked into native QB64 someday — or even just confirmed if it’s impossible without external help.
currently went back to 


Code: (Select All)
SUB GENERATE_TOTP (encryptedTOTPSecret AS STRING)
    DIM decryptedSecret AS STRING
    DIM totp_code AS STRING
    DIM cmd AS STRING
    DIM F AS INTEGER

    ' Check for missing or clearly invalid secret
    IF LEN(_TRIM$(encryptedTOTPSecret)) < 10 THEN
        LWRITELN "`4Missing or invalid TOTP secret. Cannot generate code."
        PAUSE 24
        EXIT SUB
    END IF

    ' Decrypt the stored secret
    decryptedSecret = EncryptStr$(_TRIM$(encryptedTOTPSecret), vaultKey$, "d")

    ' Build the command to call the TOTP generator
    cmd = "cmd /c py\generate_totp.exe " + decryptedSecret
    SHELL _HIDE cmd

    ' Check if output file was created
    IF NOT _FILEEXISTS("totp_code.txt") THEN
        LWRITELN "`4TOTP generation failed. Output file not found."
        PAUSE 24
        EXIT SUB
    END IF

    ' Read the generated TOTP code
    F = FREEFILE
    OPEN "totp_code.txt" FOR INPUT AS #F
    LINE INPUT #F, totp_code
    totp_code = DigitsOnly$(_TRIM$(totp_code))

    CLOSE #F

    ' Sanity check output
    IF LEN(totp_code) <> 6 OR DigitsOnly$(totp_code) <> totp_code THEN
        LWRITELN "`4TOTP generation failed. Invalid output: " + totp_code
    ELSE
        _CLIPBOARD$ = totp_code
        PRINT "TOTP code generated and copied to clipboard: " + totp_code
    END IF

    ' Clean up
    KILL "totp_code.txt"
    PAUSE 24
END SUB

With python its super easy (which I converted to .exe for portability):

Code: (Select All)
import pyotp
import sys

def generate_totp(secret_key):
    totp = pyotp.TOTP(secret_key)
    return totp.now()

if __name__ == "__main__":
    secret_key = sys.argv[1]
    totp_code = generate_totp(secret_key)
    with open("totp_code.txt", "w") as f:
        f.write(totp_code)

Thanks in advance. You guys are who I come to when the engine starts smoking.

here is all the **cough** maigc , just can't seem to pull a rabbit LOL!!

Also line: 102 I have code=0 for testing. Comment out below to see it live.  You will also need the following const if you turn this into an app to test
Code: (Select All)

' -- Constants    used for totp.bi
CONST TOTP_INTERval = 30
CONST TOTP_DIGITS = 6
CONST TOTP_TIME_OFFSET = 0 ' Adjust if system time is skewed



FILE: TOTP.BI
Code: (Select All)

' genTOTP.bi - Internal TOTP generation module for QB64


' -- Helper: Base32 decoding
FUNCTION Base32Decode$ (inputStr AS STRING)
    DIM alphabet AS STRING: alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
    DIM buffer AS STRING, outputStr AS STRING
    DIM i AS INTEGER, bits AS INTEGER, value AS LONG, ch AS STRING

    FOR i = 1 TO LEN(inputStr)
        ch = MID$(UCASE$(inputStr), i, 1)
        value = INSTR(alphabet, ch) - 1
        IF value >= 0 THEN
            buffer = buffer + RIGHT$("00000" + BIN$(value AND 31), 5)

        END IF
    NEXT

    FOR i = 1 TO LEN(buffer) STEP 8
        IF i + 7 <= LEN(buffer) THEN
            outputStr = outputStr + CHR$(VAL("&B" + MID$(buffer, i, 8)))

        END IF
    NEXT
    Base32Decode$ = outputStr
END FUNCTION

FUNCTION BIN$ (value AS _UNSIGNED LONG)
    DIM result AS STRING
    DO
        result = LTRIM$(STR$(value MOD 2)) + result
        value = value \ 2
    LOOP UNTIL value = 0
    BIN$ = result
END FUNCTION



' -- Helper: Convert number to 8-byte big-endian
FUNCTION IntToBytes$ (counter AS _UNSIGNED _INTEGER64)
    DIM i AS INTEGER
    DIM b AS STRING
    DIM byteVal AS _UNSIGNED _BYTE

    FOR i = 7 TO 0 STEP -1
        byteVal = (counter \ (2 ^ (i * 8))) AND &HFF
        b = b + CHR$(byteVal)
    NEXT

    IntToBytes$ = b
END FUNCTION

FUNCTION GetUnixTime~&&
    DIM y AS INTEGER, m AS INTEGER, d AS INTEGER
    y = VAL(LEFT$(DATE$, 4))
    m = VAL(MID$(DATE$, 6, 2))
    d = VAL(RIGHT$(DATE$, 2))

    GetUnixTime~&& = DaysSinceEpoch(y, m, d) * 86400 + TIMER
END FUNCTION

FUNCTION DaysSinceEpoch~& (y, m, d)
    DIM days AS LONG, i AS INTEGER
    DIM mdays(12) AS INTEGER
    mdays(1) = 31: mdays(2) = 28: mdays(3) = 31: mdays(4) = 30
    mdays(5) = 31: mdays(6) = 30: mdays(7) = 31: mdays(8) = 31
    mdays(9) = 30: mdays(10) = 31: mdays(11) = 30: mdays(12) = 31

    days = 0
    FOR i = 1970 TO y - 1
        IF (i MOD 4 = 0 AND i MOD 100 <> 0) OR (i MOD 400 = 0) THEN
            days = days + 366
        ELSE
            days = days + 365
        END IF
    NEXT

    IF (y MOD 4 = 0 AND y MOD 100 <> 0) OR (y MOD 400 = 0) THEN
        mdays(2) = 29
    END IF

    FOR i = 1 TO m - 1
        days = days + mdays(i)
    NEXT

    days = days + (d - 1)
    DaysSinceEpoch~& = days
END FUNCTION


' -- TOTP generation
FUNCTION GenerateTOTP$ (base32Secret$)
    DIM keyStr AS STRING, counter AS _UNSIGNED _INTEGER64
    DIM hash AS STRING, offset AS INTEGER, code AS LONG
    DIM binCounter AS STRING
    DIM unixTime AS _UNSIGNED _INTEGER64
    unixTime = GetUnixTime~&&
    counter = (unixTime + TOTP_TIME_OFFSET) \ TOTP_INTERval


    keyStr = Base32Decode$(base32Secret$)
    counter = 0
    'counter = (TIMER + TOTP_TIME_OFFSET) \ TOTP_INTERval
    binCounter$ = IntToBytes$(counter)
    hash$ = HMAC_SHA1$(keyStr$, binCounter$)




    IF LEN(hash$) = 0 THEN
        GenerateTOTP$ = "ERROR"
        EXIT FUNCTION
    END IF

    offset = ASC(RIGHT$(hash$, 1)) AND &HF
    DIM segment AS STRING
    segment = MID$(hash$, offset + 1, 4)
    code = BytesToLong~&(segment) AND &H7FFFFFFF
    code = code MOD (10 ^ TOTP_DIGITS)
    GenerateTOTP$ = RIGHT$("000000" + LTRIM$(STR$(code)), TOTP_DIGITS)
END FUNCTION

' === SHA1 Function ===
' Returns binary digest of the message
FUNCTION SHA1$ (message$)
    ' Based on FIPS PUB 180-1. Credit to open-source BASIC implementations.
    DIM A AS _UNSIGNED LONG, B AS _UNSIGNED LONG, C AS _UNSIGNED LONG
    DIM D AS _UNSIGNED LONG, E AS _UNSIGNED LONG, F AS _UNSIGNED LONG
    DIM b1 AS _UNSIGNED LONG, b2 AS _UNSIGNED LONG, b3 AS _UNSIGNED LONG, b4 AS _UNSIGNED LONG
    DIM K AS _UNSIGNED LONG, TEMP AS _UNSIGNED LONG
    DIM H0 AS _UNSIGNED LONG: H0 = &H67452301
    DIM H1 AS _UNSIGNED LONG: H1 = &HEFCDAB89
    DIM H2 AS _UNSIGNED LONG: H2 = &H98BADCFE
    DIM H3 AS _UNSIGNED LONG: H3 = &H10325476
    DIM H4 AS _UNSIGNED LONG: H4 = &HC3D2E1F0
    DIM padded AS STRING, chunk AS STRING
    DIM W(80) AS _UNSIGNED LONG
    DIM i AS INTEGER, j AS INTEGER

    ' Padding
    padded$ = message$ + CHR$(&H80)
    DO WHILE (LEN(padded$) MOD 64) <> 56
        padded$ = padded$ + CHR$(0)
    LOOP

    ' Append original length in bits
    DIM bitLen AS _UNSIGNED _INTEGER64: bitLen = LEN(message$) * 8
    FOR i = 7 TO 0 STEP -1
        padded$ = padded$ + CHR$((bitLen \ (2 ^ (i * 8))) AND &HFF)
    NEXT

    ' Process each 512-bit chunk
    FOR i = 1 TO LEN(padded$) STEP 64
        chunk = MID$(padded$, i, 64)

        ' Break chunk into 16 big-endian words
        FOR j = 0 TO 15
            b1 = ASC(MID$(chunk, j * 4 + 1, 1))
            b2 = ASC(MID$(chunk, j * 4 + 2, 1))
            b3 = ASC(MID$(chunk, j * 4 + 3, 1))
            b4 = ASC(MID$(chunk, j * 4 + 4, 1))

            W(j) = _SHL(b1, 24) OR _SHL(b2, 16) OR _SHL(b3, 8) OR b4

        NEXT

        ' Extend words
        FOR j = 16 TO 79
            W(j) = _SHL(W(j - 3) XOR W(j - 8) XOR W(j - 14) XOR W(j - 16), 1) OR _
                  _SHR(W(j - 3) XOR W(j - 8) XOR W(j - 14) XOR W(j - 16), 31)
        NEXT

        A = H0: B = H1: C = H2: D = H3: E = H4

        FOR j = 0 TO 79
            SELECT CASE j
                CASE j <= 19: F = (B AND C) OR ((NOT B) AND D): K = &H5A827999
                CASE j <= 39: F = B XOR C XOR D: K = &H6ED9EBA1
                CASE j <= 59: F = (B AND C) OR (B AND D) OR (C AND D): K = &H8F1BBCDC
                CASE ELSE: F = B XOR C XOR D: K = &HCA62C1D6
            END SELECT
            TEMP = (_SHL(A, 5) OR _SHR(A, 27)) + F + E + K + W(j)
            E = D: D = C
            C = (_SHL(B, 30) OR _SHR(B, 2))
            B = A: A = TEMP
        NEXT

        H0 = (H0 + A) AND &HFFFFFFFF
        H1 = (H1 + B) AND &HFFFFFFFF
        H2 = (H2 + C) AND &HFFFFFFFF
        H3 = (H3 + D) AND &HFFFFFFFF
        H4 = (H4 + E) AND &HFFFFFFFF
    NEXT

    ' Output 20-byte binary hash
    ' Output 20-byte binary hash
    SHA1$ = ""
    FOR i = 0 TO 4
        SELECT CASE i
            CASE 0: TEMP = H0
            CASE 1: TEMP = H1
            CASE 2: TEMP = H2
            CASE 3: TEMP = H3
            CASE 4: TEMP = H4
        END SELECT
    SHA1$ = SHA1$ + CHR$((TEMP \ 2 ^ 24) AND &HFF) + _
                    CHR$((TEMP \ 2 ^ 16) AND &HFF) + _
                    CHR$((TEMP \ 2 ^ 8) AND &HFF) + _
                    CHR$(TEMP AND &HFF)
    NEXT
END FUNCTION

FUNCTION HMAC_SHA1$ (inputKey$, inputMsg$)
    CONST BLOCK_SIZE = 64
    DIM i AS INTEGER
    DIM oPad$, iPad$, keyStr$

    ' Ensure key is 20-byte binary SHA1 if too long
    keyStr$ = inputKey$
    IF LEN(keyStr$) > BLOCK_SIZE THEN keyStr$ = SHA1$(keyStr$)

    keyStr$ = keyStr$ + STRING$(BLOCK_SIZE - LEN(keyStr$), CHR$(0))

    FOR i = 1 TO BLOCK_SIZE
        oPad$ = oPad$ + CHR$(ASC(MID$(keyStr$, i, 1)) XOR &H5C)
        iPad$ = iPad$ + CHR$(ASC(MID$(keyStr$, i, 1)) XOR &H36)
    NEXT

    ' SHA1$ must return 20-byte binary digest
    HMAC_SHA1$ = SHA1$(oPad$ + SHA1$(iPad$ + inputMsg$))
END FUNCTION

FUNCTION BytesToLong~& (s AS STRING)
    BytesToLong~& = (_SHL(ASC(MID$(s, 1, 1)), 24) OR _
                    _SHL(ASC(MID$(s, 2, 1)), 16) OR _
                    _SHL(ASC(MID$(s, 3, 1)), 8) OR _
                    ASC(MID$(s, 4, 1)))
END FUNCTION

I think we can do this - and don't tell me this has already been done, will hurt my feeling ahaha. I spent a long time on this - and it is not working
3 out of 2 people have trouble with fractions

Reply


Messages In This Thread
TOTP 100% qb64 Yup - sorta LOL - by Ra7eN - 06-02-2025, 05:51 PM
RE: TOTP 100% qb64 Yup - sorta LOL - by Ra7eN - 06-02-2025, 08:23 PM
RE: TOTP 100% qb64 Yup - sorta LOL - by Ra7eN - 06-02-2025, 08:45 PM
RE: TOTP 100% qb64 Yup - sorta LOL - by Ra7eN - 06-03-2025, 05:26 PM
RE: TOTP 100% qb64 Yup - sorta LOL - by Ra7eN - 06-03-2025, 12:00 PM
RE: TOTP 100% qb64 Yup - sorta LOL - by Jack - 06-03-2025, 07:10 PM
RE: TOTP 100% qb64 Yup - sorta LOL - by Ra7eN - 06-04-2025, 04:38 PM



Users browsing this thread: 1 Guest(s)