- Store and check a password with bcrypt
- Verify a webhook HMAC signature
- Generate a password and measure its strength
- Signed licence key checked offline
- Field-level encryption of an IBAN with key escrow
- Audit of Unix file permissions
- Subnet of a CIDR block and firewall check
- Collision risk of random identifiers
- Detect credential stuffing on the login page
- Tamper-evident audit log with a hash chain
Store and check a password with bcrypt
At sign-up a password is hashed with bcrypt (cost 10); at login it is verified. The script shows the salted hash, reads its cost factor, verifies a right and a wrong password, and contrasts it with an unsalted SHA-256.
#1. Hash the password at sign-up, cost 10; -> "[hash]" (string bcrypt_hash "Tr0ub4dor&3" 10); #2. Login attempts: right and wrong password; -> "[ok]" (string bcrypt_verify "Tr0ub4dor&3" [hash]); -> "[ko]" (string bcrypt_verify "tr0ub4dor&3" [hash]); #3. Never store a fast hash: compare with SHA-256; concat "{\"bcrypt\": \"" [hash] "\", \"length\": " (string length [hash]) ", \"cost\": " (string regexp_extract [hash] "^\$2a\$([0-9]+)\$" 1) ", \"login_ok\": " [ok] ", \"login_wrong_case\": " [ko] ", \"sha256_unsalted\": \"" (string sha_256 "Tr0ub4dor&3") "\"}";
{"bcrypt": "$2a$10$OY5CaYzycZ7wviYLHzU9R./TZq8zXEBGEiBcNDeWX46w6AqRKcx7y", "length": 60, "cost": 10, "login_ok": 1, "login_wrong_case": 0, "sha256_unsalted": "48486e1514e842346ff405b1e45f44059ae82619f2306f99d0940dcb386e91f7"}- The hash changes at every run because of the random salt: that is expected
- Raise the cost to 12 when the server gets faster, and rehash at the next login
Verify a webhook HMAC signature
A payment provider posts a webhook with a header 'sha256=...' computed as HMAC SHA-256 of 'timestamp.body' with a shared secret. The script recomputes the signature, accepts the genuine body and rejects a body whose amount was changed.
#1. Webhook received: timestamp, raw body and signature header; -> "[ts]" "1793520000"; -> "[body]" "{\"event\":\"payment.succeeded\",\"id\":\"evt_1042\",\"amount\":4990}"; -> "[header]" "sha256=4ca5bb3ebb5a24e5a70d949ca924b5348d600112726a3921bb1e3203e5aed2c6"; -> "[secret]" "whsec_9f8e7d6c"; #2. Expected signature of the genuine and of a tampered body; -> "[sig]" (concat "sha256=" (string hmac_sha256 (concat [ts] "." [body]) [secret])); -> "[bad]" (concat "sha256=" (string hmac_sha256 (concat [ts] "." (string replace [body] "4990" "1")) [secret])); #3. Accept or reject, and fingerprint the payload for the audit log; concat "{\"genuine_valid\": " (string equals_ignore_case [header] [sig]) ", \"tampered_valid\": " (string equals_ignore_case [header] [bad]) ", \"body_sha256\": \"" (string sha_256 [body]) "\"}";
{"genuine_valid": 1, "tampered_valid": 0, "body_sha256": "6c92459694aba28f0c8862409d884c47b3f24a6be75d17acaf18dd39e1e74582"}- Reject old replays: compare the timestamp with
date current_msand a 5-minute tolerance - Sign your own outgoing webhooks with the same
string hmac_sha256call
Generate a password and measure its strength
A service account needs a random password. The script generates 16 characters, counts the character classes it contains (lower, upper, digit, symbol) and computes its entropy in bits, compared with an 8-letter lowercase password.
#1. Generate a 16-character password; -> "[pwd]" (string generate_random_str 16); #2. Character classes present; -> "[cls]" (+ (string regexp [pwd] ".*[a-z].*") (string regexp [pwd] ".*[A-Z].*") (string regexp [pwd] ".*[0-9].*") (string regexp [pwd] ".*[^A-Za-z0-9].*")); #3. Entropy of a random string over an alphabet of 62; -> "[bits]" (math round (math calc "16*log(62)/log(2)") 1); concat "{\"password\": \"" [pwd] "\", \"classes\": " [cls] ", \"entropy_bits\": " [bits] ", \"weak_example_bits\": " (math round (math calc "8*log(26)/log(2)") 1) "}";
{"password": "mU35Yx8ftlXiEpeR", "classes": 3, "entropy_bits": 95.3, "weak_example_bits": 37.6}- The generated password changes at every run
- Reject passwords close to a known leaked one with
string levenshtein_distance
Signed licence key checked offline
A software vendor issues licence keys. The licence JSON is signed with the private key of the vendor; the installed product, which only knows the public key, verifies the key offline. A key edited by the customer (25 seats changed to 250) is rejected.
#1. The vendor key pair, the private key stays on the licence server; json load "rx_kp" (string encode_sign_generate_keypair "2048"); #2. Licence of a customer, signed with the private key; -> "[lic]" (concat "{\"customer\": \"ACME Reunion\", \"plan\": \"enterprise\", \"seats\": 25, \"expires\": \"" (date add "2026-11-01" "YEAR" 1) "\"}"); -> "[sig]" (string encode_sign [lic] (json select "rx_kp" "/privateKey")); -> "[key]" (concat (string encode_b64 [lic]) "." [sig]); #3. The installed product only knows the public key and its fingerprint; -> "[pub]" (json select "rx_kp" "/publicKey"); -> "[fp]" (string left (string sha_512 [pub]) 16); -> "[pinned]" [fp]; -> "[body]" (string decode_b64 (atom get [key] 1 ".")); -> "[ok]" (string decode_sign_verify [body] (atom get [key] 2 ".") [pub]); #4. A customer edits the seats to 250 and keeps the signature; -> "[forged]" (string replace [body] "\"seats\": 25" "\"seats\": 250"); -> "[ko]" (string decode_sign_verify [forged] (atom get [key] 2 ".") [pub]); json load "rx_lic" [body]; -> "[out]" (concat "{\"genuine\": " [ok] ", \"forged\": " [ko] ", \"expires\": \"" (json select "rx_lic" "/expires") "\", \"still_valid\": " (date is_after (json select "rx_lic" "/expires") "2026-11-01") ", \"pubkey_pinned\": " (equal [fp] [pinned]) ", \"licence_key_parts\": " (+ (string count [key] ".") 1) "}"); json unload "rx_kp"; json unload "rx_lic"; [out]
{"genuine": 1, "forged": 0, "expires": "2027-11-01", "still_valid": 1, "pubkey_pinned": 1, "licence_key_parts": 2}- Never ship the private key: the product embeds only the public key and its
string sha_512fingerprint - Add the machine id to the signed JSON to bind the licence to one server
Field-level encryption of an IBAN with key escrow
The payroll table must not store IBANs in clear. The IBAN is encrypted with a random master password (PBE), the master password is escrowed with the RSA key pair of the security officer, and a recovery decrypts both. Screens get a masked IBAN, searches use a hash.
#1. Field-level encryption of an employee IBAN before it goes into the payroll table; -> "[iban]" "FR7630006000011234567890189"; -> "[master]" (string generate_random_str 24); -> "[cipher]" (string encode_pbe [iban] [master]); #2. The master password is escrowed: encrypted with the RSA key pair of the security officer; json load "rx_rsa" (string encode_rsa_generate_keypair "2048"); -> "[escrow]" (string encode_rsa [master] (json select "rx_rsa" "/publicKey") (json select "rx_rsa" "/privateKey")); #3. Recovery: the officer decrypts the master password, then the IBAN; -> "[master2]" (string decode_rsa [escrow] (json select "rx_rsa" "/privateKey")); -> "[iban2]" (string decode_pbe [cipher] [master2]); #4. What the table and the screens can show; json load "rx_c" [cipher]; -> "[out]" (concat "{\"stored_fields\": " (string regexp_replace (json fields "rx_c" "/") "\s" "") ", \"plain_in_cipher\": " (string contains [cipher] "FR76") ", \"escrow_recovered\": " (equal [master2] [master]) ", \"iban_recovered\": " (equal [iban2] [iban]) ", \"iban_ok\": " (type is_iban [iban2]) ", \"masked\": \"" (string left [iban2] 4) (string repeat "*" 19) (string right [iban2] 4) "\", \"lookup_hash\": \"" (string left (string sha_256 [iban]) 12) "\"}"); json unload "rx_rsa"; json unload "rx_c"; [out]
{"stored_fields": ["encrypted","params"], "plain_in_cipher": 0, "escrow_recovered": 1, "iban_recovered": 1, "iban_ok": 1, "masked": "FR76*******************0189", "lookup_hash": "2847f6106ab6"}- Keep the master password in a private parameter, never in the script
- A plain SHA-256 of an IBAN can be brute-forced: prefer
string hmac_sha256with a secret for the lookup column
Audit of Unix file permissions
The hardening script of a web server reads the octal modes of sensitive files. The script converts each mode to an integer, tests the world-writable, setuid and world-readable bits with AND, proposes the fixed mode with AND NOT umask 022 and prints it back in octal.
#1. Modes in octal read by the audit script on a web server, the default umask is 022; json load "rx_fs" "{\"/var/www/app/config.php\": \"0666\", \"/var/www/app/index.php\": \"0644\", \"/usr/local/bin/backup\": \"4755\", \"/var/www/uploads\": \"0777\", \"/etc/app/secret.key\": \"0640\"}"; -> "[umask]" 18; -> "[out]" (concat "new file mode 666 AND NOT 022 = " (string int_to_oct (math bit_and 438 (math bit_xor 511 [umask]))) "; "); json parse_obj "rx_fs" "/" "[path]" "[oct]" { #2. Octal string to integer, digit by digit; -> "[m]" 0; for (-> "[i]" 0) (< [i] 4) (++ "[i]") {-> "[m]" (+ (* [m] 8) (string mid [oct] [i] (+ [i] 1)));}; #3. Bits 2 = world writable, 2048 = setuid, 4 = world readable; -> "[f]" ""; if (> (math bit_and [m] 2) 0) {-> "[f]" (concat [f] " WORLD-WRITABLE");}; if (> (math bit_and [m] 2048) 0) {-> "[f]" (concat [f] " SETUID");}; if (and (string contains [path] "secret") (> (math bit_and [m] 4) 0)) {-> "[f]" (concat [f] " SECRET-READABLE");}; #4. Fix proposal: remove the write bit of group and others with AND NOT 022; if (not (equal [f] "")) {-> "[out]" (concat [out] [path] " " [oct] [f] " -> chmod " (string int_to_oct (math bit_and [m] (math bit_xor 4095 [umask]))) "; ");}; }; json unload "rx_fs"; [out]
new file mode 666 AND NOT 022 = 644; /var/www/uploads 0777 WORLD-WRITABLE -> chmod 755; /var/www/app/config.php 0666 WORLD-WRITABLE -> chmod 644; /usr/local/bin/backup 4755 SETUID -> chmod 4755;
- Setuid binaries must be listed and justified: a setuid script is a classic privilege escalation
- Run it on the output of
find / -perm -o+wcollected by the system team
Subnet of a CIDR block and firewall check
A firewall rule allows the admin network given as 10.20.37.14/21. The script converts the address to an exact 32-bit integer, computes the mask, the network and broadcast addresses with AND, OR and XOR, prints the network in dotted notation, the mask in hex, the usable hosts, and checks the source addresses of the last connections.
#1. Firewall rule: allow the admin network of 10.20.37.14/21, then check the source addresses of the last connections; -> "[cidr]" "10.20.37.14/21"; -> "[ip]" (atom get [cidr] 1 "/"); -> "[pfx]" (atom get [cidr] 2 "/"); #2. Exact 32-bit integers with bigdec: a.b.c.d = a.2^24 + b.2^16 + c.2^8 + d; -> "[n]" (bd+ (bd+ (bd* (atom get [ip] 1 ".") 16777216) (bd* (atom get [ip] 2 ".") 65536)) (bd+ (bd* (atom get [ip] 3 ".") 256) (atom get [ip] 4 "."))); #3. Mask = 2^32 - 2^ 32 - prefix, network = ip AND mask, broadcast = network OR the inverted mask; -> "[mask]" (bd- 4294967296 (bigdec pow 2 (- 32 [pfx]) 20)); -> "[net]" (math bit_and [n] [mask]); -> "[bc]" (math bit_or [net] (math bit_xor [mask] 4294967295)); #4. Network in dotted notation, then test of each source address; -> "[d]" ""; for (-> "[k]" 3) (>= [k] 0) (-- "[k]") {-> "[d]" (concat [d] (math bit_and (bigdec round (bigdec div [net] (bigdec pow 256 [k] 20) 30) 0 DOWN) 255) (if (> [k] 0) {".";} {"";}));}; json load "rx_src" "[\"10.20.39.200\", \"10.20.40.1\", \"10.20.32.7\", \"192.168.1.20\"]"; -> "[chk]" ""; json parse_array "rx_src" "/" "[s]" { -> "[x]" (bd+ (bd+ (bd* (atom get [s] 1 ".") 16777216) (bd* (atom get [s] 2 ".") 65536)) (bd+ (bd* (atom get [s] 3 ".") 256) (atom get [s] 4 "."))); -> "[chk]" (concat [chk] [s] (if (== (math bit_and [x] [mask]) [net]) {" allow";} {" deny";}) ", "); }; json unload "rx_src"; concat "network " [d] "/" [pfx] ", mask hex " (string int_to_hex [mask]) ", usable hosts " (- (- [bc] [net]) 1) " | " [chk]
network 10.20.32.0/21, mask hex fffff800, usable hosts 2046 | 10.20.39.200 allow, 10.20.40.1 deny, 10.20.32.7 allow, 192.168.1.20 deny,
- The /21 given with a host address (37.14) is normalised to its network 10.20.32.0
- Check the address format first with
type is_ipv4
Collision risk of random identifiers
Before choosing the format of random identifiers, the architect computes the birthday bound: the probability of at least one collision after one million ids, and the number of ids that gives a 1 pct risk, for 7-character base62 short links, 64-bit ids and 128-bit tokens. expm1 and log1p keep the digits of tiny probabilities.
#1. How many identifiers can be issued before a collision becomes likely: short links of 7 base62 chars, 64-bit ids, 128-bit tokens; json load "rx_sp" "{\"short link 62^7\": 3.52161103e12, \"64-bit id\": 1.8446744073709552e19, \"128-bit token\": 3.402823669209385e38}"; -> "[issued]" 1000000; -> "[out]" ""; json parse_obj "rx_sp" "/" "[name]" "[N]" { #2. Birthday bound p = 1 - e^ -n.n-1 / 2N, computed as - expm1 so that tiny values keep their digits; -> "[p]" (* -1 (math expm1 (/ (* -1 (* [issued] (- [issued] 1))) (* 2 [N])))); #3. Number of ids for a 1 pct collision risk: n = sqrt of 2N ln of 1 / 1 - 0.01; -> "[n1]" (math sqrt (* (* 2 [N]) (* -1 (math log1p -0.01)))); -> "[out]" (concat [out] [name] ": P collision after 1M ids " [p] ", 1 pct risk after " (math round [n1] 0) " ids; "); }; json unload "rx_sp"; [out]
64-bit id: P collision after 1M ids 2.7105026839742053E-8, 1 pct risk after 6.08926881E8 ids; short link 62^7: P collision after 1M ids 0.13236167817948616, 1 pct risk after 266058 ids; 128-bit token: P collision after 1M ids 1.4693664691599209E-27, 1 pct risk after 2.615321040553087E18 ids;
- 7 base62 chars collide with 13 pct probability after 1M links: check the database or use more characters
- With 1 - exp(...) in plain doubles the 128-bit result would be 0, expm1 gives 1.47E-27
Detect credential stuffing on the login page
The SOC monitors the failed logins per minute. The baseline of the month is 4 per minute with a standard deviation of 1.5. The script runs a z-test on the last 12 minutes, counts the minutes above 3 sigma, finds the start of the attack and raises an alert.
#1. Failed logins per minute on the portal over the last 12 minutes, the baseline of the month is 4 per minute, sd 1.5; -> "[fails]" "[5, 3, 6, 4, 9, 14, 17, 15, 19, 16, 18, 21]"; -> "[mu]" 4; -> "[sigma]" 1.5; #2. z-test of the window against the baseline; json load "rx_z" (stat ztest [fails] [mu] [sigma]); #3. Minutes above 3 sigma, the first one is the start of the attack; json load "rx_f" [fails]; -> "[hot]" 0; -> "[start]" -1; for (-> "[i]" 0) (< [i] (json count "rx_f" "/")) (++ "[i]") { if (> (/ (- (json select "rx_f" (concat "/[" [i] "]")) [mu]) [sigma]) 3) {-> "[hot]" (+ [hot] 1); if (== [start] -1) {-> "[start]" [i];};}; }; -> "[out]" (concat "mean " (math round (stat mean [fails]) 2) " per min, z " (math round (json select "rx_z" "/z") 2) ", p " (if (< (json select "rx_z" "/p_value") 0.000001) {"< 1e-6";} {json select "rx_z" "/p_value";}) ", minutes over 3 sigma " [hot] ", attack from minute " (+ [start] 1) " of 12" " | " (if (< (json select "rx_z" "/p_value") 0.001) {"ALERT: credential stuffing, enable the CAPTCHA and rate limit by IP";} {"normal";})); json unload "rx_z"; json unload "rx_f"; [out]
mean 12.25 per min, z 19.05, p < 1e-6, minutes over 3 sigma 8, attack from minute 5 of 12 | ALERT: credential stuffing, enable the CAPTCHA and rate limit by IP
- Run it every minute with a job and write the alert with
log write - Group the failures by source IP and by user: stuffing means many users from few IPs
Tamper-evident audit log with a hash chain
Each line of an audit log is chained to the previous one: hash = SHA-256(previous hash + line). An intruder rewrites a line afterwards; the verification recomputes the chain from the genesis value and stops at the first broken link, even though the edited line looks plausible.
#1. Audit log where each line stores the SHA-256 of the previous hash + its own text, the chain starts from a genesis value; json load "rx_log" "[\"2026-10-31 08:02 login admin ok\", \"2026-10-31 08:05 role change user42 to auditor\", \"2026-10-31 08:09 export payroll.csv\", \"2026-10-31 08:15 logout admin\"]"; -> "[h]" (string sha_256 "genesis"); json load "rx_h" "[]"; json parse_array "rx_log" "/" "[l]" {-> "[h]" (string sha_256 (concat [h] [l])); json iarray "rx_h" "/" [h] STR;}; #2. An intruder edits line 2 afterwards to hide the role change; json uarray "rx_log" "/" 1 "2026-10-31 08:05 password reset user42" STR; #3. Verification recomputes the chain and stops at the first broken link; -> "[h]" (string sha_256 "genesis"); -> "[bad]" 0; for (-> "[i]" 0) (< [i] (json count "rx_log" "/")) (++ "[i]") { -> "[h]" (string sha_256 (concat [h] (json select "rx_log" (concat "/[" [i] "]")))); if (and (== [bad] 0) (not (equal [h] (json select "rx_h" (concat "/[" [i] "]"))))) {-> "[bad]" (+ [i] 1);}; }; -> "[out]" (concat "anchor " (string left (json select "rx_h" "/[3]") 16) "... | first broken line " [bad] ": " (json select "rx_log" (concat "/[" (- [bad] 1) "]"))); json unload "rx_log"; json unload "rx_h"; [out]
anchor c4de521b2ac6a524... | first broken line 2: 2026-10-31 08:05 password reset user42
- Publish the last hash (the anchor) every day outside the server: an attacker cannot rebuild the whole chain unseen
- Sign the anchor with
string encode_signto prove who closed the day
