PBToolboxAI v5 ← 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
il_timeout_mslong20000How long one call to the hidden page may take: generating a 4096-bit key, or deriving a key with millions of PBKDF2 rounds, can take a few seconds. The hashes, the files and the blobs are native: no timeout applies to them
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 (of_get_last_error() says so, -5 for a file), never replaced Also for of_encrypt_blob. A value carries its rounds: changing them never makes an older value unreadable
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 Raise them, and of_password_needs_rehash tells which stored values to renew
il_jwt_leeway_secondslong60The tolerance of of_jwt_verify and of_jwt_verify_jwks on exp and nbf, in seconds: the clocks of two machines drift. 0 to 86,400; any other value makes the check refuse
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 Blobs follow the cap of the files

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_RSA_3072, KEY_RSA_4096, KEY_EC_P256, KEY_EC_P384, KEY_ED25519 for the kind of a pair; JWT_HS256, JWT_HS384, JWT_HS512, JWT_RS256, JWT_RS384, JWT_RS512, JWT_PS256, JWT_PS384, JWT_PS512, JWT_ES256, JWT_ES384, JWT_EDDSA; SIGN_PKCS1, SIGN_PSS for the scheme of an RSA signature; 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_get_last_error ( ) → stringWhy the last call returned an empty string, false or a negative code: wrong password, unreadable file, unknown algorithm, demo limit A NULL argument counts as an empty one: the reason is never NULL.
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 (of_get_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 base64url is read too.
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 as_expected may also be base64url.
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; of_get_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 of_get_last_error() — never garbage. Bytes are written to a file with of_base64_decode_to_file Or of_base64_decode_blob, which returns them in a blob.
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 32 MB in a 32-bit application (the PowerBuilder IDE is one), 512 MB in 64 bits; beyond, of_base64_encode_file_to_file. Empty on failure; of_get_last_error() says which: a file too large for this process, unreadable, or the demo limit
of_base64_encode_file_to_file (string as_source, string as_target) → longThe base64 of a file written to ANOTHER file (ASCII, no line break), by the DLL, in constant memory and whatever the size: what a large upload sends. Paths as for of_hash_file. Returns 0, -5 on an empty path or a target that depends on a drive's current folder (\x, C:x), -2 unreadable source, -4 target cannot be written or, in demo, source over 2048 bytes
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 of_get_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 or of_decrypt_blob on its bytes.
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 { , string as_scheme, string as_hash }) → 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 as_scheme and as_hash: what the other side expects — SIGN_PSS for an RSA key (salt of the hash length, MGF1 with the same hash), HASH_SHA384 or HASH_SHA512; an empty text keeps the key's own; an EC key takes a hash, never PSS; an Ed25519 key takes neither. A PUBLIC key given instead is named
of_verify (string as_public_pem, string as_text, string as_signature { , string as_scheme, string as_hash }) → 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) With the scheme and hash the signer used (see of_sign`).
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 or KEY_EC_P384 (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, ec-p384 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 of_get_last_error(), with the openssl command that converts it
of_jwk_to_pem (string as_jwk_json) → stringA key published as a JWK (RFC 7517: kty RSA, EC P-256 / P-384 or OKP Ed25519) in PEM, what of_verify, of_jwt_verify and openssl take: public in SPKI, private in PKCS#8 when the JWK holds d. Its usage members (alg, use, key_ops, kid) are not part of the key. Empty on failure
of_pem_to_jwk (string as_pem) → stringA PEM key as a JWK (JSON): what a key set publishes, or what an API asks to register. A private PEM gives a private JWK (with d): keep it to yourself. Empty on failure
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_password_needs_rehash (string as_stored) → booleanTrue when a value stored by of_password_hash carries FEWER rounds than il_password_hash_rounds: just after a successful of_password_verify, when the password is known, hash it again and store the new value — the OWASP advice when the cost goes up. False otherwise; false with of_get_last_error() when as_stored is no such value. No page
of_totp_secret ( ) → stringA new secret for two-factor codes (RFC 6238), in base32
of_totp_code (string as_secret { , string as_hash, integer ai_digits, integer ai_period }) → stringThe six-digit code of the moment, the one the authenticator shows With settings: HASH_SHA1, HASH_SHA256 or HASH_SHA512, 6 to 8 digits, a period of 1 to 3,600 seconds — the ones of_totp_uri writes
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) Spaces the user typed ("864 010", as the app shows it) are ignored.
of_totp_verify (string as_secret, string as_code, string as_hash, integer ai_digits, integer ai_period, ref long al_step) → booleanThe same check with the settings of of_totp_code; al_step counts periods of ai_period seconds
of_totp_uri (string as_secret, string as_account, string as_issuer { , string as_hash, integer ai_digits, integer ai_period }) → stringThe otpauth:// URI to put in a QR code to enrol the user The secret is written in base32 capitals, without spaces or =; a secret holding any other character is refused. With the settings of of_totp_code, the app shows the codes it computes with them
of_hotp_code (string as_secret, long al_counter) → stringThe six-digit code of a COUNTER (HOTP, RFC 4226, SHA-1): a hardware token, a code sent by mail — the server keeps the counter and raises it at each code accepted. al_counter: 0 or more. Empty on failure
of_generate_key ( ) → stringA new AES-256 key, 32 bytes in hexadecimal
of_encrypt_with_key (string as_key, string as_text { , string as_aad }) → stringAES-256-GCM with an EXPLICIT key (hex or base64): base64 of nonce + ciphertext + tag, what openssl or Python decrypt as_aad: ADDITIONAL DATA, authenticated but not encrypted — an order number, a user id: the value only decrypts with the same. The key may also be base64url
of_decrypt_with_key (string as_key, string as_cipher { , string as_aad }) → stringThe text back with the same key; empty and of_get_last_error() otherwise With the SAME additional data when there was some.
of_rsa_encrypt (string as_public_pem, string as_text { , string as_hash }) → 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, of_get_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 as_hash: the hash of OAEP and of MGF1 — HASH_SHA1 is what openssl pkeyutl, .NET (OaepSHA1) and Java (OAEPPadding) use by default
of_rsa_decrypt (string as_private_pem, string as_cipher { , string as_hash }) → stringThe secret back, with the private key With the hash the sender used (HASH_SHA256 by default).
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 Also JWT_HS384/HS512, JWT_RS384/RS512, JWT_PS256/PS384/PS512 and JWT_ES384 (key KEY_EC_P384). A key of the wrong family, or its PUBLIC half, is named
of_jwt_verify (string as_algorithm, string as_key, string as_token { , string as_key_format { , string as_expected_claims_json } }) → stringThe claims (JSON) of a token whose signature, algorithm and dates are right (il_jwt_leeway_seconds of tolerance, one minute by default). The algorithm is REQUIRED and compared EXACTLY with the header's: "" is refused, and a PEM key is never an HS 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. as_key_format as for of_jwt_sign. as_expected_claims_json: the claims that must be there with that value — the issuer (iss), the audience (aud, found in a list too); a missing or different claim is refused and named. Empty and of_get_last_error() otherwise; read a claim with n_pbt_json (of_load, then of_get_string)
of_jwt_verify_jwks (string as_algorithm, string as_jwks_json, string as_token { , string as_expected_claims_json }) → stringThe claims of a token checked against a KEY SET (JWKS, RFC 7517) — what Azure AD, Google, Auth0 or Keycloak publish at their jwks_uri (fetch it with the restclient, keep it a few hours): the token's kid picks the key. A token without a kid, a kid the set does not hold or holds twice, a key for another algorithm or another use (use = enc): refused, of_get_last_error() says which. Dates and expected claims as for of_jwt_verify
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 of_get_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 A target that depends on a drive's current folder (\x, C:x) returns -5, and of_get_last_error() says so
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_base64_encode_blob (blob ab_data) → stringThe bytes of a blob in base64 — a photo read by SelectBlob, a PDF received — without a file. Native. Up to 32 MB in 32 bits, 512 MB in 64 bits; in demo, 2048 bytes. Empty on failure
of_base64_decode_blob (string as_base64, ref blob ab_data) → longThe BYTES of a base64 (or base64url) value in a blob — an attachment received, ready for UpdateBlob. Native. Returns 0 once ab_data is filled, -5 when the value is not base64, -4 in demo over 2048 bytes; ab_data is empty then
of_hash_blob (string as_algorithm, blob ab_data) → stringThe hash of the bytes of a blob, with the algorithms of of_hash — the same value as of_hash_file on the same bytes. Native. Empty on failure
of_hmac_blob (string as_algorithm, string as_key, blob ab_data { , string as_key_format }) → stringThe HMAC of the bytes of a blob under a secret key, the key as for of_hmac: a file received, checked against the signature the service sent with it. Native. Empty on failure
of_crc32_blob (blob ab_data) → stringThe CRC32 of the bytes of a blob, 8 hex digits. Native
of_encrypt_blob (string as_password, blob ab_data, ref blob ab_cipher) → longThe bytes of a blob encrypted by password: the container of of_encrypt and of_encrypt_file, as bytes (52 more than the data) — read in base64, it decrypts with of_decrypt, and the reverse. Native, il_pbkdf2_rounds. Returns 0 once ab_cipher is filled, -5 empty password or rounds out of bounds, -4 on failure or, in demo, over 2048 bytes
of_decrypt_blob (string as_password, blob ab_cipher, ref blob ab_data) → longThe bytes back from of_encrypt_blob (or from a value of of_encrypt read as bytes), with the same password. Returns 0 once ab_data is filled, -3 wrong password or altered bytes (ab_data stays empty), -5 empty password or bytes that are no such value, -4 in demo over 2048 bytes
of_close ( ) → longReleases the hidden page; done for you when the object is destroyed Returns 0 once done.
of_reset ( ) → longBack to the defaults: il_timeout_ms, il_pbkdf2_rounds, il_password_hash_rounds, il_jwt_leeway_seconds, of_get_last_error(); ipo_owner stays Returns 0 once done.

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 #

// Local variables
n_pbt_json lnv_j
n_pbt_restclient lnv_rest

// The REST client of the window
lnv_rest = create n_pbt_restclient

// 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")

// Free the client
destroy lnv_rest

Keeping a secret in an INI #

// Local variables
string ls_master, ls_db_password

// 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.of_get_last_error())

Signing an order, verifying it elsewhere #

// Local variables
string ls_public, ls_private, ls_signature, ls_order_json

// The order to sign, as the application writes it
ls_order_json = '{"order":4152,"amount":690.00}'

// 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 #

// Local variables
string ls_api_secret, ls_token, ls_claims, ls_role
n_pbt_restclient lnv_rest
n_pbt_json lnv_json

// The REST client of the window, and the secret the API shares with you
lnv_rest = create n_pbt_restclient
ls_api_secret = ProfileString("app.ini", "api", "secret", "")

// 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.of_get_last_error())

// One claim, read with n_pbt_json
lnv_json.of_load(/*json*/ ls_claims)
ls_role = lnv_json.of_get_string(/*path*/ "role")
destroy lnv_rest

Checking a webhook signature #

// Local variables
string ls_header, ls_signature, ls_raw_body, ls_webhook_secret

// ls_raw_body and ls_header : the body and the signature header your web service received ;
// the secret, the one the service shares with you
ls_webhook_secret = ProfileString("app.ini", "webhook", "secret", "")

// 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 #

// Local variables
string ls_db_password, ls_secret, ls_login, ls_typed, ls_stored_hash, ls_six_digits
long ll_step, ll_last_step

// ls_login, ls_typed and ls_six_digits come from the login window ; ls_secret,
// ls_stored_hash and ll_last_step from the record of the 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

Verifying a token of Azure AD, Google or Auth0 (JWKS) #

// Local variables
n_pbt_restclient lnv_rest
string ls_jwks, ls_token, ls_claims

// ls_token : the token the user brought (the Authorization header). The key set of the identity provider : fetched once, kept a few hours
lnv_rest = create n_pbt_restclient
if lnv_rest.of_get(/*url*/ "https://login.microsoftonline.com/common/discovery/v2.0/keys") = 200 then ls_jwks = lnv_rest.of_response_text()
destroy lnv_rest

// The kid of the token picks the key ; the issuer and the audience must be yours
ls_claims = lnv_crypto.of_jwt_verify_jwks(/*algorithm*/ n_pbt_crypto.JWT_RS256, /*jwks_json*/ ls_jwks, /*token*/ ls_token, /*expected_claims_json*/ '{"iss":"https://login.microsoftonline.com/my-tenant-id/v2.0","aud":"api://orders"}')
if ls_claims = "" then MessageBox("Login", lnv_crypto.of_get_last_error())

A photo from the database, in base64 (blob) #

// Local variables
blob lb_photo
string ls_answer
n_pbt_json lnv_j

// ls_answer : the base64 an API answered. The photo of the employee, read from the database
SELECTBLOB photo INTO :lb_photo FROM employee WHERE emp_id = 4152 USING SQLCA;

// Its bytes in base64 in the JSON body of a call, with its fingerprint
lnv_j.of_set_string(/*path*/ "photo", /*value*/ lnv_crypto.of_base64_encode_blob(/*data*/ lb_photo))
lnv_j.of_set_string(/*path*/ "sha256", /*value*/ lnv_crypto.of_hash_blob(/*algorithm*/ n_pbt_crypto.HASH_SHA256, /*data*/ lb_photo))

// The other way : the bytes an API answered, back into the database
if lnv_crypto.of_base64_decode_blob(/*base64*/ ls_answer, /*data*/ lb_photo) = 0 then
	UPDATEBLOB employee SET photo = :lb_photo WHERE emp_id = 4152 USING SQLCA;
end if

Good practice #

← Component reference · Guide contents