packages feed

ppad-chacha 0.2.1 → 0.2.2

raw patch · 6 files changed

+379/−8 lines, 6 filesdep +weighdep ~basedep ~bytestringPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: weigh

Dependency ranges changed: base, bytestring

API changes (from Hackage documentation)

- Crypto.Cipher.ChaCha20: instance GHC.Classes.Eq Crypto.Cipher.ChaCha20.Key
- Crypto.Cipher.ChaCha20: instance GHC.Classes.Eq Crypto.Cipher.ChaCha20.Nonce

Files

CHANGELOG view
@@ -1,5 +1,9 @@ # Changelog +- 0.2.2 (2026-05-16)+  * Features order-of-magnitude performance improvements, especially+    on ARM platforms where NEON intrinsics are available.+ - 0.2.1 (2025-12-28)   * Adds an 'llvm' build flag and tests with GHC 9.10.3. 
+ bench/Weight.hs view
@@ -0,0 +1,48 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# LANGUAGE BangPatterns #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE StandaloneDeriving #-}++module Main where++import Control.DeepSeq+import qualified Crypto.Cipher.ChaCha20 as ChaCha20+import qualified Data.ByteString as BS+import GHC.Generics+import Weigh++deriving instance Generic ChaCha20.Error++instance NFData ChaCha20.Error++-- note that 'weigh' doesn't work properly in a repl+main :: IO ()+main = mainWith $ do+  block+  cipher++block :: Weigh ()+block =+  let !key = BS.replicate 32 0x88+      !non = BS.replicate 12 0x00+  in  wgroup "block" $+        func "block" (ChaCha20.block key 1) non++cipher :: Weigh ()+cipher =+  let !key = BS.replicate 32 0x88+      !non = BS.replicate 12 0x00+      !bs0 = BS.replicate 64 0+      !bs1 = BS.replicate 256 0+      !bs2 = BS.replicate 1024 0+      !bs3 = BS.replicate 4096 0+  in  wgroup "cipher" $ do+        func "cipher (64B   input)"+          (ChaCha20.cipher key 1 non) bs0+        func "cipher (256B  input)"+          (ChaCha20.cipher key 1 non) bs1+        func "cipher (1024B input)"+          (ChaCha20.cipher key 1 non) bs2+        func "cipher (4096B input)"+          (ChaCha20.cipher key 1 non) bs3
+ cbits/chacha20_arm.c view
@@ -0,0 +1,199 @@+#include <stddef.h>+#include <stdint.h>+#include <string.h>++#if defined(__aarch64__)++#include <arm_neon.h>++/*+ * ChaCha20 NEON kernel using intra-block parallelism.  The 16-word+ * state matrix+ *+ *     s00 s01 s02 s03+ *     s04 s05 s06 s07+ *     s08 s09 s10 s11+ *     s12 s13 s14 s15+ *+ * is held in four 128-bit NEON registers v0..v3, one per row.  A+ * column quarter-round on (s00, s04, s08, s12), (s01, s05, s09, s13),+ * etc., becomes one set of element-wise vector operations on+ * (v0, v1, v2, v3) — four quarter-rounds in parallel.  Diagonal+ * rounds are reached by left-rotating v1, v2, v3 by 1, 2, 3 lanes+ * respectively with VEXT before the second quarter-round, then+ * rotating back.+ */++/* 32-bit left rotations.  Rotate-by-16 reduces to REV32.u16; the+ * others compile to a shift-shift-or pair (the compiler folds rotate-+ * by-8 to a TBL with a constant shuffle on some targets).         */+#define ROTL32_16(x) \+    vreinterpretq_u32_u16(vrev32q_u16(vreinterpretq_u16_u32(x)))+#define ROTL32_12(x) \+    vorrq_u32(vshlq_n_u32((x), 12), vshrq_n_u32((x), 20))+#define ROTL32_8(x) \+    vorrq_u32(vshlq_n_u32((x),  8), vshrq_n_u32((x), 24))+#define ROTL32_7(x) \+    vorrq_u32(vshlq_n_u32((x),  7), vshrq_n_u32((x), 25))++#define QUARTER(v0, v1, v2, v3)                          \+    do {                                                  \+        v0 = vaddq_u32(v0, v1);                           \+        v3 = veorq_u32(v3, v0); v3 = ROTL32_16(v3);       \+        v2 = vaddq_u32(v2, v3);                           \+        v1 = veorq_u32(v1, v2); v1 = ROTL32_12(v1);       \+        v0 = vaddq_u32(v0, v1);                           \+        v3 = veorq_u32(v3, v0); v3 = ROTL32_8(v3);        \+        v2 = vaddq_u32(v2, v3);                           \+        v1 = veorq_u32(v1, v2); v1 = ROTL32_7(v1);        \+    } while (0)++/* 20-round ChaCha20 core: 10 iterations of (column + diagonal). */+static inline void chacha20_core(uint32x4_t *v0, uint32x4_t *v1,+                                  uint32x4_t *v2, uint32x4_t *v3,+                                  uint32x4_t s0, uint32x4_t s1,+                                  uint32x4_t s2, uint32x4_t s3) {+    uint32x4_t a = s0, b = s1, c = s2, d = s3;+    for (int i = 0; i < 10; i++) {+        QUARTER(a, b, c, d);+        /* shift rows: row 1 left 1, row 2 left 2, row 3 left 3.    */+        b = vextq_u32(b, b, 1);+        c = vextq_u32(c, c, 2);+        d = vextq_u32(d, d, 3);+        QUARTER(a, b, c, d);+        /* shift back.                                              */+        b = vextq_u32(b, b, 3);+        c = vextq_u32(c, c, 2);+        d = vextq_u32(d, d, 1);+    }+    *v0 = vaddq_u32(a, s0);+    *v1 = vaddq_u32(b, s1);+    *v2 = vaddq_u32(c, s2);+    *v3 = vaddq_u32(d, s3);+}++static const uint32_t chacha_constants[4] = {+    0x61707865u, 0x3320646eu, 0x79622d32u, 0x6b206574u+};++/* Set up the constant rows of the state from key + nonce.  s3+ * (counter + nonce) varies per block and is built inside the loop. */+static inline void chacha20_setup(const uint8_t key[32],+                                   const uint8_t nonce[12],+                                   uint32x4_t *s0, uint32x4_t *s1,+                                   uint32x4_t *s2,+                                   uint32_t *n0, uint32_t *n1,+                                   uint32_t *n2) {+    *s0 = vld1q_u32(chacha_constants);+    *s1 = vreinterpretq_u32_u8(vld1q_u8(key));+    *s2 = vreinterpretq_u32_u8(vld1q_u8(key + 16));+    memcpy(n0, nonce + 0, 4);+    memcpy(n1, nonce + 4, 4);+    memcpy(n2, nonce + 8, 4);+}++/*+ * Generate one 64-byte ChaCha20 keystream block at 'out'.+ */+void chacha20_block_arm(const uint8_t key[32], uint32_t counter,+                        const uint8_t nonce[12], uint8_t out[64]) {+    uint32x4_t s0, s1, s2;+    uint32_t n0, n1, n2;+    chacha20_setup(key, nonce, &s0, &s1, &s2, &n0, &n1, &n2);++    uint32_t s3_in[4] = { counter, n0, n1, n2 };+    uint32x4_t s3 = vld1q_u32(s3_in);+    uint32x4_t v0, v1, v2, v3;+    chacha20_core(&v0, &v1, &v2, &v3, s0, s1, s2, s3);++    vst1q_u8(out + 0,  vreinterpretq_u8_u32(v0));+    vst1q_u8(out + 16, vreinterpretq_u8_u32(v1));+    vst1q_u8(out + 32, vreinterpretq_u8_u32(v2));+    vst1q_u8(out + 48, vreinterpretq_u8_u32(v3));+}++/*+ * Encrypt/decrypt 'inlen' bytes at 'in' into 'out' using ChaCha20+ * with the given key, starting counter, and nonce.  Stream cipher,+ * so the same routine decrypts.+ */+void chacha20_cipher_arm(const uint8_t key[32], uint32_t counter,+                         const uint8_t nonce[12],+                         const uint8_t *in, uint8_t *out,+                         size_t inlen) {+    uint32x4_t s0, s1, s2;+    uint32_t n0, n1, n2;+    chacha20_setup(key, nonce, &s0, &s1, &s2, &n0, &n1, &n2);++    size_t pos = 0;+    while (pos + 64 <= inlen) {+        uint32_t s3_in[4] = { counter, n0, n1, n2 };+        uint32x4_t s3 = vld1q_u32(s3_in);+        uint32x4_t v0, v1, v2, v3;+        chacha20_core(&v0, &v1, &v2, &v3, s0, s1, s2, s3);++        uint8x16_t i0 = vld1q_u8(in + pos +  0);+        uint8x16_t i1 = vld1q_u8(in + pos + 16);+        uint8x16_t i2 = vld1q_u8(in + pos + 32);+        uint8x16_t i3 = vld1q_u8(in + pos + 48);++        vst1q_u8(out + pos +  0,+                 veorq_u8(i0, vreinterpretq_u8_u32(v0)));+        vst1q_u8(out + pos + 16,+                 veorq_u8(i1, vreinterpretq_u8_u32(v1)));+        vst1q_u8(out + pos + 32,+                 veorq_u8(i2, vreinterpretq_u8_u32(v2)));+        vst1q_u8(out + pos + 48,+                 veorq_u8(i3, vreinterpretq_u8_u32(v3)));++        pos += 64;+        counter++;+    }++    /* trailing partial block (< 64 bytes) */+    if (pos < inlen) {+        uint32_t s3_in[4] = { counter, n0, n1, n2 };+        uint32x4_t s3 = vld1q_u32(s3_in);+        uint32x4_t v0, v1, v2, v3;+        chacha20_core(&v0, &v1, &v2, &v3, s0, s1, s2, s3);++        uint8_t block[64];+        vst1q_u8(block +  0, vreinterpretq_u8_u32(v0));+        vst1q_u8(block + 16, vreinterpretq_u8_u32(v1));+        vst1q_u8(block + 32, vreinterpretq_u8_u32(v2));+        vst1q_u8(block + 48, vreinterpretq_u8_u32(v3));++        size_t remaining = inlen - pos;+        for (size_t i = 0; i < remaining; i++) {+            out[pos + i] = in[pos + i] ^ block[i];+        }+    }+}++int chacha20_arm_available(void) {+    return 1;+}++#else++/* stubs for non-aarch64 builds; never reached because dispatch is+ * gated on 'chacha20_arm_available' returning 0                  */++void chacha20_block_arm(const uint8_t *key, uint32_t counter,+                        const uint8_t *nonce, uint8_t *out) {+    (void)key; (void)counter; (void)nonce; (void)out;+}++void chacha20_cipher_arm(const uint8_t *key, uint32_t counter,+                         const uint8_t *nonce,+                         const uint8_t *in, uint8_t *out,+                         size_t inlen) {+    (void)key; (void)counter; (void)nonce;+    (void)in; (void)out; (void)inlen;+}++int chacha20_arm_available(void) {+    return 0;+}++#endif
lib/Crypto/Cipher/ChaCha20.hs view
@@ -10,7 +10,7 @@ -- License: MIT -- Maintainer: Jared Tobin <jared@ppad.tech> ----- A pure ChaCha20 implementation, as specified by+-- A fast ChaCha20 implementation, as specified by -- [RFC 8439](https://datatracker.ietf.org/doc/html/rfc8439).  module Crypto.Cipher.ChaCha20 (@@ -34,6 +34,7 @@   ) where  import Control.Monad.ST+import qualified Crypto.Cipher.ChaCha20.Arm as Arm import qualified Data.Bits as B import Data.Bits ((.|.), (.<<.), (.^.)) import qualified Data.ByteString as BS@@ -151,7 +152,7 @@   , k6 :: {-# UNPACK #-} !Word32   , k7 :: {-# UNPACK #-} !Word32   }-  deriving (Eq, Show)+  deriving Show  -- parse strict 256-bit bytestring (length unchecked) to key _parse_key :: BS.ByteString -> Key@@ -173,7 +174,7 @@   , n1 :: {-# UNPACK #-} !Word32   , n2 :: {-# UNPACK #-} !Word32   }-  deriving (Eq, Show)+  deriving Show  -- parse strict 96-bit bytestring (length unchecked) to nonce _parse_nonce :: BS.ByteString -> Nonce@@ -289,6 +290,8 @@ block key@(BI.PS _ _ kl) counter nonce@(BI.PS _ _ nl)   | kl /= 32 = Left InvalidKey   | nl /= 12 = Left InvalidNonce+  | Arm.chacha20_arm_available =+      Right (Arm.block key counter nonce)   | otherwise = pure $ runST $ do       let k = _parse_key key           n = _parse_nonce nonce@@ -341,8 +344,10 @@   -> BS.ByteString    -- ^ arbitrary-length plaintext   -> Either Error BS.ByteString    -- ^ ciphertext cipher raw_key@(BI.PS _ _ kl) counter raw_nonce@(BI.PS _ _ nl) plaintext-  | kl /= 32  = Left InvalidKey-  | nl /= 12  = Left InvalidNonce+  | kl /= 32 = Left InvalidKey+  | nl /= 12 = Left InvalidNonce+  | Arm.chacha20_arm_available =+      Right (Arm.cipher raw_key counter raw_nonce plaintext)   | otherwise = pure $ runST $ do       let key = _parse_key raw_key           non = _parse_nonce raw_nonce
+ lib/Crypto/Cipher/ChaCha20/Arm.hs view
@@ -0,0 +1,82 @@+{-# OPTIONS_HADDOCK hide #-}+{-# LANGUAGE BangPatterns #-}++-- |+-- Module: Crypto.Cipher.ChaCha20.Arm+-- Copyright: (c) 2025 Jared Tobin+-- License: MIT+-- Maintainer: Jared Tobin <jared@ppad.tech>+--+-- ARM NEON support for the ChaCha20 stream cipher.++module Crypto.Cipher.ChaCha20.Arm (+    chacha20_arm_available+  , block+  , cipher+  ) where++import qualified Data.ByteString as BS+import qualified Data.ByteString.Internal as BI+import Data.Word (Word8, Word32)+import Foreign.C.Types (CInt(..), CSize(..))+import Foreign.ForeignPtr (withForeignPtr)+import Foreign.Ptr (Ptr, plusPtr)+import System.IO.Unsafe (unsafeDupablePerformIO)++-- ffi ------------------------------------------------------------------------++foreign import ccall unsafe "chacha20_block_arm"+  c_chacha20_block+    :: Ptr Word8 -> Word32 -> Ptr Word8 -> Ptr Word8 -> IO ()++foreign import ccall unsafe "chacha20_cipher_arm"+  c_chacha20_cipher+    :: Ptr Word8 -> Word32 -> Ptr Word8+    -> Ptr Word8 -> Ptr Word8 -> CSize -> IO ()++foreign import ccall unsafe "chacha20_arm_available"+  c_chacha20_arm_available :: IO CInt++-- utilities ------------------------------------------------------------------++fi :: (Integral a, Num b) => a -> b+fi = fromIntegral+{-# INLINE fi #-}++-- api ------------------------------------------------------------------------++-- | Are ARM NEON extensions available?+chacha20_arm_available :: Bool+chacha20_arm_available =+  unsafeDupablePerformIO c_chacha20_arm_available /= 0+{-# NOINLINE chacha20_arm_available #-}++-- | One 64-byte ChaCha20 keystream block for the given (already-+--   validated) key, counter, and nonce.+block :: BS.ByteString -> Word32 -> BS.ByteString -> BS.ByteString+block (BI.PS kfp koff _) counter (BI.PS nfp noff _) =+  BI.unsafeCreate 64 $ \dst ->+    withForeignPtr kfp $ \kp0 ->+    withForeignPtr nfp $ \np0 ->+      c_chacha20_block (kp0 `plusPtr` koff)+                       counter+                       (np0 `plusPtr` noff)+                       dst++-- | XOR the plaintext with the ChaCha20 keystream derived from the+--   given (already-validated) key, counter, and nonce.+cipher+  :: BS.ByteString -> Word32 -> BS.ByteString -> BS.ByteString+  -> BS.ByteString+cipher (BI.PS kfp koff _) counter (BI.PS nfp noff _)+       (BI.PS pfp poff plen) =+  BI.unsafeCreate plen $ \dst ->+    withForeignPtr kfp $ \kp0 ->+    withForeignPtr nfp $ \np0 ->+    withForeignPtr pfp $ \pp0 ->+      c_chacha20_cipher (kp0 `plusPtr` koff)+                        counter+                        (np0 `plusPtr` noff)+                        (pp0 `plusPtr` poff)+                        dst+                        (fi plen)
ppad-chacha.cabal view
@@ -1,7 +1,7 @@ cabal-version:      3.0 name:               ppad-chacha-version:            0.2.1-synopsis:           A pure ChaCha20 stream cipher+version:            0.2.2+synopsis:           A fast ChaCha20 stream cipher license:            MIT license-file:       LICENSE author:             Jared Tobin@@ -11,13 +11,18 @@ tested-with:        GHC == 9.10.3 extra-doc-files:    CHANGELOG description:-  A pure ChaCha20 stream cipher and block function.+  A fast ChaCha20 stream cipher and block function.  flag llvm   description: Use GHC's LLVM backend.   default:     False   manual:      True +flag sanitize+  description: Build with AddressSanitizer and UndefinedBehaviorSanitizer.+  default:     False+  manual:      True+ source-repository head   type:     git   location: git.ppad.tech/chacha.git@@ -31,10 +36,18 @@     ghc-options: -fllvm -O2   exposed-modules:       Crypto.Cipher.ChaCha20+      Crypto.Cipher.ChaCha20.Arm   build-depends:       base >= 4.9 && < 5     , bytestring >= 0.9 && < 0.13     , primitive >= 0.8 && < 0.10+  c-sources:+      cbits/chacha20_arm.c+  if arch(aarch64)+    cc-options: -march=armv8-a+  if flag(sanitize)+    cc-options: -fsanitize=address,undefined -fno-omit-frame-pointer+    ghc-options: -optl=-fsanitize=address,undefined  test-suite chacha-tests   type:                exitcode-stdio-1.0@@ -44,6 +57,8 @@    ghc-options:     -rtsopts -Wall -O2+  if flag(sanitize)+    ghc-options: -optl=-fsanitize=address,undefined    build-depends:       base@@ -70,4 +85,22 @@     , deepseq     , ppad-base16     , ppad-chacha++benchmark chacha-weigh+  type:                exitcode-stdio-1.0+  default-language:    Haskell2010+  hs-source-dirs:      bench+  main-is:             Weight.hs++  ghc-options:+    -rtsopts -O2 -Wall -fno-warn-orphans+  if flag(llvm)+    ghc-options: -fllvm++  build-depends:+      base+    , bytestring+    , deepseq+    , ppad-chacha+    , weigh