;; 密码哈希与凭据加密(设计文档 §2、§8)
;;
;; 密码:PBKDF2-HMAC-SHA512(OTP 内置 crypto),格式 "pbkdf2$<iter>$<salt-b64>$<hash-b64>"
;; 令牌:AES-256-GCM,密文格式 {IV(12字节), CipherText, Tag(16字节)},
;; 密钥来自 data/secret.key(32 字节随机,权限 0600)。
;;
;; 说明:设计文档原定 argon2 NIF;因 hex 上无可用的 Erlang argon2 依赖,
;; 改用 OTP 标准库 PBKDF2-HMAC-SHA512(210000 轮),单管理员场景安全性足够。
(defmodule lamina_crypto
(export all))
(defun pbkdf2-iterations () 210000)
;;---- 密码哈希 ----
(defun hash_password (password)
(let* ((salt (crypto:strong_rand_bytes 16))
(iter (pbkdf2-iterations))
(hash (crypto:pbkdf2_hmac 'sha512 password salt iter 64)))
(lists:flatten
(io_lib:format "pbkdf2$~p$~s$~s"
(list iter
(base64:encode salt)
(base64:encode hash))))))
(defun verify_password (password stored)
(case (parse_stored stored)
(`#(ok ,iter ,salt ,expected)
(let ((actual (crypto:pbkdf2_hmac 'sha512 password salt iter 64)))
(crypto:hash_equals actual expected)))
('error 'false)))
(defun parse_stored (stored)
(let ((s (if (is_binary stored) (binary_to_list stored) stored)))
(case (string:split s "$" 'all)
(`("pbkdf2" ,iter-s ,salt-b64 ,hash-b64)
`#(ok ,(to-int iter-s)
,(base64:decode salt-b64)
,(base64:decode hash-b64)))
(_ 'error))))
;;---- secret key ----
(defun load_or_create_key (key-path)
(case (file:read_file key-path)
(`#(ok ,key)
(if (=:= 32 (byte_size key))
`#(ok ,key)
`#(error ,(tuple 'bad-key-size key-path))))
(`#(error ,_)
(let ((key (crypto:strong_rand_bytes 32)))
(case (file:write_file key-path key)
('ok
(file:change_mode key-path 384)
`#(ok ,key))
(`#(error ,reason)
`#(error ,(tuple 'write-failed key-path reason))))))))
;;---- AES-256-GCM 令牌加密 ----
(defun encrypt (plaintext key)
(let ((iv (crypto:strong_rand_bytes 12)))
(let ((`#(,ciphertext ,tag) (crypto:crypto_one_time_aead 'aes_256_gcm key iv plaintext "" 'true)))
(tuple iv ciphertext tag))))
(defun decrypt (iv ciphertext tag key)
(let ((result (catch (crypto:crypto_one_time_aead 'aes_256_gcm key iv ciphertext "" tag 'false))))
(if (is_binary result)
`#(ok ,result)
'error)))
(defun to-int (v)
(if (is_binary v) (binary_to_integer v) (list_to_integer v)))