PBToolboxAI v4 ← Site

crypto — n_pbt_crypto #

← Referencia de componentes · Índice de la guía

Hashes, HMAC, cifrado por contraseña, valores aleatorios y firmas RSA — la Web Crypto API del motor WebView2, que su PowerBuilder 10 no tiene en ningún otro sitio. Sin DLL de terceros, sin servicio: lo que el equipo ya sabe hacer, ofrecido a PowerScript.

▶ Verlo en vivo — Aplicación de demostración, mosaico Crypto: los resultados, el código que los produce y esta página, lado a lado.


En resumen #

Objeto no visualn_pbt_crypto
Sirve paraComprobar que un archivo no ha cambiado, guardar un secreto en un INI, firmar un pedido, autenticar una llamada a una API
PrincipioUna página oculta hace el cálculo; cada método es una llamada PowerScript normal que devuelve su valor
DependenciaEl runtime WebView2, que la biblioteca ya requiere — nada más

Inicio rápido #

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

Cada método es una llamada que devuelve su valor: sin evento, sin espera que escribir.


Los formatos, legibles por otras herramientas #

Descifrar un valor de of_encrypt en el servidor, en Python (biblioteca 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")

Propiedades #

PropiedadTipoPredeterminadoFunción
is_last_errorstring""Por qué la última llamada devolvió una cadena vacía, false o un código negativo: contraseña errónea, archivo ilegible, algoritmo desconocido, límite demo
il_timeout_mslong20000Duración máxima de una llamada. Calcular el hash de un archivo grande o generar una clave de 4096 bits puede tardar unos segundos
il_pbkdf2_roundslong100000Rondas PBKDF2 de of_encrypt y of_encrypt_file: unos 100 ms, pagados en cada lectura del valor. De 1.000 a 10.000.000: cualquier otro valor hace RECHAZAR la llamada (is_last_error lo dice, -5 para un archivo), nunca sustituir
il_password_hash_roundslong600000Rondas PBKDF2 de of_password_hash: 600.000, la cifra de OWASP para PBKDF2-HMAC-SHA-256 (unos 0,6 s). Un hash almacenado es lo que un atacante fuerza sin conexión si la tabla se filtra; se verifica una sola vez por inicio de sesión. Mismos límites
ipo_ownerpowerobjectnullEl objeto visual para el que trabaja esta utilidad: la licencia se comprueba en su clase. Se asigna antes de la primera llamada. Necesario solo en la aplicación de demostración; una clave de desarrollo o de ejecución desbloquea la utilidad sin él. Sin desbloquear, está en modo demo — textos de 2048 caracteres y archivos de 2048 bytes como máximo, tanto a la entrada como a la salida

Constantes: HASH_MD5, HASH_SHA1, HASH_SHA256, HASH_SHA384, HASH_SHA512 para el algoritmo; KEY_2048, KEY_3072, KEY_4096 para el tamaño de una clave RSA, KEY_RSA_2048, KEY_EC_P256, KEY_ED25519 para el tipo de par; JWT_HS256, JWT_RS256, JWT_ES256, JWT_EDDSA; CHARSET_ALL, CHARSET_ALPHANUM, CHARSET_LETTERS, CHARSET_DIGITS, CHARSET_HEX; KEYFORMAT_TEXT, KEYFORMAT_HEX, KEYFORMAT_BASE64 para la forma de una clave HMAC.


Métodos #

MétodoFunción
of_open ( ) → longCrea la página oculta. Opcional — cada método lo hace — pero llamarlo al abrir la ventana paga el coste una vez. Devuelve el handle (> 0) o -2 si no se pudo crear (is_last_error dice por qué)
of_is_open ( ) → booleanVerdadero en cuanto existe la página oculta
of_hash (string as_algorithm, string as_text) → stringEl hash de un texto, en hexadecimal; HASH_* para el algoritmo, leído sin mayúsculas, guiones ni espacios (SHA-256 vale HASH_SHA256). NATIVO para todos los algoritmos: no se abre ninguna página para un hash. Vacío en caso de error
of_sha256 (string as_text) → stringof_hash con HASH_SHA256 — el que más se usa
of_hmac (string as_algorithm, string as_key, string as_text { , string as_key_format }) → stringEl HMAC de un texto bajo una clave secreta, en hexadecimal — lo que una API pide para autenticar una llamada. La clave es un TEXTO por defecto; KEYFORMAT_HEX o KEYFORMAT_BASE64 dan sus BYTES: un secreto entregado codificado, o el resultado de un HMAC anterior (AWS SigV4 encadena cuatro). Una clave vacía se rechaza, y también una clave PEM: nunca es un secreto HMAC
of_hmac_verify (string as_algorithm, string as_key, string as_text, string as_expected { , string as_key_format }) → booleanVerdadero si as_expected es el HMAC de as_text bajo as_key, comparado en TIEMPO CONSTANTE — la verificación de la firma de un webhook (Stripe, GitHub, Shopify) sin delatarla, nunca if of_hmac(…) = cabecera. as_expected en hexadecimal (sin importar mayúsculas) o en base64; quite antes un prefijo como sha256=. Falso en otro caso
of_hash_file (string as_algorithm, string as_path) → stringEl hash de un archivo del equipo, de cualquier tipo y tamaño (leído por trozos), sin cargarlo en PowerBuilder. Una ruta RELATIVA se busca en la carpeta actual, luego en la carpeta en la que arrancó la aplicación, luego junto al EXE — la regla de todos los archivos de este componente. Vacío en caso de error; is_last_error distingue el algoritmo desconocido del archivo ilegible
of_base64_encode (string as_text) → stringUn texto en base64 (sus bytes UTF-8): lo que un navegador o Python escribiría. Vacío en caso de fallo
of_base64_decode (string as_base64) → stringEl TEXTO de vuelta (UTF-8). Un valor que no es base64, o cuyos bytes no son un texto (un PDF, una imagen), devuelve una cadena vacía e is_last_error — nunca ruido. Los bytes se escriben en un archivo con of_base64_decode_to_file
of_base64_encode_file (string as_path) → stringLos bytes de un archivo del equipo en base64 — un adjunto, una imagen para una llamada JSON — leídos por la DLL, nunca cargados en PowerBuilder. Hasta 64 MB en una aplicación de 32 bits (el IDE de PowerBuilder lo es), 512 MB en 64 bits. Vacío si falla; is_last_error dice cuál: archivo demasiado grande para este proceso, ilegible o límite de demo
of_base64_decode_to_file (string as_base64, string as_path) → longEscribe los bytes de un valor base64 en un archivo — el adjunto con el que respondió una API. Un valor VACÍO escribe un archivo vacío; una ruta RELATIVA se escribe en la carpeta en la que arrancó la aplicación (la carpeta actual al cargar la biblioteca, la misma en el IDE y compilada), donde of_hash_file la vuelve a encontrar. Devuelve 0 una vez escrito, -5 con una ruta vacía o que depende de la carpeta actual de una unidad (\x, C:x), o un valor que no es base64, -4 si el archivo no se puede escribir o, en demo, más allá de 2048 bytes
of_encrypt (string as_password, string as_text) → stringCifra un texto con una contraseña: una sola cadena base64 que guardar, que lleva sus rondas PBKDF2 (ver Los formatos). Una contraseña VACÍA se rechaza — cualquiera releería el valor. Vacío en caso de error
of_decrypt (string as_password, string as_cipher) → stringEl texto de vuelta, con la misma contraseña. Contraseña errónea o valor alterado: cadena vacía e is_last_error, nunca ruido. Un valor cuyo contenido no es un texto (un archivo cifrado con of_encrypt_file) devuelve vacío también: of_decrypt_file
of_uuid ( ) → stringUn UUID aleatorio (versión 4)
of_random_hex (integer ai_bytes) → stringai_bytes bytes aleatorios (1 a 4096) en hexadecimal: una sal, un token
of_generate_keypair (integer ai_bits, ref string as_public_pem, ref string as_private_pem) → longUn par RSA en PEM (KEY_2048, KEY_3072, KEY_4096). Devuelve 0 cuando ambas claves están rellenas, -5 con cualquier otro tamaño (ambas quedan vacías), -4 si falla
of_sign (string as_private_pem, string as_text) → stringLa firma de un texto con la clave privada, en base64. La clave dice su familia: RSA (RSASSA-PKCS1-v1_5, SHA-256), ECDSA P-256 (SHA-256, firma en DER — lo que openssl dgst y Java SHA256withECDSA verifican) o Ed25519 (64 bytes). Vacío en caso de error
of_verify (string as_public_pem, string as_text, string as_signature) → booleanVerdadero si la firma se hizo sobre exactamente este texto con la clave privada correspondiente; falso — no un error — si el texto cambió. Una firma ECDSA se lee en DER (openssl, Java) o en P1363 (`rs`, 64 bytes: WebCrypto, .NET)
of_generate_keypair (string as_kind, ref string as_public_pem, ref string as_private_pem) → longUn par por TIPO: KEY_RSA_2048/3072/4096, KEY_EC_P256 (firmas cortas) o KEY_ED25519. Devuelve 0, -5 con un tipo que no es ninguno de estos (nunca un par RSA en su lugar), -4 en caso de fallo
of_key_kind (string as_pem) → stringLa familia de una clave PEM: rsa, ec-p256 o ed25519; solo se lee el PRIMER bloque del texto. Una clave PKCS#1 (BEGIN RSA PRIVATE KEY), un certificado o una clave cifrada se NOMBRA en is_last_error, con el comando openssl que la convierte
of_base64url_encode (string as_text) → stringbase64 seguro para URL (JWT, cadena de consulta)
of_base64url_decode (string as_base64url) → stringEl texto de vuelta; vacío si no es base64url, o si sus bytes no son un texto
of_hex_encode (string as_text) → stringLos bytes UTF-8 de un texto en hexadecimal
of_hex_decode (string as_hex) → stringEl texto de vuelta; vacío si no es hexadecimal, o si sus bytes no son un texto
of_random_password (integer ai_length, string as_charset) → stringUna contraseña aleatoria (4 a 256 caracteres) sobre un juego CHARSET_* (vacío = CHARSET_ALL), sin sesgo. Vacía con una longitud incorrecta o un juego desconocido
of_equals_constant_time (string as_a, string as_b) → booleanIgualdad en tiempo constante: comparar un token o un código sin delatarlo
of_password_hash (string as_password) → stringLo que se GUARDA para una contraseña: PBKDF2 con sal (il_password_hash_rounds, 600.000 rondas), rondas y sal en el valor. Nunca el mismo resultado dos veces. Una contraseña vacía se hashea (verificar una contraseña vacía es legítimo). El formato es PROPIO de PBToolboxAI (ver Los formatos)
of_password_verify (string as_password, string as_stored) → booleanVerdadero si la contraseña es la del valor guardado, comparada en tiempo constante. Un valor cuyas rondas salen de 1 000 a 10 000 000 se rechaza: un valor hostil no hace girar la verificación durante minutos
of_totp_secret ( ) → stringUn secreto nuevo para los códigos de dos factores (RFC 6238), en base32
of_totp_code (string as_secret) → stringEl código de seis dígitos del momento, el que muestra el autenticador
of_totp_verify (string as_secret, string as_code { , ref long al_step }) → booleanVerdadero si el código es el del instante, del paso anterior o del siguiente. Con al_step, el PASO de 30 s para el que se aceptó el código (0 en otro caso): un código no debe servir dos veces (RFC 6238) — guarde el último paso aceptado para el usuario y rechace un paso que no sea mayor. Un secreto que contiene algo distinto de A-Z, 2-7, espacios o = se rechaza (un 0 tecleado por una O)
of_totp_uri (string as_secret, string as_account, string as_issuer) → stringLa URI otpauth:// para poner en un código QR e inscribir al usuario
of_generate_key ( ) → stringUna clave AES-256 nueva, 32 bytes en hexadecimal
of_encrypt_with_key (string as_key, string as_text) → stringAES-256-GCM con una clave EXPLÍCITA (hex o base64): base64 de nonce + cifrado + tag, lo que openssl o Python descifran
of_decrypt_with_key (string as_key, string as_cipher) → stringEl texto de vuelta con la misma clave; vacío e is_last_error si no
of_rsa_encrypt (string as_public_pem, string as_text) → stringUn secreto corto cifrado hacia una clave PÚBLICA: solo la privada lo lee. La configuración EXACTA, para el otro lado: RSA-OAEP, SHA-256, MGF1 con SHA-256, sin etiqueta — Java (OAEPWithSHA-256AndMGF1Padding) y openssl pkeyutl toman MGF1-SHA-1 por defecto, ver Los formatos. 190 bytes como máximo con una clave de 2048 bits; más allá, is_last_error lo dice — cifre los datos con of_encrypt_with_key y solo su clave así. Una clave PRIVADA en lugar de la pública se nombra
of_rsa_decrypt (string as_private_pem, string as_cipher) → stringEl secreto de vuelta, con la clave privada
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 firmado: JWT_HS256 (secreto compartido), JWT_RS256/JWT_ES256/JWT_EDDSA (clave privada); el algoritmo es OBLIGATORIO. iat añadido, exp si la duración es > 0 — un exp en los claims Y una duración se rechaza — y un exp/iat/nbf dado debe ser un número. La cabecera nombra el algoritmo como está registrado (EdDSA). as_key_format: el secreto HS256 en KEYFORMAT_TEXT, KEYFORMAT_HEX o KEYFORMAT_BASE64; una clave PEM nunca es un secreto HS256. as_header_json: miembros AÑADIDOS a la cabecera, por ejemplo {"kid":"2026-09"} — la clave que elige una API (Apple, cualquier proveedor que rota sus claves); alg y typ siguen siendo los de la biblioteca. Vacío en caso de error
of_jwt_verify (string as_algorithm, string as_key, string as_token { , string as_key_format }) → stringLos claims (JSON) de un token cuya firma, algoritmo y fechas son correctos. El algoritmo es OBLIGATORIO y se compara EXACTAMENTE con el de la cabecera: "" se rechaza, y una clave PEM nunca es un secreto HS256 — si no, una clave PÚBLICA servía de secreto y un token falsificado pasaba. Un exp o un nbf que no es un número, y una cabecera crit, se rechazan. La audiencia (aud) y el emisor (iss) quedan por VERIFICAR por usted en los claims devueltos. as_key_format como para of_jwt_sign. Vacío e is_last_error en otro caso
of_jwt_claims (string as_token) → stringLos claims SIN verificación, para leer a quién nombra el token antes de elegir la clave
of_jwt_header (string as_token) → stringLa cabecera (JSON) de un token SIN verificación: leer su kid para elegir la clave que lo verificará. No fiarse nunca solo de ella
of_protect (string as_text { , boolean ab_machine { , string as_entropy } }) → stringUn secreto protegido por Windows para este usuario (o esta máquina): DPAPI, nativo. Lo que va en el INI para una contraseña de servidor. NO lo protege de otro programa lanzado por el mismo usuario (ni, con ab_machine, de otra cuenta del equipo): as_entropy, un secreto de su aplicación, reduce ese círculo — se exige la misma entropía para releerlo
of_unprotect (string as_protected { , string as_entropy }) → stringEl texto de vuelta, en la misma cuenta y con la MISMA entropía si se dio una; vacío e is_last_error en otra
of_crc32 (string as_text) → stringEl CRC32 de un texto, 8 dígitos hexadecimales — un control de integridad, no un hash
of_crc32_file (string as_path) → stringEl CRC32 de un archivo, leído por la DLL hasta el final: una lectura que falla a mitad es un error, nunca el CRC del principio
of_encrypt_file (string as_password, string as_source, string as_target) → longUn archivo cifrado por contraseña, en nativo y por trozos (el mismo contenedor que of_encrypt), hasta 64 GB — el límite de un contenedor AES-GCM. Rutas relativas: el origen se busca como para of_hash_file, el destino se escribe en la carpeta en la que arrancó la aplicación. Devuelve 0, -5 argumento vacío o il_pbkdf2_rounds fuera de límites, -2 origen ilegible, -4 destino no escribible o, en demo, origen de más de 2048 bytes, -7 origen de más de 64 GB
of_decrypt_file (string as_password, string as_source, string as_target) → longEl archivo de vuelta. Devuelve 0, -3 contraseña incorrecta o archivo alterado (no se escribe nada: el texto se comprueba en un archivo temporal oculto que Windows borra aunque la aplicación muera), -5/-2/-4 como of_encrypt_file
of_close ( )Libera la página oculta; se hace por usted al destruir el objeto
of_reset ( )Vuelta a los valores por defecto

Ejemplos #

Comprobar que un archivo no ha cambiado #

// 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 adjunto en base64 para una 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")

Guardar un secreto en 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)

Firmar un pedido, verificarlo en otro sitio #

// 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 token para una API, y su verificación #

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

Verificar la firma de 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

Una contraseña de servidor guardada por Windows, y un usuario con dos factores #

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

Buenas prácticas #

← Referencia de componentes · Índice de la guía