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. - JWK (
of_jwk_to_pem,of_pem_to_jwk,of_jwt_verify_jwks): the JSON of RFC 7517 —ktyRSA, EC (P-256, P-384) or OKP (Ed25519); a key set (JWKS) is{"keys":[…]}, each key named by itskid. - Blobs: the
of_*_blobmethods work on the BYTES, natively — the same values as for a file holding those bytes;of_encrypt_blobwrites the container ofof_encrypt.
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 |
|---|---|---|---|
il_timeout_ms | long | 20000 | How 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_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 (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_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 Raise them, and of_password_needs_rehash tells which stored values to renew |
il_jwt_leeway_seconds | long | 60 | The 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_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 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 #
| Method | Role | |
|---|---|---|
of_get_last_error ( ) → string | Why 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 ( ) → 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 (of_get_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 base64url is read too. | |
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 as_expected may also be base64url. | |
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; of_get_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 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) → 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 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) → long | The 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) → 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 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 ( ) → 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 as_scheme, string as_hash }) → 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 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 }) → 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) 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) → long | A 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) → string | The 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) → string | A 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) → string | A 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) → 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_password_needs_rehash (string as_stored) → boolean | True 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 ( ) → string | A 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 }) → string | The 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 }) → 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) 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) → boolean | The 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 }) → string | The 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) → string | The 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 ( ) → string | A new AES-256 key, 32 bytes in hexadecimal | |
of_encrypt_with_key (string as_key, string as_text { , string as_aad }) → string | AES-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 }) → string | The 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 }) → 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, 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 }) → string | The 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 } }) → 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 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 } }) → string | The 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 }) → string | The 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) → 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 of_get_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 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) → 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_base64_encode_blob (blob ab_data) → string | The 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) → long | The 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) → string | The 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 }) → string | The 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) → string | The CRC32 of the bytes of a blob, 8 hex digits. Native | |
of_encrypt_blob (string as_password, blob ab_data, ref blob ab_cipher) → long | The 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) → long | The 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 ( ) → long | Releases the hidden page; done for you when the object is destroyed Returns 0 once done. | |
of_reset ( ) → long | Back 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 #
- 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
of_get_last_error()when 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 name the issuer and the audience you expect (as_expected_claims_json): the check refuses them when they are missing.