PBToolboxAI v4 ← Site

crypto — n_pbt_crypto #

← Component reference · Guide contents

Hashes, HMAC, password encryption, random values and RSA signatures — the Web Crypto API of the WebView2 engine, which your PowerBuilder 10 has nowhere else. No third-party DLL, no service: what the workstation already knows how to do, offered to PowerScript.

▶ See it live — Demo application, Crypto tile: the results, the code behind them and this page, side by side.


At a glance #

Nonvisual objectn_pbt_crypto
Used forChecking that a file has not changed, keeping a secret in an INI, signing an order, authenticating an API call
PrincipleA hidden page does the computing; every method is an ordinary PowerScript call that returns its value
DependencyThe WebView2 runtime, already required by the library — nothing else

Quick start #

// Local variables
n_pbt_crypto lnv_crypto
string ls_hash, ls_cipher

// Create the object once, free it when done
lnv_crypto = create n_pbt_crypto

// The fingerprint of a text, in hexadecimal
ls_hash = lnv_crypto.of_sha256(/*text*/ "Invoice 4152, 690.00 EUR")

// A secret kept with a password : one string to store, the password to keep
ls_cipher = lnv_crypto.of_encrypt(/*password*/ "s3cret", /*text*/ "The safe code is 4152.")
MessageBox("Back", lnv_crypto.of_decrypt(/*password*/ "s3cret", /*cipher*/ ls_cipher))

// Free the object
destroy lnv_crypto

Every method is a call that returns its value: no event, no waiting to write.


The formats, readable by other tools #

Decrypting a value of of_encrypt on the server side, in Python (the cryptography library):

import base64, struct
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives.ciphers.aead import AESGCM

raw = base64.b64decode(value)
assert raw[:4] == b"PBK2"
rounds = struct.unpack(">I", raw[4:8])[0]
salt, nonce, data = raw[8:24], raw[24:36], raw[36:]   # data = ciphertext + 16-byte tag
key = PBKDF2HMAC(hashes.SHA256(), 32, salt, rounds).derive(password.encode("utf-8"))
text = AESGCM(key).decrypt(nonce, data, None).decode("utf-8")

Properties #

PropertyTypeDefaultRole
is_last_errorstring""Why the last call returned an empty string, false or a negative code: wrong password, unreadable file, unknown algorithm, demo limit
il_timeout_mslong20000How long one call may take. Hashing a large file or generating a 4096-bit key can take a few seconds
il_pbkdf2_roundslong100000PBKDF2 rounds of of_encrypt and of_encrypt_file: about 100 ms, paid at every read of the value. 1,000 to 10,000,000: any other value makes the call REFUSE (is_last_error says so, -5 for a file), never replaced
il_password_hash_roundslong600000PBKDF2 rounds of of_password_hash: 600,000, the OWASP figure for PBKDF2-HMAC-SHA-256 (about 0.6 s). A stored hash is what an attacker forces offline if the table leaks; it is checked once per login. Same bounds
ipo_ownerpowerobjectnullThe visual object this helper works for: the licence is checked on its class. Set it before the first call. Needed only in the demonstration application; a development or runtime key unlocks the helper without it. Not unlocked, it runs in demo mode — texts of 2048 characters and files of 2048 bytes at most, going in as well as coming out

Constants: HASH_MD5, HASH_SHA1, HASH_SHA256, HASH_SHA384, HASH_SHA512 for the algorithm; KEY_2048, KEY_3072, KEY_4096 for the size of an RSA key, KEY_RSA_2048, KEY_EC_P256, KEY_ED25519 for the kind of a pair; JWT_HS256, JWT_RS256, JWT_ES256, JWT_EDDSA; CHARSET_ALL, CHARSET_ALPHANUM, CHARSET_LETTERS, CHARSET_DIGITS, CHARSET_HEX; KEYFORMAT_TEXT, KEYFORMAT_HEX, KEYFORMAT_BASE64 for the form of an HMAC key.


Methods #

