TOTP
A contribution to the community: use an offline TOTP authenticator for your applications. I searched the forum but didn't find anything, so I implemented this for use in my own systems; if others have similar solutions, this could be incorporated or improved.
It includes a complete example; since it's in Brazilian Portuguese, you might want to use a translator.
use with google or ms authenticator.
/*
* totp_qr.prg - Demo FiveWin: gera segredo TOTP, mostra o QR-Code na tela
* (Google Authenticator / Microsoft Authenticator) e valida
* o codigo digitado. Self-contained (usa totp.prg).
*
* QR: matriz via hbzebra -> BMP 24-bit gerado em Harbour (escala inteira,
* zona quieta) -> exibido 1:1 num TBitmap (sem gaps, sem corte, sem stretch).
* SOMENTE ASCII.
*/
#include "fivewin.ch"
PROCEDURE Main()
LOCAL cIssuer := "GaiaDemo"
LOCAL cAccount := "operador@empresa"
LOCAL cRegFile := hb_DirBase() + "totp_registro.txt"
// Loop: abre no modo certo conforme o estado. Botoes que mudam o estado
// (registrar / refazer cadastro) fecham e reabrem no outro modo.
DO WHILE RunScreen( cIssuer, cAccount, cRegFile )
ENDDO
RETURN
STATIC FUNCTION RunScreen( cIssuer, cAccount, cRegFile )
LOCAL oDlg, oFont, oFontB, oFontM, oSayNow, oTmr, oGet
LOCAL cSecret := EnrollLoadOrNew( cRegFile, cAccount )
LOCAL cURI := TOTP_AuthURI( cIssuer, cAccount, cSecret )
LOCAL cCode := Space( 6 )
LOCAL cBmp := hb_DirBase() + "qr.bmp"
LOCAL nDim := WriteQRBmp( cBmp, cURI, 240 )
LOCAL lReg := ( EnrollStatusCode( cRegFile, cAccount ) == "REGISTRADO" )
LOCAL lReopen := .F.
DEFINE FONT oFont NAME "Segoe UI" SIZE 0, -13
DEFINE FONT oFontB NAME "Segoe UI" SIZE 0, -15 BOLD
DEFINE FONT oFontM NAME "Consolas" SIZE 0, -13
DEFINE DIALOG oDlg ;
TITLE IIF( lReg, "Login 2FA (TOTP)", "Cadastro 2FA (TOTP)" ) ;
SIZE 400, 460 PIXEL FONT oFont
IF lReg
// ===================== MODO LOGIN (ja cadastrado) =====================
@ 10, 10 SAY "MODO LOGIN - 2FA ja cadastrado (sem QR)" SIZE 360, 16 PIXEL OF oDlg FONT oFontB
@ 30, 10 SAY "Conta: " + cAccount SIZE 360, 13 PIXEL OF oDlg
@ 52, 10 SAY "Abra o app autenticador e digite o codigo atual:" SIZE 360, 13 PIXEL OF oDlg FONT oFontB
@ 70, 10 GET oGet VAR cCode PICTURE "999999" SIZE 60, 18 PIXEL OF oDlg FONT oFontM
@ 69, 78 BUTTON "Entrar" SIZE 100, 20 PIXEL OF oDlg ;
ACTION DoLogin( cSecret, cCode )
@ 102, 10 SAY "Codigo atual (DEMO, p/ testar sem celular):" SIZE 360, 13 PIXEL OF oDlg
@ 118, 10 SAY oSayNow PROMPT TOTP_Now( cSecret ) SIZE 120, 18 PIXEL OF oDlg FONT oFontB
@ 148, 10 SAY "Status: " + EnrollStatus( cRegFile, cAccount ) SIZE 360, 13 PIXEL OF oDlg FONT oFontB
@ 176, 10 BUTTON "Refazer cadastro (apagar 2FA)" SIZE 190, 20 PIXEL OF oDlg ;
ACTION ( ResetEnroll( cRegFile, cAccount ), lReopen := .T., oDlg:End() )
ELSE
// ===================== MODO CADASTRO (ativar 2FA) =====================
@ 10, 10 SAY "MODO CADASTRO - ative o 2FA" SIZE 360, 16 PIXEL OF oDlg FONT oFontB
@ 30, 10 SAY "Conta: " + cAccount + " Emissor: " + cIssuer SIZE 360, 13 PIXEL OF oDlg
@ 46, 10 SAY "Chave manual: " + cSecret SIZE 360, 13 PIXEL OF oDlg FONT oFontM
@ 70, 10 BUTTON "1) Mostrar QR p/ escanear" SIZE 160, 22 PIXEL OF oDlg ;
ACTION ShowQRWin( cBmp, nDim )
@ 106, 10 SAY "2) Digite o codigo p/ confirmar o cadastro:" SIZE 360, 13 PIXEL OF oDlg FONT oFontB
@ 122, 10 GET oGet VAR cCode PICTURE "999999" SIZE 60, 18 PIXEL OF oDlg FONT oFontM
@ 121, 78 BUTTON "Validar e registrar" SIZE 140, 20 PIXEL OF oDlg ;
ACTION IIF( DoEnroll( cRegFile, cAccount, cSecret, cCode ), ;
( lReopen := .T., oDlg:End() ), NIL )
@ 152, 10 SAY "Codigo atual (DEMO, p/ testar sem celular):" SIZE 360, 13 PIXEL OF oDlg
@ 168, 10 SAY oSayNow PROMPT TOTP_Now( cSecret ) SIZE 120, 18 PIXEL OF oDlg FONT oFontB
@ 194, 10 SAY "Status: " + EnrollStatus( cRegFile, cAccount ) SIZE 360, 13 PIXEL OF oDlg FONT oFontB
ENDIF
DEFINE TIMER oTmr INTERVAL 1000 OF oDlg ;
ACTION ( oSayNow:SetText( TOTP_Now( cSecret ) ) )
ACTIVATE TIMER oTmr
ACTIVATE DIALOG oDlg CENTERED
RELEASE FONT oFont, oFontB, oFontM
RETURN lReopen
// Janelinha modal SO com o QR (renderiza limpo: nenhum controle na faixa Y).
// Usuario escaneia e fecha; volta p/ a tela principal validar.
STATIC FUNCTION ShowQRWin( cBmp, nDim )
LOCAL oQ, hBmp := ReadBitmap( 0, cBmp )
IF hBmp == 0 ; RETURN NIL ; ENDIF
DEFINE DIALOG oQ TITLE "Escaneie no autenticador e feche" ;
SIZE nDim + 56, nDim + 90 PIXEL
oQ:bPainted := {| hDC | DrawBitmap( hDC, hBmp, 18, 18, nDim, nDim ) }
ACTIVATE DIALOG oQ CENTERED
DeleteObject( hBmp )
RETURN NIL
// ---------------------------------------------------------------------------
// ENROLLMENT / persistencia (arquivo texto: conta|segredo|status|quando)
// ---------------------------------------------------------------------------
STATIC FUNCTION EnrollLoadOrNew( cFile, cAccount )
LOCAL aReg := EnrollLoad( cFile ), nPos, cSecret
nPos := AScan( aReg, {| r | r[ 1 ] == cAccount } )
IF nPos > 0
RETURN aReg[ nPos ][ 2 ] // reusa segredo ja gerado
ENDIF
cSecret := TOTP_NewSecret() // 1a vez: gera e salva como PENDENTE
AAdd( aReg, { cAccount, cSecret, "PENDENTE", "" } )
EnrollSave( cFile, aReg )
RETURN cSecret
// CADASTRO: valida o 1o codigo e marca REGISTRADO. Retorna .T. se registrou.
STATIC FUNCTION DoEnroll( cFile, cAccount, cSecret, cCode )
LOCAL aReg, nPos
IF !TOTP_Validate( cSecret, cCode )
MsgStop( "Codigo invalido. Confira a hora do dispositivo." + hb_eol() + ;
"(o codigo muda a cada 30s)", "Falhou" )
RETURN .F.
ENDIF
aReg := EnrollLoad( cFile )
nPos := AScan( aReg, {| r | r[ 1 ] == cAccount } )
IF nPos > 0
aReg[ nPos ][ 3 ] := "REGISTRADO"
aReg[ nPos ][ 4 ] := DToC( Date() ) + " " + Time()
EnrollSave( cFile, aReg )
ENDIF
MsgInfo( "Codigo VALIDO. 2FA ATIVADO para " + cAccount + "." + hb_eol() + ;
"Da proxima vez voce entra so com o codigo (sem QR).", "Cadastro OK" )
RETURN .T.
// LOGIN recorrente: valida o codigo contra o segredo guardado. SEM QR, sem regravar.
STATIC FUNCTION DoLogin( cSecret, cCode )
IF TOTP_Validate( cSecret, cCode )
MsgInfo( "Codigo correto. ACESSO LIBERADO.", "Login OK" )
ELSE
MsgStop( "Codigo invalido. Use o codigo ATUAL do app (muda a cada 30s).", "Negado" )
ENDIF
RETURN NIL
// Apaga o cadastro da conta -> volta p/ MODO CADASTRO (novo segredo/QR).
STATIC FUNCTION ResetEnroll( cFile, cAccount )
LOCAL aReg := EnrollLoad( cFile )
LOCAL nPos := AScan( aReg, {| r | r[ 1 ] == cAccount } )
IF nPos > 0
hb_ADel( aReg, nPos, .T. )
EnrollSave( cFile, aReg )
ENDIF
RETURN NIL
STATIC FUNCTION EnrollStatusCode( cFile, cAccount )
LOCAL aReg := EnrollLoad( cFile )
LOCAL nPos := AScan( aReg, {| r | r[ 1 ] == cAccount } )
RETURN IIF( nPos > 0, aReg[ nPos ][ 3 ], "PENDENTE" )
STATIC FUNCTION EnrollStatus( cFile, cAccount )
LOCAL aReg := EnrollLoad( cFile )
LOCAL nPos := AScan( aReg, {| r | r[ 1 ] == cAccount } )
RETURN IIF( nPos > 0, aReg[ nPos ][ 3 ] + ;
IIF( Empty( aReg[ nPos ][ 4 ] ), "", " (" + aReg[ nPos ][ 4 ] + ")" ), "PENDENTE" )
STATIC FUNCTION EnrollLoad( cFile )
LOCAL cTxt := hb_MemoRead( cFile ), aOut := {}, aLines, c, aF
aLines := hb_ATokens( cTxt, Chr( 10 ) )
FOR EACH c IN aLines
c := StrTran( c, Chr( 13 ), "" )
IF Empty( c ) ; LOOP ; ENDIF
aF := hb_ATokens( c, "|" )
DO WHILE Len( aF ) < 4 ; AAdd( aF, "" ) ; ENDDO
AAdd( aOut, { aF[ 1 ], aF[ 2 ], aF[ 3 ], aF[ 4 ] } )
NEXT
RETURN aOut
STATIC FUNCTION EnrollSave( cFile, aReg )
LOCAL c := "", r
FOR EACH r IN aReg
c += r[ 1 ] + "|" + r[ 2 ] + "|" + r[ 3 ] + "|" + r[ 4 ] + hb_eol()
NEXT
hb_MemoWrit( cFile, c )
RETURN NIL
// ---------------------------------------------------------------------------
// Gera a matriz do QR (array de strings "0"/"1") via hbzebra. nMods por ref.
STATIC FUNCTION QRMatrix( cURI, nMods )
LOCAL pQR, nW := 0, nH := 0, aRows, y
pQR := hb_zebra_create_qrcode( cURI, 0 )
IF Empty( pQR ) .OR. hb_zebra_geterror( pQR ) != 0
nMods := 0 ; RETURN NIL
ENDIF
hb_zebra_draw( pQR, {| x, y, w, h | nW := Max( nW, x + w ), nH := Max( nH, y + h ) }, 0, 0, 1, 1 )
aRows := Array( nH )
FOR y := 1 TO nH ; aRows[ y ] := Replicate( "0", nW ) ; NEXT
hb_zebra_draw( pQR, {| x, y, w, h | FillRows( aRows, x, y, w, h ) }, 0, 0, 1, 1 )
hb_zebra_destroy( pQR )
nMods := nH // QR e quadrado (nW == nH)
RETURN aRows
STATIC FUNCTION FillRows( aRows, x, y, w, h )
LOCAL i, j
FOR j := y TO y + h - 1
FOR i := x TO x + w - 1
aRows[ j + 1 ] := Stuff( aRows[ j + 1 ], i + 1, 1, "1" )
NEXT
NEXT
RETURN NIL
// ---------------------------------------------------------------------------
// Escreve um BMP 24-bit do QR (escala inteira + zona quieta). Retorna o lado px.
STATIC FUNCTION WriteQRBmp( cFile, cURI, nTargetPx )
LOCAL nMods := 0, aRows := QRMatrix( cURI, @nMods )
LOCAL nQuiet := 4, nUnits, nScale, nDim, nRowBytes, nPad, cPad
LOCAL cW := Chr(255) + Chr(255) + Chr(255) // pixel branco (BGR)
LOCAL cB := Chr(0) + Chr(0) + Chr(0) // pixel preto
LOCAL aOut, u, x, cRow, cPix, cData := "", nFileSize, cHdr, i
IF nMods == 0 ; RETURN 0 ; ENDIF
nUnits := nMods + 2 * nQuiet
nScale := Max( 3, Int( nTargetPx / nUnits ) )
nDim := nUnits * nScale
nRowBytes := nDim * 3
nPad := ( 4 - ( nRowBytes % 4 ) ) % 4
cPad := Replicate( Chr(0), nPad )
// pixel-row (com padding) de cada unidade vertical, top->bottom
aOut := Array( nUnits )
FOR u := 1 TO nUnits
IF u <= nQuiet .OR. u > nQuiet + nMods
cPix := Replicate( cW, nDim ) // zona quieta (linha toda)
ELSE
cRow := aRows[ u - nQuiet ]
cPix := Replicate( cW, nQuiet * nScale ) // quieta esquerda
FOR x := 1 TO nMods
cPix += Replicate( IIF( SubStr( cRow, x, 1 ) == "1", cB, cW ), nScale )
NEXT
cPix += Replicate( cW, nQuiet * nScale ) // quieta direita
ENDIF
aOut[ u ] := cPix + cPad
NEXT
// BMP e bottom-up: de baixo p/ cima, cada unidade repetida nScale vezes
FOR u := nUnits TO 1 STEP -1
FOR i := 1 TO nScale ; cData += aOut[ u ] ; NEXT
NEXT
nFileSize := 54 + Len( cData )
cHdr := "BM" + L32( nFileSize ) + L32( 0 ) + L32( 54 ) + ;
L32( 40 ) + L32( nDim ) + L32( nDim ) + L16( 1 ) + L16( 24 ) + L32( 0 ) + ;
L32( Len( cData ) ) + L32( 2835 ) + L32( 2835 ) + L32( 0 ) + L32( 0 )
hb_MemoWrit( cFile, cHdr + cData )
RETURN nDim
STATIC FUNCTION L32( n )
RETURN Chr( n % 256 ) + Chr( Int( n / 256 ) % 256 ) + ;
Chr( Int( n / 65536 ) % 256 ) + Chr( Int( n / 16777216 ) % 256 )
STATIC FUNCTION L16( n )
RETURN Chr( n % 256 ) + Chr( Int( n / 256 ) % 256 )/*
* totp.prg - nucleo TOTP/HOTP (RFC 6238 / 4226) - Harbour puro
* Usa hb_HMAC_SHA1 do core (hbrtl). SEM OpenSSL. SOMENTE ASCII (compat CP1252).
* Provado contra os vetores da RFC 6238 Apendice B.
*/
#define TOTP_STEP 30
#define TOTP_DIGITS 6
// ---- Valida o codigo digitado. nWindow=1 tolera +-30s de relogio fora de sync.
FUNCTION TOTP_Validate( cSecretB32, cCode, nWindow )
LOCAL nCounter := Int( UnixTimeUTC() / TOTP_STEP ), w
hb_default( @nWindow, 1 )
cCode := AllTrim( cCode )
FOR w := -nWindow TO nWindow
IF cCode == HOTP( Base32Decode( cSecretB32 ), nCounter + w, TOTP_DIGITS )
RETURN .T.
ENDIF
NEXT
RETURN .F.
// ---- Codigo TOTP do instante atual
FUNCTION TOTP_Now( cSecretB32 )
RETURN HOTP( Base32Decode( cSecretB32 ), Int( UnixTimeUTC() / TOTP_STEP ), TOTP_DIGITS )
// ---- Segredo aleatorio em Base32 (p/ enrollment/QR)
FUNCTION TOTP_NewSecret( nBytes )
LOCAL cRaw := "", i
hb_default( @nBytes, 20 )
FOR i := 1 TO nBytes
cRaw += Chr( hb_RandomInt( 0, 255 ) )
NEXT
RETURN Base32Encode( cRaw )
// ---- URI otpauth:// p/ o QR (string que Google/MS Authenticator leem)
FUNCTION TOTP_AuthURI( cIssuer, cAccount, cSecretB32 )
RETURN "otpauth://totp/" + cIssuer + ":" + cAccount + ;
"?secret=" + cSecretB32 + "&issuer=" + cIssuer + ;
"&digits=" + LTrim( Str( TOTP_DIGITS ) ) + ;
"&period=" + LTrim( Str( TOTP_STEP ) ) + "&algorithm=SHA1"
// ---- HOTP RFC 4226: cKey ja em bytes, nCounter inteiro
FUNCTION HOTP( cKey, nCounter, nDigits )
LOCAL cMsg := "", nC := nCounter, i, cHash, nOff, nBin, nOtp
FOR i := 1 TO 8 // contador -> 8 bytes big-endian
cMsg := Chr( nC % 256 ) + cMsg
nC := Int( nC / 256 )
NEXT
// hb_HMAC_SHA1 devolve 40 chars HEX -> converter p/ 20 bytes raw
cHash := hb_HexToStr( hb_HMAC_SHA1( cMsg, cKey ) )
nOff := Asc( SubStr( cHash, 20, 1 ) ) % 16
nBin := ( Asc( SubStr( cHash, nOff + 1, 1 ) ) % 128 ) * 16777216 + ;
Asc( SubStr( cHash, nOff + 2, 1 ) ) * 65536 + ;
Asc( SubStr( cHash, nOff + 3, 1 ) ) * 256 + ;
Asc( SubStr( cHash, nOff + 4, 1 ) )
nOtp := nBin % ( 10 ^ nDigits )
RETURN PadL( LTrim( Str( nOtp, 18 ) ), nDigits, "0" )
// ---- Segundos Unix em UTC (TOTP exige UTC, nao hora local)
FUNCTION UnixTimeUTC()
LOCAL nLocal := Round( ( hb_DateTime() - hb_SToT( "19700101000000" ) ) * 86400, 0 )
RETURN nLocal - hb_UTCOffset()
// ---- Base32 decode (RFC 4648)
FUNCTION Base32Decode( cB32 )
LOCAL cAlf := "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
LOCAL cUp := Upper( cB32 ), c, n, i, nBuf := 0, nBits := 0, cOut := ""
FOR i := 1 TO Len( cUp )
c := SubStr( cUp, i, 1 )
IF c == "=" .OR. c == " " ; LOOP ; ENDIF
n := At( c, cAlf ) - 1
IF n < 0 ; LOOP ; ENDIF
nBuf := nBuf * 32 + n
nBits += 5
IF nBits >= 8
nBits -= 8
cOut += Chr( Int( nBuf / ( 2 ^ nBits ) ) % 256 )
nBuf := nBuf % ( 2 ^ nBits )
ENDIF
NEXT
RETURN cOut
// ---- Base32 encode (RFC 4648)
FUNCTION Base32Encode( cRaw )
LOCAL cAlf := "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
LOCAL i, nBuf := 0, nBits := 0, cOut := ""
FOR i := 1 TO Len( cRaw )
nBuf := nBuf * 256 + Asc( SubStr( cRaw, i, 1 ) )
nBits += 8
DO WHILE nBits >= 5
nBits -= 5
cOut += SubStr( cAlf, ( Int( nBuf / ( 2 ^ nBits ) ) % 32 ) + 1, 1 )
nBuf := nBuf % ( 2 ^ nBits )
ENDDO
NEXT
IF nBits > 0
cOut += SubStr( cAlf, ( ( nBuf * ( 2 ^ ( 5 - nBits ) ) ) % 32 ) + 1, 1 )
ENDIF
RETURN cOut@echo off
setlocal
REM ===========================================================================
REM build.bat - compila a demo TOTP+QR (FiveWin 32-bit / MSVC).
REM *** EDITE OS 3 CAMINHOS ABAIXO para o SEU ambiente e rode: build.bat ***
REM ===========================================================================
REM 1) Pasta do Harbour (que contem \bin\hbmk2.exe, \include, \lib, \contrib)
set "HB_DIR=H:\DEVAI\_UtlAI\Harbour32msvc"
REM 2) Pasta da FiveWin (que contem \include e \lib com FiveH32.lib)
set "FWH_DIR=H:\DEVAI\_UtlAI\FWH260632"
REM 3) Script que prepara o MSVC de 32-bit no ambiente (vcvars32.bat ou similar).
REM Ex. Visual Studio: "C:\Program Files\...\VC\Auxiliary\Build\vcvars32.bat"
set "MSVC_ENV=H:\DEVAI\_UtlAI\msvc\setup_x86.bat"
REM ===========================================================================
echo Preparando MSVC 32-bit...
call "%MSVC_ENV%" >nul 2>&1
if errorlevel 1 ( echo [ERRO] MSVC_ENV falhou: %MSVC_ENV% & exit /b 1 )
REM Limpa variaveis que poderiam forcar arquitetura/compilador errados
set "HB_CPU="
set "HB_COMPILER="
set "HBMK2="
set "HBMK_OPTIONS="
set "HB_INSTALL_PREFIX=%HB_DIR%"
cd /d "%~dp0"
if exist totp_qr.exe del /q totp_qr.exe
echo Compilando...
REM (sem -inc: a compilacao incremental cacheia e ignora mudancas de layout)
"%HB_DIR%\bin\hbmk2.exe" totp_qr.hbp -comp=msvc -cpu=x86 ^
-I"%FWH_DIR%\include" -L"%FWH_DIR%\lib"
if exist totp_qr.exe (
echo.
echo [OK] Gerado: totp_qr.exe -- rode: totp_qr.exe
exit /b 0
) else (
echo.
echo [FALHOU] veja os erros acima.
exit /b 1
)/* qrdump.prg - gera o MESMO QR da demo e despeja a matriz em texto (0/1),
* p/ o teste automatizado decodificar com opencv e conferir a URI.
* Console; usa hbzebra (mesma engine que a FiveWin usa via FWZEBRA). */
#include "simpleio.ch"
PROCEDURE Main()
LOCAL cSecret := TOTP_NewSecret()
LOCAL cURI := TOTP_AuthURI( "GaiaDemo", "operador@empresa", cSecret )
LOCAL pQR, nW := 0, nH := 0, aGrid, x, y
pQR := hb_zebra_create_qrcode( cURI, 0 )
IF Empty( pQR ) .OR. hb_zebra_geterror( pQR ) != 0
OutStd( "ERRO QR" + hb_eol() ) ; RETURN
ENDIF
// 1a passada: medir o tamanho (modulos) com pin=1
hb_zebra_draw( pQR, {| x, y, w, h | nW := Max( nW, x + w ), nH := Max( nH, y + h ) }, 0, 0, 1, 1 )
aGrid := Array( nH )
FOR y := 1 TO nH ; aGrid[ y ] := Replicate( "0", nW ) ; NEXT
// 2a passada: marcar os modulos pretos
hb_zebra_draw( pQR, {| x, y, w, h | FillGrid( aGrid, x, y, w, h ) }, 0, 0, 1, 1 )
OutStd( "URI:" + cURI + hb_eol() )
OutStd( "TOTP_NOW:" + TOTP_Now( cSecret ) + hb_eol() )
FOR y := 1 TO nH ; OutStd( aGrid[ y ] + hb_eol() ) ; NEXT
hb_zebra_destroy( pQR )
RETURN
STATIC FUNCTION FillGrid( aGrid, x, y, w, h )
LOCAL i, j
FOR j := y TO y + h - 1
FOR i := x TO x + w - 1
aGrid[ j + 1 ] := Stuff( aGrid[ j + 1 ], i + 1, 1, "1" )
NEXT
NEXT
RETURN NIL# Projeto FiveWin 32-bit (MSVC) - app TOTP + QR
-gui
totp_qr.prg
totp.prg
-o${hb_targetname}
# --- FiveWin ---
-lFiveH32
-lFiveHC32
# --- QR-Code (hbzebra, puxado pelo REQUEST FWZEBRA) ---
-lhbzebra
# --- libs de sistema exigidas pela FiveWin/MSVC32 ---
-lgdiplus
-lole32
-lOleDlg
-lversion
-luxtheme
-lmsvcrt
-llegacy_stdio_definitions
# --- contribs Harbour usadas pela FiveWin ---
xhb.hbc
hbct.hbc
hbwin.hbc
hbmzip.hbc
hbziparc.hbc
hbtip.hbc
# --- CRT: usa o dinamico msvcrt, remove o estatico ---
-ldflag=/NODEFAULTLIB:libcmt
-dFWH_CC_MSVC32