PBToolboxAI v4 ← Site

crypto — n_pbt_crypto #

← Référence des composants · Sommaire du guide

Empreintes, HMAC, chiffrement par mot de passe, valeurs aléatoires et signatures RSA — la Web Crypto API du moteur WebView2, que votre PowerBuilder 10 n'a nulle part ailleurs. Aucune DLL tierce, aucun service : ce que le poste sait déjà faire, offert à PowerScript.

▶ Le voir en vrai — Application de démonstration, tuile Crypto : les résultats, le code qui les produit et cette page, côte à côte.


En bref #

Objet non visueln_pbt_crypto
Sert àVérifier qu'un fichier n'a pas changé, garder un secret dans un INI, signer une commande, authentifier un appel d'API
PrincipeUne page cachée fait le calcul ; chaque méthode est un appel PowerScript ordinaire qui rend sa valeur
DépendanceLe runtime WebView2, déjà requis par la bibliothèque — rien d'autre

Démarrage rapide #

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

Chaque méthode est un appel qui rend sa valeur : pas d'événement, pas d'attente à écrire.


Les formats, lisibles par les autres outils #

Déchiffrer une valeur d'of_encrypt côté serveur, en Python (bibliothèque cryptography) :

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

Propriétés #

PropriétéTypeDéfautRôle
is_last_errorstring""Pourquoi le dernier appel a rendu une chaîne vide, false ou un code négatif : mauvais mot de passe, fichier illisible, algorithme inconnu, limite démo
il_timeout_mslong20000Durée maximale d'un appel. Hacher un gros fichier ou générer une clé de 4096 bits peut prendre quelques secondes
il_pbkdf2_roundslong100000Tours PBKDF2 d'of_encrypt et d'of_encrypt_file : environ 100 ms, payés à chaque lecture de la valeur. De 1 000 à 10 000 000 : une autre valeur fait REFUSER l'appel (is_last_error le dit, -5 pour un fichier), jamais remplacer
il_password_hash_roundslong600000Tours PBKDF2 d'of_password_hash : 600 000, le chiffre de l'OWASP pour PBKDF2-HMAC-SHA-256 (environ 0,6 s). Une empreinte stockée est ce qu'un attaquant force hors ligne si la table fuit ; elle ne se vérifie qu'une fois par connexion. Mêmes bornes
ipo_ownerpowerobjectnullL'objet visuel pour lequel cet utilitaire travaille : la licence se vérifie sur sa classe. À poser avant le premier appel. Nécessaire seulement dans l'application de démonstration ; une clé de développement ou d'exécution débride l'utilitaire sans lui. Non débridé, il est en mode démo — textes de 2048 caractères et fichiers de 2048 octets au plus, à l'entrée comme à la sortie

Constantes : HASH_MD5, HASH_SHA1, HASH_SHA256, HASH_SHA384, HASH_SHA512 pour l'algorithme ; KEY_2048, KEY_3072, KEY_4096 pour la taille d'une clé RSA, KEY_RSA_2048, KEY_EC_P256, KEY_ED25519 pour la sorte d'une paire ; JWT_HS256, JWT_RS256, JWT_ES256, JWT_EDDSA ; CHARSET_ALL, CHARSET_ALPHANUM, CHARSET_LETTERS, CHARSET_DIGITS, CHARSET_HEX ; KEYFORMAT_TEXT, KEYFORMAT_HEX, KEYFORMAT_BASE64 pour la forme d'une clé HMAC.


Méthodes #