MethodRole
of_open ( ) → longCreates the hidden page. Optional — every method does it — but calling it when the window opens pays the cost once. Returns the handle (> 0) or -2 when it could not be created (is_last_error says why)
of_is_open ( ) → booleanTrue once the hidden page exists
of_hash (string as_algorithm, string as_text) → stringThe hash of a text, in hexadecimal; HASH_* for the algorithm, read without case, dashes or spaces (SHA-256 is HASH_SHA256). NATIVE for every algorithm: no page is opened for a hash. Empty on failure
of_sha256 (string as_text) → stringof_hash with HASH_SHA256 — the one you will use most
of_hmac (string as_algorithm, string as_key, string as_text { , string as_key_format }) → stringThe HMAC of a text under a secret key, in hexadecimal — what an API asks to authenticate a call. The key is TEXT by default; KEYFORMAT_HEX or KEYFORMAT_BASE64 give its BYTES: a secret handed out encoded, or the result of a previous HMAC (AWS SigV4 chains four). An empty key is refused, and so is a PEM key: it is never an HMAC secret
of_hmac_verify (string as_algorithm, string as_key, string as_text, string as_expected { , string as_key_format }) → booleanTrue when as_expected is the HMAC of as_text under as_key, compared in CONSTANT TIME — checking a webhook signature (Stripe, GitHub, Shopify) without leaking it, never if of_hmac(…) = header. as_expected in hexadecimal (any case) or base64; strip a prefix such as sha256= first. False otherwise
of_hash_file (string as_algorithm, string as_path) → stringThe hash of a file of the workstation, of any type and any size (read by pieces), without loading it into PowerBuilder. A RELATIVE path is looked for in the current folder, then in the folder the application started in, then next to the EXE — the rule of every file of this component. Empty on failure; is_last_error tells an unknown algorithm from an unreadable file
of_base64_encode (string as_text) → stringA text in base64 (its UTF-8 bytes): what a browser or Python would write. Empty on failure
of_base64_decode (string as_base64) → stringThe TEXT back (UTF-8). A value that is not base64, or whose bytes are not a text (a PDF, a picture), returns an empty string and is_last_error — never garbage. Bytes are written to a file with of_base64_decode_to_file
of_base64_encode_file (string as_path) → stringThe bytes of a file of the workstation in base64 — an attachment, a picture for a JSON call — read by the DLL, never loaded in PowerBuilder. Up to 64 MB in a 32-bit application (the PowerBuilder IDE is one), 512 MB in 64 bits. Empty on failure; is_last_error says which: a file too large for this process, unreadable, or the demo limit
of_base64_decode_to_file (string as_base64, string as_path) → longWrites the bytes of a base64 value to a file — the attachment an API answered with. An EMPTY value writes an empty file; a RELATIVE path is written in the folder the application started in (the current folder when the library was loaded, the same in the IDE and compiled), where of_hash_file finds it again. Returns 0 once written, -5 on an empty path or one that depends on a drive's current folder (\x, C:x), or a value that is not base64, -4 when the file cannot be written or, in demo, over 2048 bytes
of_encrypt (string as_password, string as_text) → stringEncrypts a text with a password: one base64 string to store, carrying its PBKDF2 rounds (see The formats). An EMPTY password is refused — anyone would read the value back. Empty on failure
of_decrypt (string as_password, string as_cipher) → stringThe text back, with the same password. Wrong password or altered value: empty string and is_last_error, never garbage. A value whose content is not a text (a file encrypted by of_encrypt_file) returns empty too: of_decrypt_file
of_uuid ( ) → stringA random UUID (version 4)
of_random_hex (integer ai_bytes) → stringai_bytes random bytes (1 to 4096) in hexadecimal: a salt, a token
of_generate_keypair (integer ai_bits, ref string as_public_pem, ref string as_private_pem) → longAn RSA key pair in PEM (KEY_2048, KEY_3072, KEY_4096). Returns 0 once both keys are filled, -5 on any other size (both stay empty), -4 on failure
of_sign (string as_private_pem, string as_text) → stringThe signature of a text by the private key, in base64. The key says its family: RSA (RSASSA-PKCS1-v1_5, SHA-256), ECDSA P-256 (SHA-256, a DER signature — what openssl dgst and Java SHA256withECDSA verify) or Ed25519 (64 bytes). Empty on failure
of_verify (string as_public_pem, string as_text, string as_signature) → booleanTrue when the signature was made on exactly this text by the matching private key; false — not an error — when the text changed. An ECDSA signature is read in DER (openssl, Java) or in P1363 (`rs`, 64 bytes: WebCrypto, .NET)
of_generate_keypair (string as_kind, ref string as_public_pem, ref string as_private_pem) → longA key pair by KIND: KEY_RSA_2048/3072/4096, KEY_EC_P256 (short signatures) or KEY_ED25519. Returns 0, -5 on a kind that is none of these (never an RSA pair instead), -4 on failure
of_key_kind (string as_pem) → stringThe family of a PEM key: rsa, ec-p256 or ed25519; only the FIRST block of the text is read. A PKCS#1 key (BEGIN RSA PRIVATE KEY), a certificate or an encrypted key is NAMED in is_last_error, with the openssl command that converts it
of_base64url_encode (string as_text) → stringURL-safe base64 (JWT, query string)
of_base64url_decode (string as_base64url) → stringThe text back; empty when it is not base64url, or when its bytes are not a text
of_hex_encode (string as_text) → stringThe UTF-8 bytes of a text in hexadecimal
of_hex_decode (string as_hex) → stringThe text back; empty when it is not hexadecimal, or when its bytes are not a text
of_random_password (integer ai_length, string as_charset) → stringA random password (4 to 256 characters) over a CHARSET_* set (empty = CHARSET_ALL), without bias. Empty on a bad length or an unknown set
of_equals_constant_time (string as_a, string as_b) → booleanConstant-time equality: comparing a token or a code without leaking it
of_password_hash (string as_password) → stringWhat to STORE for a password: salted PBKDF2 (il_password_hash_rounds, 600,000 rounds), rounds and salt in the value. Never the same result twice. An empty password hashes (checking an empty password is legitimate). The format is PBToolboxAI's OWN (see The formats)
of_password_verify (string as_password, string as_stored) → booleanTrue when the password is the one of the stored value, compared in constant time. A value whose rounds fall outside 1 000 to 10 000 000 is refused: a hostile value cannot make the check run for minutes
of_totp_secret ( ) → stringA new secret for two-factor codes (RFC 6238), in base32
of_totp_code (string as_secret) → stringThe six-digit code of the moment, the one the authenticator shows
of_totp_verify (string as_secret, string as_code { , ref long al_step }) → booleanTrue when the code is the one of this moment, of the previous step or of the next. With al_step, the 30-second STEP the code was accepted for (0 otherwise): a code must not be used twice (RFC 6238) — keep the last step accepted for the user and refuse a step that is not greater. A secret holding anything but A-Z, 2-7, spaces or = is refused (a 0 typed for an O)
of_totp_uri (string as_secret, string as_account, string as_issuer) → stringThe otpauth:// URI to put in a QR code to enrol the user
of_generate_key ( ) → stringA new AES-256 key, 32 bytes in hexadecimal
of_encrypt_with_key (string as_key, string as_text) → stringAES-256-GCM with an EXPLICIT key (hex or base64): base64 of nonce + ciphertext + tag, what openssl or Python decrypt
of_decrypt_with_key (string as_key, string as_cipher) → stringThe text back with the same key; empty and is_last_error otherwise
of_rsa_encrypt (string as_public_pem, string as_text) → stringA short secret encrypted to a PUBLIC key: only the private one reads it. The EXACT settings, for the other side: RSA-OAEP, SHA-256, MGF1 with SHA-256, no label — Java (OAEPWithSHA-256AndMGF1Padding) and openssl pkeyutl take MGF1-SHA-1 by default, see The formats. 190 bytes at most with a 2048-bit key; beyond, is_last_error says so — encrypt the data with of_encrypt_with_key and only its key this way. A PRIVATE key given for the public one is named
of_rsa_decrypt (string as_private_pem, string as_cipher) → stringThe secret back, with the private key
of_jwt_sign (string as_algorithm, string as_key, string as_claims_json, long al_expires_seconds { , string as_key_format { , string as_header_json } }) → stringA signed JWT: JWT_HS256 (shared secret), JWT_RS256/JWT_ES256/JWT_EDDSA (private key); the algorithm is REQUIRED. iat added, exp when the duration is > 0 — an exp in the claims AND a duration is refused — and an exp/iat/nbf given must be a number. The header names the algorithm as registered (EdDSA). as_key_format: the HS256 secret as KEYFORMAT_TEXT, KEYFORMAT_HEX or KEYFORMAT_BASE64; a PEM key is never an HS256 secret. as_header_json: members ADDED to the header, for instance {"kid":"2026-09"} — the key an API picks (Apple, any provider that rotates its keys); alg and typ stay the library's. Empty on failure
of_jwt_verify (string as_algorithm, string as_key, string as_token { , string as_key_format }) → stringThe claims (JSON) of a token whose signature, algorithm and dates are right. The algorithm is REQUIRED and compared EXACTLY with the header's: "" is refused, and a PEM key is never an HS256 secret — otherwise a PUBLIC key served as the secret and a forged token passed. An exp or nbf that is not a number, and a crit header, are refused. The audience (aud) and the issuer (iss) remain YOURS to check on the claims returned. as_key_format as for of_jwt_sign. Empty and is_last_error otherwise
of_jwt_claims (string as_token) → stringThe claims WITHOUT verification, to read who the token names before choosing the key
of_jwt_header (string as_token) → stringThe header (JSON) of a token WITHOUT verification: read its kid to choose the key that will verify it. Never trust it alone
of_protect (string as_text { , boolean ab_machine { , string as_entropy } }) → stringA secret protected by Windows for this user (or this machine): DPAPI, native. What goes in the INI for a server password. It does NOT keep it from another program run by the same user (nor, with ab_machine, from another account of the workstation): as_entropy, a secret of your application, narrows that circle — the same entropy is needed to read it back
of_unprotect (string as_protected { , string as_entropy }) → stringThe text back, on the same account and with the SAME entropy when one was given; empty and is_last_error elsewhere
of_crc32 (string as_text) → stringThe CRC32 of a text, 8 hex digits — an integrity check, not a hash
of_crc32_file (string as_path) → stringThe CRC32 of a file, read by the DLL to its end: a read that fails half-way is an error, never the CRC of the first part
of_encrypt_file (string as_password, string as_source, string as_target) → longA file encrypted by password, natively and by pieces (the same container as of_encrypt), up to 64 GB — the limit of one AES-GCM container. Relative paths: the source is looked for as for of_hash_file, the target is written in the folder the application started in. Returns 0, -5 empty argument or il_pbkdf2_rounds out of bounds, -2 unreadable source, -4 target cannot be written or, in demo, source over 2048 bytes, -7 source over 64 GB
of_decrypt_file (string as_password, string as_source, string as_target) → longThe file back. Returns 0, -3 wrong password or altered file (nothing is written: the text is checked in a hidden temporary file that Windows deletes even if the application dies), -5/-2/-4 as of_encrypt_file
of_close ( )Releases the hidden page; done for you when the object is destroyed
of_reset ( )Back to the defaults

