Fosi Fossil Service
Small teams. Open source. Built on Fossil.

CaaS / Lamina

Unified Management for Fossil Repositories

Public | Owner: CaaS | Default branch: trunk | Last update: 2026-10-25 00:41

Artifact ca570366edb485ede27b6a37c23ddd2a9f66d99f7b102d14b5341a92eb383e90:


;; 密码哈希与凭据加密(设计文档 §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)))

Clone

You can clone this repository with the following command:

fossil clone https://fosi.dev/CaaS/Lamina

Contribute

The best way to contribute is through e-mail. See the project documentation for more details.