MéthodeRôle
of_open ( ) → longCrée la page cachée. Facultatif — chaque méthode le fait — mais l'appeler à l'ouverture de la fenêtre paie le coût une fois. Rend le handle (> 0) ou -2 si elle n'a pas pu être créée (is_last_error dit pourquoi)
of_is_open ( ) → booleanVrai dès que la page cachée existe
of_hash (string as_algorithm, string as_text) → stringL'empreinte d'un texte, en hexadécimal ; HASH_* pour l'algorithme, lu sans casse, tirets ni espaces (SHA-256 vaut HASH_SHA256). NATIVE pour tous les algorithmes : aucune page n'est ouverte pour une empreinte. Vide en cas d'échec
of_sha256 (string as_text) → stringof_hash avec HASH_SHA256 — celui qu'on utilise le plus
of_hmac (string as_algorithm, string as_key, string as_text { , string as_key_format }) → stringLe HMAC d'un texte sous une clé secrète, en hexadécimal — ce qu'une API demande pour authentifier un appel. La clé est un TEXTE par défaut ; KEYFORMAT_HEX ou KEYFORMAT_BASE64 en donnent les OCTETS : un secret fourni encodé, ou le résultat d'un HMAC précédent (AWS SigV4 en enchaîne quatre). Une clé vide est refusée, et une clé PEM aussi : ce n'est jamais un secret HMAC
of_hmac_verify (string as_algorithm, string as_key, string as_text, string as_expected { , string as_key_format }) → booleanVrai si as_expected est le HMAC de as_text sous as_key, comparé en TEMPS CONSTANT — la vérification d'une signature de webhook (Stripe, GitHub, Shopify) sans la trahir, jamais if of_hmac(…) = en-tête. as_expected en hexadécimal (casse ignorée) ou en base64 ; ôtez d'abord un préfixe comme sha256=. Faux sinon
of_hash_file (string as_algorithm, string as_path) → stringL'empreinte d'un fichier du poste, de n'importe quel type et de n'importe quelle taille (lu par morceaux), sans le charger dans PowerBuilder. Un chemin RELATIF se cherche dans le dossier courant, puis dans le dossier où l'application a démarré, puis à côté de l'EXE — la règle de tous les fichiers de ce composant. Vide en cas d'échec ; is_last_error distingue l'algorithme inconnu du fichier illisible
of_base64_encode (string as_text) → stringUn texte en base64 (ses octets UTF-8) : ce qu'un navigateur ou Python écrirait. Vide en cas d'échec
of_base64_decode (string as_base64) → stringLe TEXTE de retour (UTF-8). Une valeur qui n'est pas du base64, ou dont les octets ne sont pas un texte (un PDF, une image), rend une chaîne vide et is_last_error — jamais du bruit. Des octets s'écrivent dans un fichier par of_base64_decode_to_file
of_base64_encode_file (string as_path) → stringLes octets d'un fichier du poste en base64 — une pièce jointe, une image pour un appel JSON — lus par la DLL, jamais chargés dans PowerBuilder. Jusqu'à 64 Mo dans une application 32 bits (l'IDE PowerBuilder en est une), 512 Mo en 64 bits. Vide en cas d'échec ; is_last_error dit lequel : fichier trop gros pour ce processus, illisible, ou limite démo
of_base64_decode_to_file (string as_base64, string as_path) → longÉcrit les octets d'une valeur base64 dans un fichier — la pièce jointe qu'une API a répondue. Une valeur VIDE écrit un fichier vide ; un chemin RELATIF s'écrit dans le dossier où l'application a démarré (le dossier courant au chargement de la bibliothèque, le même dans l'IDE et compilé), là où of_hash_file le retrouve. Rend 0 une fois écrit, -5 sur un chemin vide ou qui dépend du dossier courant d'un lecteur (\x, C:x), ou une valeur qui n'est pas du base64, -4 si le fichier ne peut pas être écrit ou, en démo, au-delà de 2048 octets
of_encrypt (string as_password, string as_text) → stringChiffre un texte avec un mot de passe : une seule chaîne base64 à stocker, qui porte ses tours PBKDF2 (voir Les formats). Un mot de passe VIDE est refusé — n'importe qui relirait la valeur. Vide en cas d'échec
of_decrypt (string as_password, string as_cipher) → stringLe texte de retour, avec le même mot de passe. Mauvais mot de passe ou valeur altérée : chaîne vide et is_last_error, jamais du bruit. Une valeur dont le contenu n'est pas un texte (un fichier chiffré par of_encrypt_file) rend vide aussi : of_decrypt_file
of_uuid ( ) → stringUn UUID aléatoire (version 4)
of_random_hex (integer ai_bytes) → stringai_bytes octets aléatoires (1 à 4096) en hexadécimal : un sel, un jeton
of_generate_keypair (integer ai_bits, ref string as_public_pem, ref string as_private_pem) → longUne paire RSA en PEM (KEY_2048, KEY_3072, KEY_4096). Rend 0 une fois les deux clés remplies, -5 sur toute autre taille (les deux restent vides), -4 en cas d'échec
of_sign (string as_private_pem, string as_text) → stringLa signature d'un texte par la clé privée, en base64. La clé dit sa famille : RSA (RSASSA-PKCS1-v1_5, SHA-256), ECDSA P-256 (SHA-256, signature en DER — ce que openssl dgst et Java SHA256withECDSA vérifient) ou Ed25519 (64 octets). Vide en cas d'échec
of_verify (string as_public_pem, string as_text, string as_signature) → booleanVrai si la signature a été faite sur exactement ce texte par la clé privée correspondante ; faux — pas une erreur — si le texte a changé. Une signature ECDSA se lit en DER (openssl, Java) ou en P1363 (`rs`, 64 octets : WebCrypto, .NET)
of_generate_keypair (string as_kind, ref string as_public_pem, ref string as_private_pem) → longUne paire par SORTE : KEY_RSA_2048/3072/4096, KEY_EC_P256 (signatures courtes) ou KEY_ED25519. Rend 0, -5 sur une sorte qui n'est aucune de celles-ci (jamais une paire RSA à la place), -4 en cas d'échec
of_key_kind (string as_pem) → stringLa famille d'une clé PEM : rsa, ec-p256 ou ed25519 ; seul le PREMIER bloc du texte est lu. Une clé PKCS#1 (BEGIN RSA PRIVATE KEY), un certificat ou une clé chiffrée est NOMMÉ dans is_last_error, avec la commande openssl qui la convertit
of_base64url_encode (string as_text) → stringbase64 sûr pour une URL (JWT, chaîne de requête)
of_base64url_decode (string as_base64url) → stringLe texte de retour ; vide si ce n'est pas du base64url, ou si ses octets ne sont pas un texte
of_hex_encode (string as_text) → stringLes octets UTF-8 d'un texte en hexadécimal
of_hex_decode (string as_hex) → stringLe texte de retour ; vide si ce n'est pas de l'hexadécimal, ou si ses octets ne sont pas un texte
of_random_password (integer ai_length, string as_charset) → stringUn mot de passe aléatoire (4 à 256 caractères) sur un jeu CHARSET_* (vide = CHARSET_ALL), sans biais. Vide sur une longueur fautive ou un jeu inconnu
of_equals_constant_time (string as_a, string as_b) → booleanÉgalité en temps constant : comparer un jeton ou un code sans le trahir
of_password_hash (string as_password) → stringCe qu'on STOCKE pour un mot de passe : PBKDF2 salé (il_password_hash_rounds, 600 000 tours), tours et sel dans la valeur. Jamais le même résultat deux fois. Un mot de passe vide se hache (vérifier un mot de passe vide est légitime). Le format est PROPRE à PBToolboxAI (voir Les formats)
of_password_verify (string as_password, string as_stored) → booleanVrai si le mot de passe est celui de la valeur stockée, comparé en temps constant. Une valeur dont les tours sortent de 1 000 à 10 000 000 est refusée : une valeur hostile ne fait pas tourner la vérification des minutes
of_totp_secret ( ) → stringUn secret neuf pour les codes à deux facteurs (RFC 6238), en base32
of_totp_code (string as_secret) → stringLe code à six chiffres de l'instant, celui que montre l'authentificateur
of_totp_verify (string as_secret, string as_code { , ref long al_step }) → booleanVrai si le code est celui de l'instant, du pas précédent ou du suivant. Avec al_step, le PAS de 30 s pour lequel le code a été accepté (0 sinon) : un code ne doit pas servir deux fois (RFC 6238) — gardez le dernier pas accepté pour l'utilisateur et refusez un pas qui n'est pas plus grand. Un secret qui contient autre chose que A-Z, 2-7, des espaces ou des = est refusé (un 0 tapé pour un O)
of_totp_uri (string as_secret, string as_account, string as_issuer) → stringL'URI otpauth:// à mettre dans un QR code pour enrôler l'utilisateur
of_generate_key ( ) → stringUne clé AES-256 neuve, 32 octets en hexadécimal
of_encrypt_with_key (string as_key, string as_text) → stringAES-256-GCM avec une clé EXPLICITE (hex ou base64) : base64 de nonce + chiffré + tag, ce qu'openssl ou Python déchiffrent
of_decrypt_with_key (string as_key, string as_cipher) → stringLe texte de retour avec la même clé ; vide et is_last_error sinon
of_rsa_encrypt (string as_public_pem, string as_text) → stringUn secret court chiffré vers une clé PUBLIQUE : seule la privée le lit. Le paramétrage EXACT, pour l'autre côté : RSA-OAEP, SHA-256, MGF1 en SHA-256, sans label — Java (OAEPWithSHA-256AndMGF1Padding) et openssl pkeyutl prennent MGF1-SHA-1 par défaut, voir Les formats. 190 octets au plus avec une clé de 2048 bits ; au-delà, is_last_error le dit — chiffrez les données avec of_encrypt_with_key et seulement leur clé ainsi. Une clé PRIVÉE à la place de la publique est nommée
of_rsa_decrypt (string as_private_pem, string as_cipher) → stringLe secret de retour, avec la clé privée
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 } }) → stringUn JWT signé : JWT_HS256 (secret partagé), JWT_RS256/JWT_ES256/JWT_EDDSA (clé privée) ; l'algorithme est OBLIGATOIRE. iat ajouté, exp si la durée est > 0 — un exp dans les claims ET une durée est refusé — et un exp/iat/nbf fourni doit être un nombre. L'en-tête nomme l'algorithme comme il est enregistré (EdDSA). as_key_format : le secret HS256 en KEYFORMAT_TEXT, KEYFORMAT_HEX ou KEYFORMAT_BASE64 ; une clé PEM n'est jamais un secret HS256. as_header_json : des membres AJOUTÉS à l'en-tête, par exemple {"kid":"2026-09"} — la clé qu'une API choisit (Apple, tout fournisseur qui fait tourner ses clés) ; alg et typ restent ceux de la bibliothèque. Vide en cas d'échec
of_jwt_verify (string as_algorithm, string as_key, string as_token { , string as_key_format }) → stringLes claims (JSON) d'un jeton dont la signature, l'algorithme et les dates sont bons. L'algorithme est OBLIGATOIRE et comparé EXACTEMENT à celui de l'en-tête : "" est refusé, et une clé PEM n'est jamais un secret HS256 — sans quoi une clé PUBLIQUE servait de secret et un jeton forgé passait. Un exp ou un nbf qui n'est pas un nombre, et un en-tête crit, sont refusés. L'audience (aud) et l'émetteur (iss) restent à VÉRIFIER par vous sur les claims rendues. as_key_format comme pour of_jwt_sign. Vide et is_last_error sinon
of_jwt_claims (string as_token) → stringLes claims SANS vérification, pour lire qui le jeton nomme avant de choisir la clé
of_jwt_header (string as_token) → stringL'en-tête (JSON) d'un jeton SANS vérification : lire son kid pour choisir la clé qui le vérifiera. Ne jamais s'y fier seul
of_protect (string as_text { , boolean ab_machine { , string as_entropy } }) → stringUn secret protégé par Windows pour cet utilisateur (ou cette machine) : DPAPI, natif. Ce qu'on range dans l'INI pour un mot de passe de serveur. Il ne le protège PAS d'un autre programme lancé par le même utilisateur (ni, avec ab_machine, d'un autre compte du poste) : as_entropy, un secret de votre application, réduit ce cercle — la même entropie est exigée pour relire
of_unprotect (string as_protected { , string as_entropy }) → stringLe texte de retour, sur le même compte et avec la MÊME entropie s'il y en a une ; vide et is_last_error ailleurs
of_crc32 (string as_text) → stringLe CRC32 d'un texte, 8 chiffres hexadécimaux — un contrôle d'intégrité, pas une empreinte
of_crc32_file (string as_path) → stringLe CRC32 d'un fichier, lu par la DLL jusqu'au bout : une lecture qui échoue en route est une erreur, jamais le CRC du début
of_encrypt_file (string as_password, string as_source, string as_target) → longUn fichier chiffré par mot de passe, en natif et par morceaux (même conteneur qu'of_encrypt), jusqu'à 64 Go — la limite d'un conteneur AES-GCM. Chemins relatifs : la source se cherche comme pour of_hash_file, la cible s'écrit dans le dossier où l'application a démarré. Rend 0, -5 argument vide ou il_pbkdf2_rounds hors bornes, -2 source illisible, -4 cible inscriptible impossible ou, en démo, source au-delà de 2048 octets, -7 source au-delà de 64 Go
of_decrypt_file (string as_password, string as_source, string as_target) → longLe fichier de retour. Rend 0, -3 mauvais mot de passe ou fichier altéré (rien n'est écrit : le texte est vérifié dans un fichier temporaire caché que Windows efface même si l'application meurt), -5/-2/-4 comme of_encrypt_file
of_close ( )Libère la page cachée ; fait pour vous à la destruction de l'objet
of_reset ( )Retour aux valeurs par défaut

Exemples #

Vérifier qu'un fichier n'a pas changé #

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

Une pièce jointe en base64 pour une 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")

Garder un secret dans un 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)

Signer une commande, la vérifier ailleurs #

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

Un jeton pour une API, et sa vérification #

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

Vérifier la signature d'un webhook #

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

Un mot de passe de serveur rangé par Windows, et un utilisateur à deux facteurs #

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

Bonnes pratiques #

← Référence des composants · Sommaire du guide