Examples #

Checking that a file has not changed #

// Local variables
string ls_expected, ls_actual

// The fingerprint recorded at deploy time, and the one of the file on this workstation
ls_expected = ProfileString("deploy.ini", "files", "orders.pbd", "")
ls_actual = lnv_crypto.of_hash_file(/*algorithm*/ n_pbt_crypto.HASH_SHA256, /*path*/ "C:\MyApp\orders.pbd")
if ls_actual <> ls_expected then MessageBox("Deploy", "orders.pbd is not the file that was tested.")

An attachment in base64 for an API #

// Sending : the file's bytes in the JSON body
lnv_j.of_set_string(/*path*/ "filename", /*value*/ "invoice_4152.pdf")
lnv_j.of_set_string(/*path*/ "content", /*value*/ lnv_crypto.of_base64_encode_file(/*path*/ "C:\invoices\4152.pdf"))
lnv_rest.of_post(/*url*/ "https://api.example.com/documents", /*body*/ lnv_j.of_text())

// Receiving : the answer's attachment, back on disk
lnv_crypto.of_base64_decode_to_file(/*base64*/ lnv_rest.of_json_value(/*path*/ "content"), /*path*/ "C:\inbox\receipt.pdf")

Keeping a secret in an INI #

// At setup : encrypt once, store the string
SetProfileString("app.ini", "db", "password", lnv_crypto.of_encrypt(/*password*/ ls_master, /*text*/ ls_db_password))

