(defmodule nemoptera_crypto
(export all))
(defun key-path () "data/secret.key")
(defun iterations () 210000)
(defun hash-password (password)
(let* ((salt (crypto:strong_rand_bytes 16))
(hash (crypto:pbkdf2_hmac 'sha512 (to-bin password) salt (iterations) 64)))
(lists:flatten
(io_lib:format "pbkdf2$~p$~s$~s"
(list (iterations)
(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 (to-bin password) salt iter 64)))
(crypto:hash_equals actual expected)))
(_ 'false)))
(defun parse-stored (stored)
(case (string:split (nemoptera_text:to-str stored) "$" 'all)
(`("pbkdf2" ,iter-s ,salt-b64 ,hash-b64)
(case (catch (tuple (list_to_integer iter-s)
(base64:decode salt-b64)
(base64:decode hash-b64)))
(`#(EXIT ,_) 'error)
(`#(,iter ,salt ,hash) `#(ok ,iter ,salt ,hash))))
(_ 'error)))
(defun load-or-create-key ()
(load-or-create-key (key-path)))
(defun load-or-create-key (path)
(filelib:ensure_dir path)
(case (file:read_file path)
(`#(ok ,key)
(if (=:= 32 (byte_size key))
`#(ok ,key)
`#(error bad-key-size)))
(_
(let ((key (crypto:strong_rand_bytes 32)))
(case (file:write_file path key)
('ok
(file:change_mode path 384)
`#(ok ,key))
(`#(error ,reason) `#(error ,reason)))))))
(defun secret ()
(case (persistent_term:get 'nemoptera_secret 'error)
('error
(case (load-or-create-key)
(`#(ok ,key)
(persistent_term:put 'nemoptera_secret key)
key)
(`#(error ,reason) (erlang:error reason))))
(key key)))
(defun seal (plaintext)
(let* ((key (secret))
(iv (crypto:strong_rand_bytes 12))
((tuple ciphertext tag)
(crypto:crypto_one_time_aead 'aes_256_gcm key iv (to-bin plaintext) #"" 'true))
(packed (iolist_to_binary (list iv tag ciphertext))))
(binary_to_list (base64:encode packed))))
(defun open-sealed (stored)
(case (catch (base64:decode (to-bin stored)))
(packed
(if (and (is_binary packed) (>= (byte_size packed) 28))
(let ((iv (binary:part packed 0 12))
(tag (binary:part packed 12 16))
(ciphertext (binary:part packed 28 (- (byte_size packed) 28))))
(let ((result (catch (crypto:crypto_one_time_aead
'aes_256_gcm (secret) iv ciphertext #"" tag 'false))))
(if (is_binary result)
`#(ok ,(binary_to_list result))
'error)))
'error))
(_ 'error)))
(defun to-bin (value)
(if (is_binary value)
value
(list_to_binary (nemoptera_text:to-str value))))