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 #
n_pbt_crypto lnv_crypto
string ls_hash, ls_cipher
lnv_crypto = create n_pbt_crypto
// The fingerprint of a text, in hexadecimal
ls_hash = lnv_crypto.of_sha256("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("s3cret", "The safe code is 4152.")
MessageBox("Back", lnv_crypto.of_decrypt("s3cret", ls_cipher))
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 the salt (16 bytes), the nonce (12) and the AES-256-GCM ciphertext; the key is derived from the password by PBKDF2-SHA-256, 100 000 rounds.of_decryptwith the same password is enough, on any workstation.- Keys: PEM, public in SPKI (
BEGIN PUBLIC KEY), private in PKCS#8 (BEGIN PRIVATE KEY); RSASSA-PKCS1-v1_5 signatures with SHA-256, in base64 — whatopensslreads and verifies. - No MD5: Web Crypto does not offer it, and nothing should ask for it any more.
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, of_password_hash and of_encrypt_file: about 100 ms. More is slower for everyone, the attacker included |
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.
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 a negative code |
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. 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 | The HMAC of a text under a secret key, in hexadecimal — what an API asks to authenticate a call |
of_hash_file (string as_algorithm, string as_path) → string | The hash of a file of the workstation, of any kind and up to 512 MB, without loading it in PowerBuilder |
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; a value that is not base64 gives an empty string and is_last_error, never garbage |
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 — up to 512 MB, read by the DLL, never loaded in PowerBuilder |
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. Returns 0 once written, -5 on an empty path or a value that is not base64, -4 when the file cannot be written |
of_encrypt (string as_password, string as_text) → string | Encrypts a text with a password: one base64 string to store. 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 |
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_*). Returns 0 once both keys are filled, -4 on failure |
of_sign (string as_private_pem, string as_text) → string | The signature of a text by the private key, in base64. 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 |
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, -4 on failure |
of_key_kind (string as_pem) → string` | The family of a PEM key: rsa, ec-p256 or ed25519 |
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 |
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 hex |
of_random_password (integer ai_length, string as_charset) → string` | A random password (4 to 256 characters) over a CHARSET_* set, without bias. Empty on a bad length |
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_pbkdf2_rounds), rounds and salt in the value. Never the same result twice |
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 |
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) → boolean` | True when the code is the one of the moment, of the previous or the next step |
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 (RSA-OAEP): only the private key reads it |
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` | A signed JWT: JWT_HS256 (shared secret), JWT_RS256/JWT_ES256/JWT_EDDSA (private key); iat added, exp when the duration is > 0. Empty on failure |
of_jwt_verify (string as_algorithm, string as_key, string as_token) → string` | The claims (JSON) of a token whose signature, algorithm and dates are right; 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_protect (string as_text { , boolean ab_machine }) → string` | A secret protected by Windows for this user (or this machine): DPAPI, native. What goes in the INI for a server password |
of_unprotect (string as_protected) → string` | The text back, on the same account; 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 |
of_encrypt_file (string as_password, string as_source, string as_target) → long` | A file encrypted by password, natively (same container as of_encrypt). Returns 0, -5 empty argument, -2 unreadable source, -4 unwritable target |
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), -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 #
string ls_expected, ls_actual
ls_expected = ProfileString("deploy.ini", "files", "orders.pbd", "")
ls_actual = lnv_crypto.of_hash_file(n_pbt_crypto.HASH_SHA256, "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("filename", "invoice_4152.pdf")
lnv_j.of_set_string("content", lnv_crypto.of_base64_encode_file("C:\invoices\4152.pdf"))
lnv_rest.of_post("https://api.example.com/documents", lnv_j.of_text())
// Receiving : the answer's attachment, back on disk
lnv_crypto.of_base64_decode_to_file(lnv_rest.of_json_value("content"), "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(ls_master, 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(ls_master, ProfileString("app.ini", "db", "password", ""))
if ls_db_password = "" then MessageBox("Login", lnv_crypto.is_last_error)
Signing an order, verifying it elsewhere #
string ls_public, ls_private, ls_signature
lnv_crypto.of_generate_keypair(n_pbt_crypto.KEY_2048, ls_public, ls_private) // once ; keep ls_private
ls_signature = lnv_crypto.of_sign(ls_private, ls_order_json)
// The public key and the signature travel with the order ; anyone can check :
if not lnv_crypto.of_verify(ls_public, ls_order_json, 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(n_pbt_crypto.JWT_HS256, ls_api_secret, '{"sub":"guillaume","role":"admin"}', 3600)
lnv_rest.of_set_bearer(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(n_pbt_crypto.JWT_HS256, ls_api_secret, ls_token)
if ls_claims = "" then MessageBox("API", lnv_crypto.is_last_error)
ls_role = gnv_utils.of_json_get_str(ls_claims, "role")
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
SetProfileString("app.ini", "db", "password", lnv_crypto.of_protect(ls_db_password))
// At run time
ls_db_password = lnv_crypto.of_unprotect(ProfileString("app.ini", "db", "password", ""))
// 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(ls_secret, ls_login, "MyApp")
// Logging in : the stored hash, then the six digits
if lnv_crypto.of_password_verify(ls_typed, ls_stored_hash) and lnv_crypto.of_totp_verify(ls_secret, ls_six_digits) then ...
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.