// At run time : the master password comes from the user, the INI gives the rest
ls_db_password = lnv_crypto.of_decrypt(/*password*/ ls_master, /*cipher*/ ProfileString("app.ini", "db", "password", ""))
if ls_db_password = "" then MessageBox("Login", lnv_crypto.is_last_error)

Signing an order, verifying it elsewhere #

// Local variables
string ls_public, ls_private, ls_signature

// Sign the order with the private key
lnv_crypto.of_generate_keypair(/*bits*/ n_pbt_crypto.KEY_2048, /*public_pem*/ ls_public, /*private_pem*/ ls_private)   // once ; keep ls_private
ls_signature = lnv_crypto.of_sign(/*private_pem*/ ls_private, /*text*/ ls_order_json)

// The public key and the signature travel with the order ; anyone can check :
if not lnv_crypto.of_verify(/*public_pem*/ ls_public, /*text*/ ls_order_json, /*signature*/ ls_signature) then MessageBox("Order", "This order was altered.")

A token for an API, and its verification #

// The client : a token valid one hour, in the Authorization header
ls_token = lnv_crypto.of_jwt_sign(/*algorithm*/ n_pbt_crypto.JWT_HS256, /*key*/ ls_api_secret, /*claims_json*/ '{"sub":"jdoe","role":"admin"}', /*expires_seconds*/ 3600)
lnv_rest.of_set_bearer(/*token*/ ls_token)

