pbkdf

Pure Haskell password-based KDF (docs.ppad.tech/pbkdf).
git clone git://git.ppad.tech/pbkdf.git
Log | Files | Refs | README | LICENSE

commit 784b28f48c5da77d9f763434ab652c5a3de64791
Author: Jared Tobin <jared@jtobin.io>
Date:   Mon, 24 Feb 2025 09:56:08 +0400

lib: init

Diffstat:
A.gitignore | 1+
ACHANGELOG | 0
ALICENSE | 20++++++++++++++++++++
Abench/Main.hs | 4++++
Aflake.lock | 184+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aflake.nix | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alib/Crypto/KDF/PBKDF.hs | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Appad-pbkdf.cabal | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atest/Main.hs | 4++++
Atest/Wycheproof.hs | 2++
10 files changed, 460 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1 @@ +dist-newstyle/ diff --git a/CHANGELOG b/CHANGELOG diff --git a/LICENSE b/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2025 Jared Tobin + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/bench/Main.hs b/bench/Main.hs @@ -0,0 +1,4 @@ +module Main where + +main :: IO () +main = pure () diff --git a/flake.lock b/flake.lock @@ -0,0 +1,184 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1710146030, + "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1725910328, + "narHash": "sha256-n9pCtzGZ0httmTwMuEbi5E78UQ4ZbQMr1pzi5N0LAG8=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "5775c2583f1801df7b790bf7f7d710a19bac66f4", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "ppad-base16": { + "inputs": { + "flake-utils": [ + "ppad-base16", + "ppad-nixpkgs", + "flake-utils" + ], + "nixpkgs": [ + "ppad-base16", + "ppad-nixpkgs", + "nixpkgs" + ], + "ppad-nixpkgs": [ + "ppad-nixpkgs" + ] + }, + "locked": { + "lastModified": 1739979569, + "narHash": "sha256-omEcmgzRlzIE5Vdty0/SskEcR2f7OtcHzGFE4i1dI60=", + "ref": "master", + "rev": "4439e0efafbb5185bd7d9bfb352a17c2a31b96b4", + "revCount": 15, + "type": "git", + "url": "git://git.ppad.tech/base16.git" + }, + "original": { + "ref": "master", + "type": "git", + "url": "git://git.ppad.tech/base16.git" + } + }, + "ppad-nixpkgs": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + }, + "locked": { + "lastModified": 1737297101, + "narHash": "sha256-EnXnq+JLflbWt+DvaGGnY2gfAqsGNOm5vPgHh3hkfwQ=", + "ref": "master", + "rev": "f29823875250bc99b3891f7373535ccde9a29a44", + "revCount": 1, + "type": "git", + "url": "git://git.ppad.tech/nixpkgs.git" + }, + "original": { + "ref": "master", + "type": "git", + "url": "git://git.ppad.tech/nixpkgs.git" + } + }, + "ppad-sha256": { + "inputs": { + "flake-utils": [ + "ppad-sha256", + "ppad-nixpkgs", + "flake-utils" + ], + "nixpkgs": [ + "ppad-sha256", + "ppad-nixpkgs", + "nixpkgs" + ], + "ppad-nixpkgs": [ + "ppad-nixpkgs" + ] + }, + "locked": { + "lastModified": 1737298572, + "narHash": "sha256-iAo6GFH1FLNi0wt0FczbqPCmVzCm9gfMEjk1oakExt0=", + "ref": "master", + "rev": "abc984dc65f0df9bd958c0bc8f390c68e660f710", + "revCount": 87, + "type": "git", + "url": "git://git.ppad.tech/sha256.git" + }, + "original": { + "ref": "master", + "type": "git", + "url": "git://git.ppad.tech/sha256.git" + } + }, + "ppad-sha512": { + "inputs": { + "flake-utils": [ + "ppad-sha512", + "ppad-nixpkgs", + "flake-utils" + ], + "nixpkgs": [ + "ppad-sha512", + "ppad-nixpkgs", + "nixpkgs" + ], + "ppad-nixpkgs": [ + "ppad-nixpkgs" + ] + }, + "locked": { + "lastModified": 1737298660, + "narHash": "sha256-W8wuLHRH7P5oITCXnxKEEnSD2yX1Qo7uypbxpwKvvM8=", + "ref": "master", + "rev": "e8ce88cafbf32900556832d3817997642f128242", + "revCount": 21, + "type": "git", + "url": "git://git.ppad.tech/sha512.git" + }, + "original": { + "ref": "master", + "type": "git", + "url": "git://git.ppad.tech/sha512.git" + } + }, + "root": { + "inputs": { + "flake-utils": [ + "ppad-nixpkgs", + "flake-utils" + ], + "nixpkgs": [ + "ppad-nixpkgs", + "nixpkgs" + ], + "ppad-base16": "ppad-base16", + "ppad-nixpkgs": "ppad-nixpkgs", + "ppad-sha256": "ppad-sha256", + "ppad-sha512": "ppad-sha512" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix @@ -0,0 +1,89 @@ +{ + description = "Pure Haskell PBKDF functions."; + + inputs = { + ppad-nixpkgs = { + type = "git"; + url = "git://git.ppad.tech/nixpkgs.git"; + ref = "master"; + }; + ppad-base16 = { + type = "git"; + url = "git://git.ppad.tech/base16.git"; + ref = "master"; + inputs.ppad-nixpkgs.follows = "ppad-nixpkgs"; + }; + ppad-sha256 = { + type = "git"; + url = "git://git.ppad.tech/sha256.git"; + ref = "master"; + inputs.ppad-nixpkgs.follows = "ppad-nixpkgs"; + }; + ppad-sha512 = { + type = "git"; + url = "git://git.ppad.tech/sha512.git"; + ref = "master"; + inputs.ppad-nixpkgs.follows = "ppad-nixpkgs"; + }; + flake-utils.follows = "ppad-nixpkgs/flake-utils"; + nixpkgs.follows = "ppad-nixpkgs/nixpkgs"; + }; + + outputs = { self, nixpkgs, flake-utils, ppad-nixpkgs + , ppad-sha256, ppad-sha512 + , ppad-base16 }: + flake-utils.lib.eachDefaultSystem (system: + let + lib = "ppad-pbkdf"; + + pkgs = import nixpkgs { inherit system; }; + hlib = pkgs.haskell.lib; + + base16 = ppad-base16.packages.${system}.default; + sha256 = ppad-sha256.packages.${system}.default; + sha512 = ppad-sha512.packages.${system}.default; + + hpkgs = pkgs.haskell.packages.ghc981.extend (new: old: { + ppad-base16 = base16; + ppad-sha256 = sha256; + ppad-sha512 = sha512; + ${lib} = new.callCabal2nix lib ./. { + ppad-base16 = new.ppad-base16; + ppad-sha256 = new.ppad-sha256; + ppad-sha512 = new.ppad-sha512; + }; + }); + + cc = pkgs.stdenv.cc; + ghc = hpkgs.ghc; + cabal = hpkgs.cabal-install; + in + { + packages.default = hpkgs.${lib}; + + devShells.default = hpkgs.shellFor { + packages = p: [ + (hlib.doBenchmark p.${lib}) + ]; + + buildInputs = [ + cabal + cc + ]; + + inputsFrom = builtins.attrValues self.packages.${system}; + + doBenchmark = true; + + shellHook = '' + PS1="[${lib}] \w$ " + echo "entering ${system} shell, using" + echo "cc: $(${cc}/bin/cc --version)" + echo "ghc: $(${ghc}/bin/ghc --version)" + echo "cabal: $(${cabal}/bin/cabal --version)" + ''; + }; + } + ); +} + diff --git a/lib/Crypto/KDF/PBKDF.hs b/lib/Crypto/KDF/PBKDF.hs @@ -0,0 +1,85 @@ +{-# LANGUAGE BangPatterns #-} +{-# LANGUAGE BinaryLiterals #-} +{-# LANGUAGE NumericUnderscores #-} + +module Crypto.KDF.PBKDF where + +import Data.Bits ((.>>.), (.&.)) +import qualified Data.Bits as B +import qualified Data.ByteString as BS +import qualified Data.ByteString.Builder as BSB +import Data.Word (Word32, Word64) + +-- NB following synonym really only exists to make haddocks more +-- readable + +-- | A HMAC function, taking a key as the first argument and the input +-- value as the second, producing a MAC digest. +-- +-- >>> import qualified Crypto.Hash.SHA256 as SHA256 +-- >>> :t SHA256.hmac +-- SHA256.hmac :: BS.ByteString -> BS.ByteString -> BS.ByteString +-- >>> SHA256.hmac "my HMAC key" "my HMAC input" +-- <256-bit MAC> +type HMAC = BS.ByteString -> BS.ByteString -> BS.ByteString + +fi :: (Integral a, Num b) => a -> b +fi = fromIntegral +{-# INLINE fi #-} + +-- serialize a 32-bit word, MSB first +ser32 :: Word32 -> BS.ByteString +ser32 w = + let !mask = 0b00000000_00000000_00000000_11111111 + !w0 = fi (w .>>. 24) .&. mask + !w1 = fi (w .>>. 16) .&. mask + !w2 = fi (w .>>. 08) .&. mask + !w3 = fi w .&. mask + in BS.cons w0 (BS.cons w1 (BS.cons w2 (BS.singleton w3))) + +-- bytewise xor on bytestrings +xor :: BS.ByteString -> BS.ByteString -> BS.ByteString +xor = BS.packZipWith B.xor +{-# INLINE xor #-} + +derive + :: HMAC -- ^ HMAC function + -> BS.ByteString -- ^ password + -> BS.ByteString -- ^ salt + -> Word64 -- ^ iteration count + -> Word32 -- ^ bytelength of derived key (max 0xffff_ffff * hlen) + -> BS.ByteString -- ^ derived key +derive prf p s c dklen + | dklen > 0xffff_ffff * fi hlen = -- 2 ^ 32 - 1 + error "ppad-pbkdf (derive): derived key too long" + | otherwise = + loop mempty 1 + where + !hlen = BS.length (prf mempty mempty) + !l = ceiling (fi dklen / fi hlen :: Double) :: Word32 + !r = fi (dklen - (l - 1) * fi hlen) + + f !i = + let go j !acc !las + | j == c = acc + | otherwise = + let u = prf p las + nacc = acc `xor` u + in go (j + 1) nacc u + + org = prf p (s <> ser32 i) + + in go 0 org org + {-# INLINE f #-} + + loop !acc !i + | i == l = + let t = f i + fin = BS.take r t + in BS.toStrict . BSB.toLazyByteString $ + acc <> BSB.byteString fin + | otherwise = + let t = f i + nacc = acc <> BSB.byteString t + in loop nacc (i + 1) + diff --git a/ppad-pbkdf.cabal b/ppad-pbkdf.cabal @@ -0,0 +1,71 @@ +cabal-version: 3.0 +name: ppad-pbkdf +version: 0.2.0 +synopsis: A password-based key derivation function +license: MIT +license-file: LICENSE +author: Jared Tobin +maintainer: jared@ppad.tech +category: Cryptography +build-type: Simple +tested-with: GHC == { 9.8.1 } +extra-doc-files: CHANGELOG +description: + A pure implementation of the password-based key derivation function PBKDF2, + per RFC 2898. + +source-repository head + type: git + location: git.ppad.tech/pbkdf.git + +library + default-language: Haskell2010 + hs-source-dirs: lib + ghc-options: + -Wall + exposed-modules: + Crypto.KDF.PBKDF + build-depends: + base >= 4.9 && < 5 + , bytestring >= 0.9 && < 0.13 + +test-suite pbkdf-tests + type: exitcode-stdio-1.0 + default-language: Haskell2010 + hs-source-dirs: test + main-is: Main.hs + other-modules: + Wycheproof + + ghc-options: + -rtsopts -Wall -O2 + + build-depends: + aeson + , base + , bytestring + , ppad-base16 + , ppad-pbkdf + , ppad-sha256 + , ppad-sha512 + , tasty + , tasty-hunit + , text + +benchmark pbkdf-bench + type: exitcode-stdio-1.0 + default-language: Haskell2010 + hs-source-dirs: bench + main-is: Main.hs + + ghc-options: + -rtsopts -O2 -Wall + + build-depends: + base + , bytestring + , criterion + , ppad-pbkdf + , ppad-sha256 + , ppad-sha512 + diff --git a/test/Main.hs b/test/Main.hs @@ -0,0 +1,4 @@ +module Main where + +main :: IO () +main = pure () diff --git a/test/Wycheproof.hs b/test/Wycheproof.hs @@ -0,0 +1,2 @@ + +module Wycheproof where