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 object | n_pbt_crypto |
| Used for | Checking that a file has not changed, keeping a secret in an INI, signing an order, authenticating an API call |
| Principle | A hidden page does the computing; every method is an ordinary PowerScript call that returns its value |
| Dependency | The 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 #
- Hashes and HMAC: lowercase hexadecimal, the text read as UTF-8 — Python, Java and
openssl dgstgive the same value. of_encrypt: one base64 string carrying, in this order,PBK2(4 bytes), the number of PBKDF2 rounds (4 bytes, big-endian), the salt (16), the nonce (12), the AES-256-GCM ciphertext, then its tag (16 bytes, at the end). The key is derived from the password (UTF-8) by PBKDF2-HMAC-SHA-256 with THOSE rounds —il_pbkdf2_roundsat encryption time.of_decryptwith the same password is enough, on any workstation;of_encrypt_filewrites exactly the same container, as bytes.of_password_hash:pbkdf2-sha256$rounds$salt$hash, salt and hash in standard base64. This format is PBToolboxAI's OWN: neither passlib ($pbkdf2-sha256$…) nor Django (pbkdf2_sha256$…) read it as it stands.- Keys and signatures: PEM, public in SPKI (
BEGIN PUBLIC KEY), private in PKCS#8 (BEGIN PRIVATE KEY); signatures in base64, by the family of the key — RSA: RSASSA-PKCS1-v1_5 with SHA-256; ECDSA P-256: SHA-256, in DER (openssl dgst -sha256 -verify, JavaSHA256withECDSA); Ed25519: 64 bytes.of_verifyreads an ECDSA signature in DER or in P1363 (r | s); the ES256 JWT stays in P1363, as RFC 7518 requires. - RSA-OAEP (
of_rsa_encrypt): SHA-256, MGF1 with SHA-256, no label. In Java:OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT); with openssl:-pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -pkeyopt rsa_mgf1_md:sha256. A PKCS#1 key (BEGIN RSA PRIVATE KEY) converts withopenssl pkcs8 -topk8 -nocrypt. - MD5 is offered NATIVELY (
HASH_MD5) for the legacy exchanges that require it — never for a password nor for integrity against an adversary.
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 #
| Property | Type | Default | Role |
|---|---|---|---|
is_last_error | string | "" | Why the last call returned an empty string, false or a negative code: wrong password, unreadable file, unknown algorithm, demo limit |
il_timeout_ms | long | 20000 | How long one call may take. Hashing a large file or generating a 4096-bit key can take a few seconds |
il_pbkdf2_rounds | long | 100000 | PBKDF2 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_rounds | long | 600000 | PBKDF2 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_owner | powerobject | null | The 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 #
| Method | Role | |
|---|---|---|
of_open ( ) → long | Creates 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 ( ) → boolean | True once the hidden page exists | |
of_hash (string as_algorithm, string as_text) → string | The 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) → string | of_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 }) → string | The 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 }) → boolean | True 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) → string | The 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) → string | A text in base64 (its UTF-8 bytes): what a browser or Python would write. Empty on failure | |
of_base64_decode (string as_base64) → string | The 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) → string | The 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) → long | Writes 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) → string | Encrypts 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) → string | The 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 ( ) → string | A random UUID (version 4) | |
of_random_hex (integer ai_bytes) → string | ai_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) → long | An 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) → string | The 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) → boolean | True 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 (`r | s`, 64 bytes: WebCrypto, .NET) |
of_generate_keypair (string as_kind, ref string as_public_pem, ref string as_private_pem) → long | A 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) → string | The 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) → string | URL-safe base64 (JWT, query string) | |
of_base64url_decode (string as_base64url) → string | The text back; empty when it is not base64url, or when its bytes are not a text | |
of_hex_encode (string as_text) → string | The UTF-8 bytes of a text in hexadecimal | |
of_hex_decode (string as_hex) → string | The 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) → string | A 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) → boolean | Constant-time equality: comparing a token or a code without leaking it | |
of_password_hash (string as_password) → string | What 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) → boolean | True 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 ( ) → string | A new secret for two-factor codes (RFC 6238), in base32 | |
of_totp_code (string as_secret) → string | The six-digit code of the moment, the one the authenticator shows | |
of_totp_verify (string as_secret, string as_code { , ref long al_step }) → boolean | True 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) → string | The otpauth:// URI to put in a QR code to enrol the user | |
of_generate_key ( ) → string | A new AES-256 key, 32 bytes in hexadecimal | |
of_encrypt_with_key (string as_key, string as_text) → string | AES-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) → string | The text back with the same key; empty and is_last_error otherwise | |
of_rsa_encrypt (string as_public_pem, string as_text) → string | A 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) → string | The 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 } }) → string | A 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 }) → string | The 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) → string | The claims WITHOUT verification, to read who the token names before choosing the key | |
of_jwt_header (string as_token) → string | The 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 } }) → string | A 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 }) → string | The 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) → string | The CRC32 of a text, 8 hex digits — an integrity check, not a hash | |
of_crc32_file (string as_path) → string | The 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) → long | A 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) → long | The 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 #
- One object per window, opened with it (
of_open): the hidden page costs a few hundred milliseconds the first time, nothing afterwards. - The password is not stored:
of_encryptreturns everything needed to decrypt, except it — that is the contract. - The private key does not travel: sign at home, publish the public key.
of_verifyonly needs that one. - Read
is_last_errorwhen a method returns an empty string: the reason is there, and it often says "wrong password" or "unreadable file" — not a defect of the component. - DPAPI protects from ANOTHER ACCOUNT, not from another program: anything the same user runs reads an
of_protectback (any account of the workstation withab_machine). Pass an entropy of your own application to narrow that circle. - A JWT is verified with ITS algorithm, written in the code (
n_pbt_crypto.JWT_RS256), never read from the token nor from an INI that may lack it: an empty algorithm is refused, but it is this choice that shuts the door on the HS/RS confusion. Then checkaudandisson the claims returned.