crypto — n_pbt_crypto #
← Riferimento dei componenti · Sommario della guida
Hash, HMAC, cifratura con password, valori casuali e firme RSA — la Web Crypto API del motore WebView2, che il tuo PowerBuilder 10 non ha da nessun'altra parte. Nessuna DLL di terzi, nessun servizio: ciò che la postazione sa già fare, offerto a PowerScript.
▶ Vederlo dal vivo — Applicazione dimostrativa, riquadro Crypto: i risultati, il codice che li produce e questa pagina, fianco a fianco.
In breve #
| Oggetto non visuale | n_pbt_crypto |
| Serve a | Verificare che un file non sia cambiato, custodire un segreto in un INI, firmare un ordine, autenticare una chiamata API |
| Principio | Una pagina nascosta fa il calcolo; ogni metodo è una normale chiamata PowerScript che restituisce il suo valore |
| Dipendenza | Il runtime WebView2, già richiesto dalla libreria — nient'altro |
Avvio rapido #
// 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
Ogni metodo è una chiamata che restituisce il suo valore: nessun evento, nessuna attesa da scrivere.
I formati, leggibili dagli altri strumenti #
- Hash e HMAC: esadecimale minuscolo, il testo letto in UTF-8 — Python, Java e
openssl dgstdanno lo stesso valore. of_encrypt: una sola stringa base64 che porta, in quest'ordine,PBK2(4 byte), il numero di round PBKDF2 (4 byte, big-endian), il sale (16), il nonce (12), il cifrato AES-256-GCM, poi il suo tag (16 byte, alla fine). La chiave è derivata dalla password (UTF-8) con PBKDF2-HMAC-SHA-256 con QUEI round —il_pbkdf2_roundsal momento della cifratura. Bastaof_decryptcon la stessa password, su qualsiasi postazione;of_encrypt_filescrive esattamente lo stesso contenitore, in byte.of_password_hash:pbkdf2-sha256$round$sale$hash, sale e hash in base64 standard. Questo formato è PROPRIO di PBToolboxAI: né passlib ($pbkdf2-sha256$…) né Django (pbkdf2_sha256$…) lo leggono così com'è.- Chiavi e firme: PEM, pubblica in SPKI (
BEGIN PUBLIC KEY), privata in PKCS#8 (BEGIN PRIVATE KEY); firme in base64, secondo la famiglia della chiave — RSA: RSASSA-PKCS1-v1_5 con SHA-256; ECDSA P-256: SHA-256, in DER (openssl dgst -sha256 -verify, JavaSHA256withECDSA); Ed25519: 64 byte.of_verifylegge una firma ECDSA in DER o in P1363 (r | s); il JWT ES256 resta in P1363, come richiede la RFC 7518. - RSA-OAEP (
of_rsa_encrypt): SHA-256, MGF1 con SHA-256, senza label. In Java:OAEPParameterSpec("SHA-256", "MGF1", MGF1ParameterSpec.SHA256, PSource.PSpecified.DEFAULT); con openssl:-pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -pkeyopt rsa_mgf1_md:sha256. Una chiave PKCS#1 (BEGIN RSA PRIVATE KEY) si converte conopenssl pkcs8 -topk8 -nocrypt. - MD5 è offerto in NATIVO (
HASH_MD5) per gli scambi datati che lo richiedono — mai per una password né per un'integrità di fronte a un avversario. - JWK (
of_jwk_to_pem,of_pem_to_jwk,of_jwt_verify_jwks): il JSON della RFC 7517 —ktyRSA, EC (P-256, P-384) o OKP (Ed25519); un insieme di chiavi (JWKS) è{"keys":[…]}, ogni chiave nominata dal suokid. - Blob: i metodi
of_*_bloblavorano sui BYTE, in nativo — gli stessi valori di un file che contenesse quei byte;of_encrypt_blobscrive il contenitore diof_encrypt.
Decifrare un valore di of_encrypt lato server, in Python (libreria 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")
Proprietà #
| Proprietà | Tipo | Predefinito | Ruolo |
|---|---|---|---|
il_timeout_ms | long | 20000 | Quanto può durare una chiamata alla pagina nascosta: generare una chiave da 4096 bit, o derivare una chiave con milioni di cicli PBKDF2, può richiedere qualche secondo. Hash, file e blob sono nativi: nessun limite di tempo si applica a loro |
il_pbkdf2_rounds | long | 100000 | Round PBKDF2 di of_encrypt e of_encrypt_file: circa 100 ms, pagati a ogni lettura del valore. Da 1.000 a 10.000.000: un altro valore fa RIFIUTARE la chiamata (of_get_last_error() lo dice, -5 per un file), mai sostituire Vale anche per of_encrypt_blob. Un valore porta i suoi cicli: cambiarli non rende mai illeggibile un valore più vecchio |
il_password_hash_rounds | long | 600000 | Round PBKDF2 di of_password_hash: 600.000, la cifra OWASP per PBKDF2-HMAC-SHA-256 (circa 0,6 s). Un hash memorizzato è ciò che un attaccante forza offline se la tabella trapela; si verifica una sola volta per accesso. Stessi limiti Aumentateli, e of_password_needs_rehash dice quali valori memorizzati rinnovare |
il_jwt_leeway_seconds | long | 60 | Tolleranza di of_jwt_verify e of_jwt_verify_jwks su exp e nbf, in secondi: gli orologi di due macchine derivano. Da 0 a 86 400; ogni altro valore fa rifiutare la verifica |
ipo_owner | powerobject | null | L'oggetto visuale per cui lavora questo helper: la licenza si verifica sulla sua classe. Da impostare prima della prima chiamata. Necessario solo nell'applicazione dimostrativa; una chiave di sviluppo o di runtime sblocca l'helper senza di esso. Non sbloccato, è in modalità demo — testi di al più 2048 caratteri e file di al più 2048 byte, in ingresso come in uscita I blob seguono il limite dei file |
Costanti: HASH_MD5, HASH_SHA1, HASH_SHA256, HASH_SHA384, HASH_SHA512 per l'algoritmo; KEY_2048, KEY_3072, KEY_4096 per la dimensione di una chiave RSA, KEY_RSA_2048, KEY_RSA_3072, KEY_RSA_4096, KEY_EC_P256, KEY_EC_P384, KEY_ED25519 per il tipo di coppia; 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 per lo schema di una firma RSA; CHARSET_ALL, CHARSET_ALPHANUM, CHARSET_LETTERS, CHARSET_DIGITS, CHARSET_HEX; KEYFORMAT_TEXT, KEYFORMAT_HEX, KEYFORMAT_BASE64 per la forma di una chiave HMAC.
Metodi #
| Metodo | Ruolo | |
|---|---|---|
of_get_last_error ( ) → string | Perché l'ultima chiamata ha restituito una stringa vuota, false o un codice negativo: password errata, file illeggibile, algoritmo sconosciuto, limite demo Un argomento NULL vale come vuoto: il motivo non è mai NULL. | |
of_open ( ) → long | Crea la pagina nascosta. Facoltativo — ogni metodo lo fa — ma chiamarlo all'apertura della finestra paga il costo una volta. Restituisce l'handle (> 0) o -2 se non è stato possibile crearla (of_get_last_error() dice perché) | |
of_is_open ( ) → boolean | Vero appena la pagina nascosta esiste | |
of_hash (string as_algorithm, string as_text) → string | L'hash di un testo, in esadecimale; HASH_* per l'algoritmo, letto senza maiuscole, trattini o spazi (SHA-256 vale HASH_SHA256). NATIVO per ogni algoritmo: nessuna pagina si apre per un hash. Vuoto in caso di errore | |
of_sha256 (string as_text) → string | of_hash con HASH_SHA256 — quello che si usa di più | |
of_hmac (string as_algorithm, string as_key, string as_text { , string as_key_format }) → string | L'HMAC di un testo con una chiave segreta, in esadecimale — ciò che un'API chiede per autenticare una chiamata. La chiave è un TESTO per impostazione predefinita; KEYFORMAT_HEX o KEYFORMAT_BASE64 ne danno i BYTE: un segreto fornito codificato, o il risultato di un HMAC precedente (AWS SigV4 ne concatena quattro). Una chiave vuota è rifiutata, e anche una chiave PEM: non è mai un segreto HMAC Anche il base64url è letto. | |
of_hmac_verify (string as_algorithm, string as_key, string as_text, string as_expected { , string as_key_format }) → boolean | Vero se as_expected è l'HMAC di as_text con as_key, confrontato in TEMPO COSTANTE — la verifica della firma di un webhook (Stripe, GitHub, Shopify) senza tradirla, mai if of_hmac(…) = intestazione. as_expected in esadecimale (maiuscole indifferenti) o in base64; togli prima un prefisso come sha256=. Falso altrimenti as_expected può anche essere in base64url. | |
of_hash_file (string as_algorithm, string as_path) → string | L'hash di un file della postazione, di qualsiasi tipo e dimensione (letto a pezzi), senza caricarlo in PowerBuilder. Un percorso RELATIVO si cerca nella cartella corrente, poi nella cartella in cui l'applicazione è partita, poi accanto all'EXE — la regola di tutti i file di questo componente. Vuoto in caso di errore; of_get_last_error() distingue l'algoritmo sconosciuto dal file illeggibile | |
of_base64_encode (string as_text) → string | Un testo in base64 (i suoi byte UTF-8): ciò che un browser o Python scriverebbe. Vuoto in caso di errore | |
of_base64_decode (string as_base64) → string | Il TESTO di ritorno (UTF-8). Un valore che non è base64, o i cui byte non sono un testo (un PDF, un'immagine), rende una stringa vuota e of_get_last_error() — mai rumore. I byte si scrivono in un file con of_base64_decode_to_file Oppure of_base64_decode_blob, che li rende in un blob. | |
of_base64_encode_file (string as_path) → string | I byte di un file della postazione in base64 — un allegato, un'immagine per una chiamata JSON — letti dalla DLL, mai caricati in PowerBuilder. Fino a 32 MB in un'applicazione a 32 bit (l'IDE PowerBuilder lo è), 512 MB a 64 bit; oltre, of_base64_encode_file_to_file. Vuoto in caso di errore; of_get_last_error() dice quale: un file troppo grande per questo processo, illeggibile, o il limite demo | |
of_base64_encode_file_to_file (string as_source, string as_target) → long | Il base64 di un file scritto in UN ALTRO file (ASCII, senza a capo), dalla DLL, a memoria costante e qualunque sia la dimensione: ciò che invia un grosso caricamento. Percorsi come per of_hash_file. Rende 0, -5 su un percorso vuoto o una destinazione che dipende dalla cartella corrente di un'unità (\x, C:x), -2 sorgente illeggibile, -4 destinazione non scrivibile o, in demo, sorgente oltre 2048 byte | |
of_base64_decode_to_file (string as_base64, string as_path) → long | Scrive i byte di un valore base64 in un file — l'allegato con cui un'API ha risposto. Un valore VUOTO scrive un file vuoto; un percorso RELATIVO si scrive nella cartella in cui l'applicazione è partita (la cartella corrente al caricamento della libreria, la stessa nell'IDE e compilata), dove of_hash_file lo ritrova. Rende 0 una volta scritto, -5 su un percorso vuoto o che dipende dalla cartella corrente di un'unità (\x, C:x), o un valore che non è base64, -4 se il file non può essere scritto o, in demo, oltre 2048 byte | |
of_encrypt (string as_password, string as_text) → string | Cifra un testo con una password: una sola stringa base64 da memorizzare, che porta i suoi round PBKDF2 (vedi I formati). Una password VUOTA è rifiutata — chiunque rileggerebbe il valore. Vuoto in caso di errore | |
of_decrypt (string as_password, string as_cipher) → string | Il testo di ritorno, con la stessa password. Password errata o valore alterato: stringa vuota e of_get_last_error(), mai rumore. Un valore il cui contenuto non è un testo (un file cifrato con of_encrypt_file) rende vuoto anch'esso: of_decrypt_file o of_decrypt_blob sui suoi byte. | |
of_uuid ( ) → string | Un UUID casuale (versione 4) | |
of_random_hex (integer ai_bytes) → string | ai_bytes byte casuali (da 1 a 4096) in esadecimale: un sale, un token | |
of_generate_keypair (integer ai_bits, ref string as_public_pem, ref string as_private_pem) → long | Una coppia RSA in PEM (KEY_2048, KEY_3072, KEY_4096). Restituisce 0 quando entrambe le chiavi sono riempite, -5 su qualsiasi altra dimensione (entrambe restano vuote), -4 in caso di errore | |
of_sign (string as_private_pem, string as_text { , string as_scheme, string as_hash }) → string | La firma di un testo con la chiave privata, in base64. La chiave dice la sua famiglia: RSA (RSASSA-PKCS1-v1_5, SHA-256), ECDSA P-256 (SHA-256, firma in DER — ciò che openssl dgst e Java SHA256withECDSA verificano) o Ed25519 (64 byte). Vuoto in caso di errore as_scheme e as_hash: ciò che l'altra parte si aspetta — SIGN_PSS per una chiave RSA (sale della lunghezza dell'hash, MGF1 con lo stesso hash), HASH_SHA384 o HASH_SHA512; un testo vuoto tiene quelli della chiave; una chiave EC prende un hash, mai PSS; una chiave Ed25519 né l'uno né l'altro. Una chiave PUBBLICA data al suo posto viene nominata | |
of_verify (string as_public_pem, string as_text, string as_signature { , string as_scheme, string as_hash }) → boolean | Vero se la firma è stata fatta esattamente su questo testo dalla chiave privata corrispondente; falso — non un errore — se il testo è cambiato. Una firma ECDSA si legge in DER (openssl, Java) o in P1363 (`r | s, 64 byte: WebCrypto, .NET) Con lo schema e l'hash del firmatario (vedi of_sign`). |
of_generate_keypair (string as_kind, ref string as_public_pem, ref string as_private_pem) → long | Una coppia per TIPO: KEY_RSA_2048/3072/4096, KEY_EC_P256 o KEY_EC_P384 (firme corte) o KEY_ED25519. Rende 0, -5 su un tipo che non è nessuno di questi (mai una coppia RSA al suo posto), -4 in caso di errore | |
of_key_kind (string as_pem) → string | La famiglia di una chiave PEM: rsa, ec-p256, ec-p384 o ed25519; si legge solo il PRIMO blocco del testo. Una chiave PKCS#1 (BEGIN RSA PRIVATE KEY), un certificato o una chiave cifrata è NOMINATA in of_get_last_error(), con il comando openssl che la converte | |
of_jwk_to_pem (string as_jwk_json) → string | Una chiave pubblicata come JWK (RFC 7517: kty RSA, EC P-256 / P-384 o OKP Ed25519) in PEM, ciò che prendono of_verify, of_jwt_verify e openssl: pubblica in SPKI, privata in PKCS#8 quando il JWK contiene d. I suoi membri d'uso (alg, use, key_ops, kid) non fanno parte della chiave. Vuoto in caso di errore | |
of_pem_to_jwk (string as_pem) → string | Una chiave PEM come JWK (JSON): ciò che pubblica un insieme di chiavi, o ciò che un'API chiede di registrare. Un PEM privato dà un JWK privato (con d): tenetelo per voi. Vuoto in caso di errore | |
of_base64url_encode (string as_text) → string | base64 sicuro per URL (JWT, query string) | |
of_base64url_decode (string as_base64url) → string | Il testo di ritorno; vuoto se non è base64url, o se i suoi byte non sono un testo | |
of_hex_encode (string as_text) → string | I byte UTF-8 di un testo in esadecimale | |
of_hex_decode (string as_hex) → string | Il testo di ritorno; vuoto se non è esadecimale, o se i suoi byte non sono un testo | |
of_random_password (integer ai_length, string as_charset) → string | Una password casuale (4-256 caratteri) su un insieme CHARSET_* (vuoto = CHARSET_ALL), senza bias. Vuota su una lunghezza errata o un insieme sconosciuto | |
of_equals_constant_time (string as_a, string as_b) → boolean | Uguaglianza a tempo costante: confrontare un token o un codice senza tradirlo | |
of_password_hash (string as_password) → string | Ciò che si MEMORIZZA per una password: PBKDF2 con sale (il_password_hash_rounds, 600.000 round), round e sale nel valore. Mai lo stesso risultato due volte. Una password vuota si hash (verificare una password vuota è legittimo). Il formato è PROPRIO di PBToolboxAI (vedi I formati) | |
of_password_verify (string as_password, string as_stored) → boolean | Vero se la password è quella del valore conservato, confrontata a tempo costante. Un valore i cui round escono da 1 000 - 10 000 000 è rifiutato: un valore ostile non fa girare la verifica per minuti | |
of_password_needs_rehash (string as_stored) → boolean | Vero quando un valore memorizzato da of_password_hash porta MENO cicli di il_password_hash_rounds: subito dopo un of_password_verify riuscito, quando la password è nota, ricalcolatene l'hash e memorizzate il nuovo valore — il consiglio OWASP quando il costo sale. Falso altrimenti; falso con of_get_last_error() quando as_stored non è un tale valore. Nessuna pagina | |
of_totp_secret ( ) → string | Un segreto nuovo per i codici a due fattori (RFC 6238), in base32 | |
of_totp_code (string as_secret { , string as_hash, integer ai_digits, integer ai_period }) → string | Il codice a sei cifre del momento, quello che mostra l'autenticatore Con delle impostazioni: HASH_SHA1, HASH_SHA256 o HASH_SHA512, da 6 a 8 cifre, un periodo da 1 a 3 600 secondi — quelle che scrive of_totp_uri | |
of_totp_verify (string as_secret, string as_code { , ref long al_step }) → boolean | Vero se il codice è quello dell'istante, del passo precedente o del successivo. Con al_step, il PASSO di 30 s per cui il codice è stato accettato (0 altrimenti): un codice non deve servire due volte (RFC 6238) — conserva l'ultimo passo accettato per l'utente e rifiuta un passo che non sia maggiore. Un segreto che contiene altro che A-Z, 2-7, spazi o = è rifiutato (uno 0 digitato per una O) Gli spazi digitati dall'utente («864 010», come l'app lo mostra) sono ignorati. | |
of_totp_verify (string as_secret, string as_code, string as_hash, integer ai_digits, integer ai_period, ref long al_step) → boolean | La stessa verifica con le impostazioni di of_totp_code; al_step conta periodi di ai_period secondi | |
of_totp_uri (string as_secret, string as_account, string as_issuer { , string as_hash, integer ai_digits, integer ai_period }) → string | L'URI otpauth:// da mettere in un QR code per iscrivere l'utente Il segreto vi è scritto in base32 maiuscolo, senza spazi né =; un segreto con un altro carattere è rifiutato. Con le impostazioni di of_totp_code, l'app mostra i codici che esso calcola con esse | |
of_hotp_code (string as_secret, long al_counter) → string | Il codice a sei cifre di un CONTATORE (HOTP, RFC 4226, SHA-1): un token hardware, un codice inviato per email — il server tiene il contatore e lo incrementa a ogni codice accettato. al_counter: 0 o più. Vuoto in caso di errore | |
of_generate_key ( ) → string | Una chiave AES-256 nuova, 32 byte in esadecimale | |
of_encrypt_with_key (string as_key, string as_text { , string as_aad }) → string | AES-256-GCM con una chiave ESPLICITA (hex o base64): base64 di nonce + cifrato + tag, ciò che openssl o Python decifrano as_aad: DATI AGGIUNTIVI, autenticati ma non cifrati — un numero d'ordine, un identificativo: il valore si decifra solo con gli stessi. La chiave può anche essere in base64url | |
of_decrypt_with_key (string as_key, string as_cipher { , string as_aad }) → string | Il testo di ritorno con la stessa chiave; vuoto e of_get_last_error() altrimenti Con gli STESSI dati aggiuntivi quando c'erano. | |
of_rsa_encrypt (string as_public_pem, string as_text { , string as_hash }) → string | Un segreto breve cifrato verso una chiave PUBBLICA: solo la privata lo legge. Le impostazioni ESATTE, per l'altro lato: RSA-OAEP, SHA-256, MGF1 con SHA-256, senza label — Java (OAEPWithSHA-256AndMGF1Padding) e openssl pkeyutl usano MGF1-SHA-1 per impostazione predefinita, vedi I formati. 190 byte al massimo con una chiave a 2048 bit; oltre, of_get_last_error() lo dice — cifra i dati con of_encrypt_with_key e solo la loro chiave così. Una chiave PRIVATA al posto della pubblica è nominata as_hash: l'hash di OAEP e di MGF1 — HASH_SHA1 è ciò che usano per impostazione predefinita openssl pkeyutl, .NET (OaepSHA1) e Java (OAEPPadding) | |
of_rsa_decrypt (string as_private_pem, string as_cipher { , string as_hash }) → string | Il segreto di ritorno, con la chiave privata Con l'hash del mittente (HASH_SHA256 per impostazione predefinita). | |
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 | Un JWT firmato: JWT_HS256 (segreto condiviso), JWT_RS256/JWT_ES256/JWT_EDDSA (chiave privata); l'algoritmo è OBBLIGATORIO. iat aggiunto, exp se la durata è > 0 — un exp nei claims E una durata è rifiutato — e un exp/iat/nbf fornito deve essere un numero. L'intestazione nomina l'algoritmo come registrato (EdDSA). as_key_format: il segreto HS256 in KEYFORMAT_TEXT, KEYFORMAT_HEX o KEYFORMAT_BASE64; una chiave PEM non è mai un segreto HS256. as_header_json: membri AGGIUNTI all'intestazione, per esempio {"kid":"2026-09"} — la chiave che un'API sceglie (Apple, ogni fornitore che ruota le sue chiavi); alg e typ restano quelli della libreria. Vuoto in caso di errore Anche JWT_HS384/HS512, JWT_RS384/RS512, JWT_PS256/PS384/PS512 e JWT_ES384 (chiave KEY_EC_P384). Una chiave della famiglia sbagliata, o la sua metà PUBBLICA, viene nominata | |
of_jwt_verify (string as_algorithm, string as_key, string as_token { , string as_key_format { , string as_expected_claims_json } }) → string | Le claim (JSON) di un token la cui firma, il cui algoritmo e le cui date sono giusti (il_jwt_leeway_seconds di tolleranza, un minuto per impostazione predefinita). L'algoritmo è OBBLIGATORIO e confrontato ESATTAMENTE con quello dell'intestazione: "" è rifiutato, e una chiave PEM non è mai un segreto HS — altrimenti una chiave PUBBLICA serviva da segreto e un token falsificato passava. Un exp o un nbf che non è un numero, e un'intestazione crit, sono rifiutati. as_key_format come per of_jwt_sign. as_expected_claims_json: le claim che devono esserci con quel valore — l'emittente (iss), il pubblico (aud, trovato anche in una lista); una claim mancante o diversa è rifiutata e nominata. Vuoto e of_get_last_error() altrimenti; una claim si legge con n_pbt_json (of_load, poi of_get_string) | |
of_jwt_verify_jwks (string as_algorithm, string as_jwks_json, string as_token { , string as_expected_claims_json }) → string | Le claim di un token verificato con un INSIEME DI CHIAVI (JWKS, RFC 7517) — ciò che Azure AD, Google, Auth0 o Keycloak pubblicano al loro jwks_uri (da leggere con il restclient, da tenere qualche ora): il kid del token sceglie la chiave. Un token senza kid, un kid assente dall'insieme o presente due volte, una chiave di un altro algoritmo o di un altro uso (use = enc): rifiutati, of_get_last_error() dice quale. Date e claim attese come per of_jwt_verify | |
of_jwt_claims (string as_token) → string | I claim SENZA verifica, per leggere chi il token nomina prima di scegliere la chiave | |
of_jwt_header (string as_token) → string | L'intestazione (JSON) di un token SENZA verifica: leggerne il kid per scegliere la chiave che lo verificherà. Non fidarsene mai da sola | |
of_protect (string as_text { , boolean ab_machine { , string as_entropy } }) → string | Un segreto protetto da Windows per questo utente (o questa macchina): DPAPI, nativo. Ciò che si mette nell'INI per una password di server. NON lo protegge da un altro programma avviato dallo stesso utente (né, con ab_machine, da un altro account della postazione): as_entropy, un segreto della tua applicazione, restringe questo cerchio — la stessa entropia serve per rileggerlo | |
of_unprotect (string as_protected { , string as_entropy }) → string | Il testo di ritorno, sullo stesso account e con la STESSA entropia se ce n'è una; vuoto e of_get_last_error() altrove | |
of_crc32 (string as_text) → string | Il CRC32 di un testo, 8 cifre esadecimali — un controllo di integrità, non un hash | |
of_crc32_file (string as_path) → string | Il CRC32 di un file, letto dalla DLL fino in fondo: una lettura che fallisce a metà è un errore, mai il CRC dell'inizio | |
of_encrypt_file (string as_password, string as_source, string as_target) → long | Un file cifrato con password, in nativo e a pezzi (stesso contenitore di of_encrypt), fino a 64 GB — il limite di un contenitore AES-GCM. Percorsi relativi: la sorgente si cerca come per of_hash_file, la destinazione si scrive nella cartella in cui l'applicazione è partita. Rende 0, -5 argomento vuoto o il_pbkdf2_rounds fuori limiti, -2 sorgente illeggibile, -4 destinazione non scrivibile o, in demo, sorgente oltre 2048 byte, -7 sorgente oltre 64 GB Una destinazione che dipende dalla cartella corrente di un'unità (\x, C:x) rende -5, e of_get_last_error() lo dice | |
of_decrypt_file (string as_password, string as_source, string as_target) → long | Il file di ritorno. Rende 0, -3 password errata o file alterato (nulla è scritto: il testo è verificato in un file temporaneo nascosto che Windows cancella anche se l'applicazione muore), -5/-2/-4 come of_encrypt_file | |
of_base64_encode_blob (blob ab_data) → string | I byte di un blob in base64 — una foto letta con SelectBlob, un PDF ricevuto — senza file. Nativo. Fino a 32 MB a 32 bit, 512 MB a 64 bit; in demo, 2048 byte. Vuoto in caso di errore | |
of_base64_decode_blob (string as_base64, ref blob ab_data) → long | I BYTE di un valore base64 (o base64url) in un blob — un allegato ricevuto, pronto per UpdateBlob. Nativo. Rende 0 una volta riempito ab_data, -5 quando il valore non è base64, -4 in demo oltre 2048 byte; ab_data è allora vuoto | |
of_hash_blob (string as_algorithm, blob ab_data) → string | L'hash dei byte di un blob, con gli algoritmi di of_hash — lo stesso valore di of_hash_file sugli stessi byte. Nativo. Vuoto in caso di errore | |
of_hmac_blob (string as_algorithm, string as_key, blob ab_data { , string as_key_format }) → string | L'HMAC dei byte di un blob con una chiave segreta, la chiave come per of_hmac: un file ricevuto, verificato con la firma che il servizio ha inviato con esso. Nativo. Vuoto in caso di errore | |
of_crc32_blob (blob ab_data) → string | Il CRC32 dei byte di un blob, 8 cifre esadecimali. Nativo | |
of_encrypt_blob (string as_password, blob ab_data, ref blob ab_cipher) → long | I byte di un blob cifrati con password: il contenitore di of_encrypt e di of_encrypt_file, in byte (52 in più dei dati) — letto in base64, si decifra con of_decrypt, e viceversa. Nativo, il_pbkdf2_rounds. Rende 0 una volta riempito ab_cipher, -5 password vuota o cicli fuori limite, -4 in caso di errore o, in demo, oltre 2048 byte | |
of_decrypt_blob (string as_password, blob ab_cipher, ref blob ab_data) → long | I byte di of_encrypt_blob (o di un valore di of_encrypt letto in byte), con la stessa password. Rende 0 una volta riempito ab_data, -3 password errata o byte alterati (ab_data resta vuoto), -5 password vuota o byte che non sono un tale valore, -4 in demo oltre 2048 byte | |
of_close ( ) → long | Rilascia la pagina nascosta; fatto per te alla distruzione dell'oggetto Rende 0 una volta fatto. | |
of_reset ( ) → long | Torna ai valori predefiniti: il_timeout_ms, il_pbkdf2_rounds, il_password_hash_rounds, il_jwt_leeway_seconds, of_get_last_error(); ipo_owner resta Rende 0 una volta fatto. |
Esempi #
Verificare che un file non sia cambiato #
// 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.")
Un allegato in base64 per un'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
Custodire un segreto in un 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())
Firmare un ordine, verificarlo altrove #
// 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.")
Un token per un'API, e la sua verifica #
// 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
Verificare la firma di un webhook #
// 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
Una password di server custodita da Windows, e un utente a due fattori #
// 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
Verificare un token di Azure AD, Google o 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())
Una foto dal 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
Buone pratiche #
- Un solo oggetto per finestra, aperto con lei (
of_open): la pagina nascosta costa qualche centinaio di millisecondi la prima volta, nulla dopo. - La password non si conserva:
of_encryptrestituisce tutto ciò che serve per decifrare, tranne lei — è il contratto. - La chiave privata non viaggia: firma a casa tua, pubblica la chiave pubblica.
of_verifychiede solo quella. - Leggi
of_get_last_error()quando un metodo restituisce una stringa vuota: il motivo è lì, e spesso dice «password errata» o «file illeggibile» — non un difetto del componente. - DPAPI protegge da un ALTRO ACCOUNT, non da un altro programma: tutto ciò che lo stesso utente avvia rilegge un
of_protect(qualsiasi account della postazione conab_machine). Passa un'entropia propria dell'applicazione per restringere questo cerchio. - Un JWT si verifica con il SUO algoritmo, scritto nel codice (
n_pbt_crypto.JWT_RS256), mai letto dal token né da un INI a cui può mancare: un algoritmo vuoto è rifiutato, ma è questa scelta che chiude la porta alla confusione HS/RS. Indicate poi l'emittente e il pubblico attesi (as_expected_claims_json): la verifica li rifiuta se mancano.