// The server side (or a check of what came back) : the claims, once the signature and the expiry are right
ls_claims = lnv_crypto.of_jwt_verify(/*algorithm*/ n_pbt_crypto.JWT_HS256, /*key*/ ls_api_secret, /*token*/ ls_token)
if ls_claims = "" then MessageBox("API", lnv_crypto.is_last_error)
ls_role = gnv_utils.of_json_get_str(/*json*/ ls_claims, /*key*/ "role")

Checking a webhook signature #

// The service signs the raw body with the shared secret and sends the HMAC in a header
// (GitHub : "sha256=<hex>", Stripe : hex, Shopify : base64). Compare in CONSTANT time.
ls_signature = ls_header
if Left(ls_signature, 7) = "sha256=" then ls_signature = Mid(ls_signature, 8)
if not lnv_crypto.of_hmac_verify(/*algorithm*/ n_pbt_crypto.HASH_SHA256, /*key*/ ls_webhook_secret, /*text*/ ls_raw_body, /*expected*/ ls_signature) then
	MessageBox("Webhook", "This call was not sent by the service.")
	return
end if

A server password kept by Windows, and a two-factor user #

// At setup : the INI holds a value Windows protects for this account, never the password ;
// the entropy, a secret of the application, keeps other programs of the same user out
SetProfileString("app.ini", "db", "password", lnv_crypto.of_protect(/*text*/ ls_db_password, /*machine*/ false, /*entropy*/ "MyApp-7f3a"))

// At run time : the same entropy
ls_db_password = lnv_crypto.of_unprotect(/*protected*/ ProfileString("app.ini", "db", "password", ""), /*entropy*/ "MyApp-7f3a")

// Enrolling a user : a secret in the database, the QR on screen
ls_secret = lnv_crypto.of_totp_secret()
uo_qr.is_data = lnv_crypto.of_totp_uri(/*secret*/ ls_secret, /*account*/ ls_login, /*issuer*/ "MyApp")

// Logging in : the stored hash, then the six digits - and never the same code twice (RFC 6238)
if lnv_crypto.of_password_verify(/*password*/ ls_typed, /*stored*/ ls_stored_hash) and lnv_crypto.of_totp_verify(/*secret*/ ls_secret, /*code*/ ls_six_digits, /*step*/ ll_step) then
	if ll_step > ll_last_step then ll_last_step = ll_step   // store it with the user ; accept the login
end if

Good practice #

← Component reference · Guide contents