06-02-2025, 05:51 PM
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
With python its super easy (which I converted to .exe for portability):
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
FILE: TOTP.BI
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
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