crypton-box (empty) → 1.1.0
raw patch · 9 files changed
+690/−0 lines, 9 filesdep +basedep +bytestringdep +crypton
Dependencies added: base, bytestring, crypton, crypton-box, hspec, memory
Files
- ChangeLog.md +12/−0
- LICENSE +29/−0
- README.md +23/−0
- crypton-box.cabal +74/−0
- src/Crypto/Box.hs +209/−0
- src/Crypto/SecretBox.hs +67/−0
- test/Crypto/BoxSpec.hs +162/−0
- test/Crypto/SecretBoxSpec.hs +113/−0
- test/Spec.hs +1/−0
+ ChangeLog.md view
@@ -0,0 +1,12 @@+# Change Log for crypton-box + +## Unreleased Changes + +## 1.1.0 + +- Migrate away from the deprecated `cryptonite` package to `crypton` +- Rename package from `shecretbox` to `crypton-box` for clarity + +## 1.0.0 + +- Implement CryptoBox and SecretBox create/open operations
+ LICENSE view
@@ -0,0 +1,29 @@+BSD 3-Clause License + +Copyright (c) 2022, Yuto Takano +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,23 @@+# crypton-box + +`crypton-box` is a Haskell library that provides NaCl's [box](https://nacl.cr.yp.to/box.html) and [secretbox](https://nacl.cr.yp.to/secretbox.html) operations, using primitives provided in [`crypton`](https://hackage.haskell.org/package/crypton). + +**Important**: This library is provided as a proof of concept. Please carefully evaluate the security related to your requirements before using! No professional security review has taken place for the implementations, and I **strongly recommended** not to use this library for anything critical. + +## Usage + +Qualified imports are recommended for use: + +```hs +import qualified Crypto.PubKey.Curve25519 as X25519 +import qualified Crypto.Box as Box + +message, nonce :: B.ByteString +secret :: X25519.DhSecret + +encrypted :: B.ByteString +encrypted = Box.create message nonce secret + +decrypted :: B.ByteString +decrypted = Box.open encrypted nonce secret +```
+ crypton-box.cabal view
@@ -0,0 +1,74 @@+cabal-version: 1.12 + +name: crypton-box +version: 1.1.0 +synopsis: NaCl crypto/secret box implementations based on crypton primitives. +description: + This library provides a high-level API for authenticated encryption and + decryption using the NaCl [crypto_box](https://nacl.cr.yp.to/box.html) and + [crypto_secretbox](https://nacl.cr.yp.to/secretbox.html) constructs. + . + The API is implemented in pure Haskell using XSalsa and Poly1305 primitives + provided by the [crypton](https://hackage.haskell.org/package/crypton) library. + . + __Important: This library has not been professionally reviewed. Side__ + __channel attacks and memory-related vulnerabilities may exist! Use at your__ + __own risk.__ + . + /(P.S. I would love to hear from you if you can audit this library and/ + /improve its security!)/ +category: Cryptography +homepage: https://github.com/yutotakano/crypton-box#readme +bug-reports: https://github.com/yutotakano/crypton-box/issues +author: Yuto Takano +maintainer: moa17stock@gmail.com +copyright: 2025 Yuto Takano +license: BSD3 +license-file: LICENSE +build-type: Simple +extra-source-files: + README.md + ChangeLog.md + +tested-with: + GHC == 9.4.8 + +source-repository head + type: git + location: https://github.com/yutotakano/crypton-box + +library + exposed-modules: + Crypto.Box + Crypto.SecretBox + hs-source-dirs: + src + default-extensions: + OverloadedStrings + build-depends: + base >=4.7 && <5 + , bytestring >= 0.9 && <1.0.0.0 + , crypton >= 1.0.0 && <2.0.0 + , memory >= 0.18 && <1.0 + default-language: Haskell2010 + +test-suite crypton-box-test + type: exitcode-stdio-1.0 + main-is: Spec.hs + other-modules: + Crypto.BoxSpec + Crypto.SecretBoxSpec + hs-source-dirs: + test + default-extensions: + OverloadedStrings + build-tool-depends: + hspec-discover:hspec-discover + build-depends: + base >=4.7 && <5 + , bytestring + , crypton + , hspec + , memory + , crypton-box + default-language: Haskell2010
+ src/Crypto/Box.hs view
@@ -0,0 +1,209 @@+{-# LANGUAGE ImportQualifiedPost #-} +-- | This module provides the creation and opening of a crypto_box. +module Crypto.Box where + +import Crypto.Cipher.Salsa (State(..)) +import Crypto.Cipher.XSalsa qualified as XSalsa +import Crypto.ECC qualified as ECC +import Crypto.Error (CryptoFailable(..)) +import Crypto.MAC.Poly1305 qualified as Poly1305 +import Crypto.PubKey.Curve25519 qualified as X25519 +import Data.ByteArray qualified as BA +import Data.ByteString qualified as B +import Data.Data (Proxy(..)) +import Data.Function ((&)) +import Data.Foldable (traverse_) +import Foreign.Ptr qualified as Ptr +import Foreign.Storable qualified as Storable +import GHC.IO (unsafePerformIO) + +-- The implementation in this module is based off a small snippet of code from +-- the cryptonite library, which is licensed under the BSD3 license. +-- https://github.com/haskell-crypto/cryptonite/blob/master/Crypto/Tutorial.hs + +-- | Build a @crypto_box@ packet encrypting the specified content with a +-- 192-bit nonce, receiver public key and sender private key. +-- +-- This function performs no validation for the key pair, and will use an +-- all-zero shared secret if the Diffie hellman secret value is at infinity. +-- Use 'Box.beforeNM' to get only the precomputed secret if you want to verify +-- the key pair before use. +create + :: (BA.ByteArray content, BA.ByteArray nonce) + => content + -- ^ Message to encrypt + -> nonce + -- ^ 192-bit nonce + -> X25519.PublicKey + -- ^ Receiver's public key + -> X25519.SecretKey + -- ^ Sender's private key + -> content + -- ^ Ciphertext +create message nonce pk sk = BA.convert tag `BA.append` c + -- convert the tag from Auth to ByteString (reallocating), instead of + -- converting both of them to a polymorphic (ByteArrayAccess ciphertext), + -- preventing unnecessary conversion. People who need other byte access types + -- can convert it themselves. + where + shared = X25519.dh pk sk + (iv0, iv1) = BA.splitAt 8 nonce + zero = BA.zero 16 + state0 = XSalsa.initialize 20 shared (zero `BA.append` iv0) + state1 = XSalsa.derive state0 iv1 + (rs, state2) = XSalsa.generate state1 32 + (c, _) = XSalsa.combine state2 message + tag = Poly1305.auth (rs :: B.ByteString) c + +-- | Precompute the shared key for building a @crypto_box@ packet. +-- This function first computes the shared secret using the receiver public key +-- and sender private key. Then, a first-level key is computed using HSalsa20 +-- with the shared secret and a nonce of zero. This is as described in section +-- 7 (page 15) of https://cr.yp.to/highspeed/naclcrypto-20090310.pdf. +-- +-- The function returns a XSalsa State that contains the first-level key. +-- +-- May fail if the Diffie hellman secret value is at infinity. See 'ECC.ecdh' for +-- more information. +beforeNM + :: X25519.PublicKey + -- ^ Receiver public key + -> X25519.SecretKey + -- ^ Sender private key + -> CryptoFailable XSalsa.State + -- ^ XSalsa State that contains the precomputed first-level key to use with 'createAfterNM' or 'openAfterNM' +beforeNM pk sk = do + let zero = B.replicate 24 0 + shared <- ECC.ecdh (Proxy :: Proxy ECC.Curve_X25519) sk pk + pure $ XSalsa.initialize 20 shared zero + +-- | Build a @crypto_box@ packet that encrypts the specified content with a +-- 192-bit nonce and a state containing the precomputed first-level key. +-- Use 'beforeNM' to create such a state. +createAfterNM + :: (BA.ByteArray content, BA.ByteArray nonce) + => content + -- ^ Message to encrypt + -> nonce + -- ^ 192-bit nonce + -> XSalsa.State + -- ^ XSalsa State that contains the precomputed first-level key + -> content + -- ^ Ciphertext +createAfterNM message nonce (State state0) = BA.convert tag `BA.append` c + where + zero = B.replicate 16 0 + (iv0, iv1) = BA.splitAt 8 nonce + -- This is very hacky. The XSalsa.initialise that we performed in the beforeNM + -- stage has mostly what we need, except for state[6] and state[7] which is + -- where the first 8 bytes of the IV/nonce go to. Since those are currently + -- zero because we used zeros during the beforeNM (we didn't know the nonce), + -- we now need to poke into that pointer location and overwrite its contents. + state1 = unsafePerformIO $ do + memview <- BA.withByteArray state0 $ \state0Ptr -> do + -- We start writing at byte 24, this is 6*4 where 6 is the location + -- that the 16th byte of the 24-byte IV (the first 16 are zeros even + -- if we use cryptoBox) passed to xsalsa is written to, and 4 is + -- because the base type is uint32. + -- + -- TODO: This currently relies on the system endianness since we're + -- writing the nonce bytes one by one instead of as a little-endian + -- 32-bit chunk. This needs to be made system-agnostic. + BA.unpack iv0 + & zip [24..31] + & traverse_ (\(i, word) -> + Storable.poke (state0Ptr `Ptr.plusPtr` i) word) + -- Return the 132 bytes that is the size of the State struct + -- This is how much is allocated when you look at the source of + -- XSalsa.initialize, and the struct contents are defined here: + -- https://github.com/haskell-crypto/cryptonite/blob/cf89276b5cdd87fcd60cce2fb424e64f0de7016a/cbits/cryptonite_salsa.h + pure $ BA.MemView state0Ptr 132 + -- Convert from a pointer to Words to a pointer to State + pure $ State $ BA.convert $ memview + + state2 = XSalsa.derive state1 iv1 + (rs, state3) = XSalsa.generate state2 32 + (c, _) = XSalsa.combine state3 message + tag = Poly1305.auth (rs :: B.ByteString) c + +-- | Try to open a @crypto_box@ packet and recover the content using the +-- 192-bit nonce, sender public key and receiver private key. +-- +-- This function performs no validation for the key pair, and will use an +-- all-zero shared secret if the Diffie hellman secret value is at infinity. +-- Use 'Box.beforeNM' to get only the precomputed secret if you want to verify +-- the key pair before use. +open + :: (BA.ByteArray content, BA.ByteArray nonce) + => content + -- ^ Ciphertext to decrypt + -> nonce + -- ^ 192-bit nonce + -> X25519.PublicKey + -- ^ Sender's public key + -> X25519.SecretKey + -- ^ Receiver's private key + -> Maybe content + -- ^ Plaintext +open packet nonce pk sk + | BA.length packet < 16 = Nothing + | BA.constEq tag' tag = Just content + | otherwise = Nothing + where + (tag', c) = BA.splitAt 16 packet + zero = BA.zero 16 + shared = X25519.dh pk sk + (iv0, iv1) = BA.splitAt 8 nonce + state0 = XSalsa.initialize 20 shared (zero `BA.append` iv0) + state1 = XSalsa.derive state0 iv1 + (rs, state2) = XSalsa.generate state1 32 + (content, _) = XSalsa.combine state2 c + tag = Poly1305.auth (rs :: B.ByteString) c + +-- | Try to open a @crypto_box@ packet and recover the content using the +-- 192-bit nonce and a state containing the precomputed first-level key. +-- Use 'beforeNM' to create such a state. +openAfterNM + :: (BA.ByteArray content, BA.ByteArray nonce) + => content + -- ^ Ciphertext + -> nonce + -- ^ 192-bit nonce + -> XSalsa.State + -- ^ XSalsa State that contains the precomputed first-level key + -> Maybe content +openAfterNM packet nonce (State state0) + | BA.length packet < 16 = Nothing + | BA.constEq tag' tag = Just content + | otherwise = Nothing + where + (tag', c) = BA.splitAt 16 packet + (iv0, iv1) = BA.splitAt 8 nonce + + -- This is very hacky. The XSalsa.initialise that we performed in the beforeNM + -- stage has mostly what we need, except for state[6] and state[7] which is + -- where the first 8 bytes of the IV/nonce go to. Since those are currently + -- zero because we used zeros during the beforeNM (we didn't know the nonce), + -- we now need to poke into that pointer location and overwrite its contents. + state1 = unsafePerformIO $ do + memview <- BA.withByteArray state0 $ \state0Ptr -> do + -- We start writing at byte 24, this is 6*4 where 6 is the location + -- that the 16th byte of the 24-byte IV (the first 16 are zeros even + -- if we use cryptoBox) passed to xsalsa is written to, and 4 is + -- because the base type is uint32. + BA.unpack iv0 + & zip [24..31] + & traverse_ (\(i, word) -> + Storable.poke (state0Ptr `Ptr.plusPtr` i) word) + -- Return the 132 bytes that is the size of the State struct + -- This is how much is allocated when you look at the source of + -- XSalsa.initialize, and the struct contents are defined here: + -- https://github.com/haskell-crypto/cryptonite/blob/cf89276b5cdd87fcd60cce2fb424e64f0de7016a/cbits/cryptonite_salsa.h + pure $ BA.MemView state0Ptr 132 + -- Convert from a pointer to Words to a pointer to State + pure $ State $ BA.convert $ memview + + state2 = XSalsa.derive state1 iv1 + (rs, state3) = XSalsa.generate state2 32 + (content, _) = XSalsa.combine state3 c + tag = Poly1305.auth (rs :: B.ByteString) c
+ src/Crypto/SecretBox.hs view
@@ -0,0 +1,67 @@+{-# LANGUAGE ImportQualifiedPost #-} +-- | This module provides the creation and opening of a secret_box. +module Crypto.SecretBox where + +import Data.ByteArray qualified as BA +import Data.ByteString qualified as B + +import Crypto.Cipher.XSalsa qualified as XSalsa +import Crypto.MAC.Poly1305 qualified as Poly1305 +import Crypto.PubKey.Curve25519 qualified as X25519 + +-- | Build a @secret_box@ packet encrypting the specified content with a +-- 192-bit nonce and a 256-bit symmetric secret key. +create + :: (BA.ByteArray content, BA.ByteArray nonce) + => content + -- ^ Message to encrypt + -> nonce + -- ^ 192-bit nonce + -> X25519.DhSecret + -- ^ Symmetric secret key + -> content + -- ^ Ciphertext +create message nonce key = BA.convert tag `BA.append` c + where + -- No need to prepend 16 bytes of zero before the nonce and then call derive + -- with the rest. This is because secret_box directly calls + -- crypto_secretbox_xsalsa20poly1305 + -- which begins with the XOR-ing, while crypto_box calls + -- crypto_box_curve25519xsalsa20poly1305_beforenm + -- (which has a single HSalsa() round applied on the shared secret) before + -- calling + -- crypto_box_curve25519xsalsa20poly1305_afternm, which calls + -- crypto_secretbox_xsalsa20poly1305 + -- + -- + -- So, since we already have a symmetric secret key that has one layer of + -- HSalsa() performed on it, we can directly use that without calling "derive" + -- twice (once in init and once in derive) and start performing the xor after + -- the first HSalsa() in initialize. + state0 = XSalsa.initialize 20 key nonce + (rs, state1) = XSalsa.generate state0 32 + (c, _) = XSalsa.combine state1 message + tag = Poly1305.auth (rs :: B.ByteString) c + +-- | Try to open a @secret_box@ packet and recover the content using the +-- 192-bit nonce and a 256-bit symmetric secret key. +open + :: (BA.ByteArray content, BA.ByteArray nonce) + => content + -- ^ Ciphertext to decrypt + -> nonce + -- ^ 192-bit nonce + -> X25519.DhSecret + -- ^ Symmetric secret key + -> Maybe content + -- ^ Message +open packet nonce key + | BA.length packet < 16 = Nothing + | BA.constEq tag' tag = Just content + | otherwise = Nothing + where + (tag', c) = BA.splitAt 16 packet + state0 = XSalsa.initialize 20 key nonce + (rs, state1) = XSalsa.generate state0 32 + (content, _) = XSalsa.combine state1 c + tag = Poly1305.auth (rs :: B.ByteString) c
+ test/Crypto/BoxSpec.hs view
@@ -0,0 +1,162 @@+{-# LANGUAGE ImportQualifiedPost #-} + +module Crypto.BoxSpec where + +import Data.ByteString qualified as B +import Data.Function ((&)) +import Test.Hspec +import Text.Printf (printf) +import Control.Monad.IO.Class (liftIO) +import Crypto.Error +import Crypto.PubKey.Curve25519 +import Data.Maybe (fromJust, isJust, isNothing) + +import qualified Crypto.Box as Box + +hexifier :: B.ByteString -> String +hexifier bytes = B.unpack bytes + & map (\word -> printf ",0x%02x" word) + & zip [0..] + & map (\(i, str) -> str <> (if i `mod` 8 == 7 then "\n" else "")) + & concat + +isCryptoPassed :: CryptoFailable a -> Bool +isCryptoPassed (CryptoPassed _) = True +isCryptoPassed _ = False + +isCryptoFailed :: CryptoFailable a -> Bool +isCryptoFailed (CryptoFailed _) = True +isCryptoFailed _ = False + +spec :: Spec +spec = do + -- Taken from https://github.com/jedisct1/libsodium/blob/master/test/default/box.c + describe "cryptobox check 1 (box.exp)" $ do + let aliceskFailable = secretKey $ B.pack + [ 0x77, 0x07, 0x6d, 0x0a, 0x73, 0x18, 0xa5, 0x7d, 0x3c, 0x16, 0xc1 + , 0x72, 0x51, 0xb2, 0x66, 0x45, 0xdf, 0x4c, 0x2f, 0x87, 0xeb, 0xc0 + , 0x99, 0x2a, 0xb1, 0x77, 0xfb, 0xa5, 0x1d, 0xb9, 0x2c, 0x2a + ] + let alicesk = fromJust $ maybeCryptoError aliceskFailable + let bobpkFailable = publicKey $ B.pack + [ 0xde, 0x9e, 0xdb, 0x7d, 0x7b, 0x7d, 0xc1, 0xb4, 0xd3, 0x5b, 0x61 + , 0xc2, 0xec, 0xe4, 0x35, 0x37, 0x3f, 0x83, 0x43, 0xc8, 0x5b, 0x78 + , 0x67, 0x4d, 0xad, 0xfc, 0x7e, 0x14, 0x6f, 0x88, 0x2b, 0x4f + ] + let bobpk = fromJust $ maybeCryptoError bobpkFailable + let smallOrderPFailable = publicKey $ B.pack + [ 0xe0, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3 + , 0xfa, 0xf1, 0x9f, 0xc4, 0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32 + , 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49, 0xb8, 0x00 + ] + let smallOrderP = fromJust $ maybeCryptoError smallOrderPFailable + let nonce = B.pack + [ 0x69, 0x69, 0x6e, 0xe9, 0x55, 0xb6 + , 0x2b, 0x73, 0xcd, 0x62, 0xbd, 0xa8 + , 0x75, 0xfc, 0x73, 0xd6, 0x82, 0x19 + , 0xe0, 0x03, 0x6b, 0x7a, 0x0b, 0x37 + ] + let m = B.pack + [ 0xbe, 0x07, 0x5f, 0xc5 + , 0x3c, 0x81, 0xf2, 0xd5, 0xcf, 0x14, 0x13, 0x16, 0xeb, 0xeb, 0x0c, 0x7b + , 0x52, 0x28, 0xc5, 0x2a, 0x4c, 0x62, 0xcb, 0xd4, 0x4b, 0x66, 0x84, 0x9b + , 0x64, 0x24, 0x4f, 0xfc, 0xe5, 0xec, 0xba, 0xaf, 0x33, 0xbd, 0x75, 0x1a + , 0x1a, 0xc7, 0x28, 0xd4, 0x5e, 0x6c, 0x61, 0x29, 0x6c, 0xdc, 0x3c, 0x01 + , 0x23, 0x35, 0x61, 0xf4, 0x1d, 0xb6, 0x6c, 0xce, 0x31, 0x4a, 0xdb, 0x31 + , 0x0e, 0x3b, 0xe8, 0x25, 0x0c, 0x46, 0xf0, 0x6d, 0xce, 0xea, 0x3a, 0x7f + , 0xa1, 0x34, 0x80, 0x57, 0xe2, 0xf6, 0x55, 0x6a, 0xd6, 0xb1, 0x31, 0x8a + , 0x02, 0x4a, 0x83, 0x8f, 0x21, 0xaf, 0x1f, 0xde, 0x04, 0x89, 0x77, 0xeb + , 0x48, 0xf5, 0x9f, 0xfd, 0x49, 0x24, 0xca, 0x1c, 0x60, 0x90, 0x2e, 0x52 + , 0xf0, 0xa0, 0x89, 0xbc, 0x76, 0x89, 0x70, 0x40, 0xe0, 0x82, 0xf9, 0x37 + , 0x76, 0x38, 0x48, 0x64, 0x5e, 0x07, 0x05 + ] + + -- We use `shouldBe` instead of `shouldSatisfy` because ECC.SharedSecret + -- has no Show instance. + it "checks Box.beforeNM doesn't fail with valid data" $ do + isCryptoPassed (Box.beforeNM bobpk alicesk) `shouldBe` True + + it "checks Box.beforeNM fails with small order p" $ do + isCryptoPassed (Box.beforeNM smallOrderP alicesk) `shouldBe` False + + it "checks Box.create and Box.createAfterNM match expected output" $ do + -- once with cryptoBox + let c1 = Box.create m nonce bobpk alicesk + + -- once with before/after split + let k = fromJust $ maybeCryptoError $ Box.beforeNM bobpk alicesk + let c2 = Box.createAfterNM m nonce k + + let output = hexifier c1 <> "\n" <> hexifier c2 <> "\n" + contents <- liftIO $ readFile "test/Crypto/box.exp" + output `shouldBe` contents + + -- Taken from https://github.com/jedisct1/libsodium/blob/master/test/default/box.c + describe "cryptobox check 2 (box2.exp)" $ do + let bobskFailable = secretKey $ B.pack + [ 0x5d, 0xab, 0x08, 0x7e, 0x62, 0x4a, 0x8a + , 0x4b, 0x79, 0xe1, 0x7f, 0x8b, 0x83, 0x80 + , 0x0e, 0xe6, 0x6f, 0x3b, 0xb1, 0x29, 0x26 + , 0x18, 0xb6, 0xfd, 0x1c, 0x2f, 0x8b, 0x27 + , 0xff, 0x88, 0xe0, 0xeb + ] + bobsk = fromJust $ maybeCryptoError bobskFailable + let alicepkFailable = publicKey $ B.pack + [ 0x85, 0x20, 0xf0, 0x09, 0x89, 0x30, 0xa7 + , 0x54, 0x74, 0x8b, 0x7d, 0xdc, 0xb4, 0x3e + , 0xf7, 0x5a, 0x0d, 0xbf, 0x3a, 0x0d, 0x26 + , 0x38, 0x1a, 0xf4, 0xeb, 0xa4, 0xa9, 0x8e + , 0xaa, 0x9b, 0x4e, 0x6a + ] + alicepk = fromJust $ maybeCryptoError alicepkFailable + let smallOrderPFailable = publicKey $ B.pack + [ 0xe0, 0xeb, 0x7a, 0x7c, 0x3b, 0x41, 0xb8, 0xae, 0x16, 0x56, 0xe3 + , 0xfa, 0xf1, 0x9f, 0xc4, 0x6a, 0xda, 0x09, 0x8d, 0xeb, 0x9c, 0x32 + , 0xb1, 0xfd, 0x86, 0x62, 0x05, 0x16, 0x5f, 0x49, 0xb8, 0x00 + ] + smallOrderP = fromJust $ maybeCryptoError smallOrderPFailable + let nonce = B.pack + [ 0x69, 0x69, 0x6e, 0xe9, 0x55, 0xb6 + , 0x2b, 0x73, 0xcd, 0x62, 0xbd, 0xa8 + , 0x75, 0xfc, 0x73, 0xd6, 0x82, 0x19 + , 0xe0, 0x03, 0x6b, 0x7a, 0x0b, 0x37 + ] + let c = B.pack + [ 0xf3, 0xff, 0xc7, 0x70, 0x3f, 0x94, 0x00, 0xe5 + , 0x2a, 0x7d, 0xfb, 0x4b, 0x3d, 0x33, 0x05, 0xd9, 0x8e, 0x99, 0x3b, 0x9f + , 0x48, 0x68, 0x12, 0x73, 0xc2, 0x96, 0x50, 0xba, 0x32, 0xfc, 0x76, 0xce + , 0x48, 0x33, 0x2e, 0xa7, 0x16, 0x4d, 0x96, 0xa4, 0x47, 0x6f, 0xb8, 0xc5 + , 0x31, 0xa1, 0x18, 0x6a, 0xc0, 0xdf, 0xc1, 0x7c, 0x98, 0xdc, 0xe8, 0x7b + , 0x4d, 0xa7, 0xf0, 0x11, 0xec, 0x48, 0xc9, 0x72, 0x71, 0xd2, 0xc2, 0x0f + , 0x9b, 0x92, 0x8f, 0xe2, 0x27, 0x0d, 0x6f, 0xb8, 0x63, 0xd5, 0x17, 0x38 + , 0xb4, 0x8e, 0xee, 0xe3, 0x14, 0xa7, 0xcc, 0x8a, 0xb9, 0x32, 0x16, 0x45 + , 0x48, 0xe5, 0x26, 0xae, 0x90, 0x22, 0x43, 0x68, 0x51, 0x7a, 0xcf, 0xea + , 0xbd, 0x6b, 0xb3, 0x73, 0x2b, 0xc0, 0xe9, 0xda, 0x99, 0x83, 0x2b, 0x61 + , 0xca, 0x01, 0xb6, 0xde, 0x56, 0x24, 0x4a, 0x9e, 0x88, 0xd5, 0xf9, 0xb3 + , 0x79, 0x73, 0xf6, 0x22, 0xa4, 0x3d, 0x14, 0xa6, 0x59, 0x9b, 0x1f, 0x65 + , 0x4c, 0xb4, 0x5a, 0x74, 0xe3, 0x55, 0xa5 + ] + it "checks Box.open doesn't fail with valid data" $ do + Box.open c nonce alicepk bobsk `shouldSatisfy` isJust + + it "checks Box.open fails with small order p" $ do + Box.open c nonce smallOrderP bobsk `shouldSatisfy` isNothing + + it "checks Box.beforeNM doesn't fail with valid data" $ do + isCryptoPassed (Box.beforeNM alicepk bobsk) `shouldBe` True + + it "checks Box.beforeNM fails with small order p" $ do + isCryptoPassed (Box.beforeNM smallOrderP bobsk) `shouldBe` False + + it "checks Box.open and Box.openAfterNM matches expected output" $ do + -- once with cryptoBoxOpen + let m1 = fromJust $ Box.open c nonce alicepk bobsk + + -- once with before/after split + let k = fromJust $ maybeCryptoError $ Box.beforeNM alicepk bobsk + let m2 = fromJust $ Box.openAfterNM c nonce k + + let output = hexifier m1 <> "\n" <> hexifier m2 <> "\n" + contents <- liftIO $ readFile "test/Crypto/box2.exp" + output `shouldBe` contents +
+ test/Crypto/SecretBoxSpec.hs view
@@ -0,0 +1,113 @@+{-# LANGUAGE ImportQualifiedPost #-} + +module Crypto.SecretBoxSpec where + +import Data.ByteString qualified as B +import Data.Function ((&)) +import Test.Hspec +import Text.Printf (printf) +import Control.Monad.IO.Class (liftIO) +import Crypto.Error +import Crypto.PubKey.Curve25519 +import Data.Maybe (fromJust, isJust, isNothing) + +import qualified Crypto.SecretBox as SecretBox + +hexifier :: B.ByteString -> String +hexifier bytes = B.unpack bytes + & map (\word -> printf ",0x%02x" word) + & zip [0..] + & map (\(i, str) -> str <> (if i `mod` 8 == 7 then "\n" else "")) + & concat + +isCryptoPassed :: CryptoFailable a -> Bool +isCryptoPassed (CryptoPassed _) = True +isCryptoPassed _ = False + +isCryptoFailed :: CryptoFailable a -> Bool +isCryptoFailed (CryptoFailed _) = True +isCryptoFailed _ = False + +spec :: Spec +spec = do + -- Taken from https://github.com/jedisct1/libsodium/blob/master/test/default/secretbox.c + describe "secretbox check 1 (secretbox.exp)" $ do + let firstkeyFailable = dhSecret $ B.pack + [ 0x1b, 0x27, 0x55, 0x64, 0x73, 0xe9, 0x85 + , 0xd4, 0x62, 0xcd, 0x51, 0x19, 0x7a, 0x9a + , 0x46, 0xc7, 0x60, 0x09, 0x54, 0x9e, 0xac + , 0x64, 0x74, 0xf2, 0x06, 0xc4, 0xee, 0x08 + , 0x44, 0xf6, 0x83, 0x89 + ] + let firstkey = fromJust $ maybeCryptoError firstkeyFailable + let nonce = B.pack + [ 0x69, 0x69, 0x6e, 0xe9, 0x55, 0xb6 + , 0x2b, 0x73, 0xcd, 0x62, 0xbd, 0xa8 + , 0x75, 0xfc, 0x73, 0xd6, 0x82, 0x19 + , 0xe0, 0x03, 0x6b, 0x7a, 0x0b, 0x37 + ] + let m = B.pack + [ 0xbe, 0x07, 0x5f, 0xc5 + , 0x3c, 0x81, 0xf2, 0xd5, 0xcf, 0x14, 0x13, 0x16, 0xeb, 0xeb, 0x0c, 0x7b + , 0x52, 0x28, 0xc5, 0x2a, 0x4c, 0x62, 0xcb, 0xd4, 0x4b, 0x66, 0x84, 0x9b + , 0x64, 0x24, 0x4f, 0xfc, 0xe5, 0xec, 0xba, 0xaf, 0x33, 0xbd, 0x75, 0x1a + , 0x1a, 0xc7, 0x28, 0xd4, 0x5e, 0x6c, 0x61, 0x29, 0x6c, 0xdc, 0x3c, 0x01 + , 0x23, 0x35, 0x61, 0xf4, 0x1d, 0xb6, 0x6c, 0xce, 0x31, 0x4a, 0xdb, 0x31 + , 0x0e, 0x3b, 0xe8, 0x25, 0x0c, 0x46, 0xf0, 0x6d, 0xce, 0xea, 0x3a, 0x7f + , 0xa1, 0x34, 0x80, 0x57, 0xe2, 0xf6, 0x55, 0x6a, 0xd6, 0xb1, 0x31, 0x8a + , 0x02, 0x4a, 0x83, 0x8f, 0x21, 0xaf, 0x1f, 0xde, 0x04, 0x89, 0x77, 0xeb + , 0x48, 0xf5, 0x9f, 0xfd, 0x49, 0x24, 0xca, 0x1c, 0x60, 0x90, 0x2e, 0x52 + , 0xf0, 0xa0, 0x89, 0xbc, 0x76, 0x89, 0x70, 0x40, 0xe0, 0x82, 0xf9, 0x37 + , 0x76, 0x38, 0x48, 0x64, 0x5e, 0x07, 0x05 + ] + + it "checks SecretBox.create matches expected output" $ do + -- once with secretBox + let c1 = SecretBox.create m nonce firstkey + + -- once again + let c2 = SecretBox.create m nonce firstkey + + let output = hexifier c1 <> "\n" <> hexifier c2 <> "\n" + contents <- liftIO $ readFile "test/Crypto/secretbox.exp" + output `shouldBe` contents + + -- Taken from https://github.com/jedisct1/libsodium/blob/master/test/default/secretbox2.c + describe "secretbox check 2 (secretbox2.exp)" $ do + let firstkeyFailable = dhSecret $ B.pack + [ 0x1b, 0x27, 0x55, 0x64, 0x73, 0xe9, 0x85 + , 0xd4, 0x62, 0xcd, 0x51, 0x19, 0x7a, 0x9a + , 0x46, 0xc7, 0x60, 0x09, 0x54, 0x9e, 0xac + , 0x64, 0x74, 0xf2, 0x06, 0xc4, 0xee, 0x08 + , 0x44, 0xf6, 0x83, 0x89 + ] + firstkey = fromJust $ maybeCryptoError firstkeyFailable + let nonce = B.pack + [ 0x69, 0x69, 0x6e, 0xe9, 0x55, 0xb6 + , 0x2b, 0x73, 0xcd, 0x62, 0xbd, 0xa8 + , 0x75, 0xfc, 0x73, 0xd6, 0x82, 0x19 + , 0xe0, 0x03, 0x6b, 0x7a, 0x0b, 0x37 + ] + let c = B.pack + [ 0xf3, 0xff, 0xc7, 0x70, 0x3f, 0x94, 0x00, 0xe5 + , 0x2a, 0x7d, 0xfb, 0x4b, 0x3d, 0x33, 0x05, 0xd9, 0x8e, 0x99, 0x3b, 0x9f + , 0x48, 0x68, 0x12, 0x73, 0xc2, 0x96, 0x50, 0xba, 0x32, 0xfc, 0x76, 0xce + , 0x48, 0x33, 0x2e, 0xa7, 0x16, 0x4d, 0x96, 0xa4, 0x47, 0x6f, 0xb8, 0xc5 + , 0x31, 0xa1, 0x18, 0x6a, 0xc0, 0xdf, 0xc1, 0x7c, 0x98, 0xdc, 0xe8, 0x7b + , 0x4d, 0xa7, 0xf0, 0x11, 0xec, 0x48, 0xc9, 0x72, 0x71, 0xd2, 0xc2, 0x0f + , 0x9b, 0x92, 0x8f, 0xe2, 0x27, 0x0d, 0x6f, 0xb8, 0x63, 0xd5, 0x17, 0x38 + , 0xb4, 0x8e, 0xee, 0xe3, 0x14, 0xa7, 0xcc, 0x8a, 0xb9, 0x32, 0x16, 0x45 + , 0x48, 0xe5, 0x26, 0xae, 0x90, 0x22, 0x43, 0x68, 0x51, 0x7a, 0xcf, 0xea + , 0xbd, 0x6b, 0xb3, 0x73, 0x2b, 0xc0, 0xe9, 0xda, 0x99, 0x83, 0x2b, 0x61 + , 0xca, 0x01, 0xb6, 0xde, 0x56, 0x24, 0x4a, 0x9e, 0x88, 0xd5, 0xf9, 0xb3 + , 0x79, 0x73, 0xf6, 0x22, 0xa4, 0x3d, 0x14, 0xa6, 0x59, 0x9b, 0x1f, 0x65 + , 0x4c, 0xb4, 0x5a, 0x74, 0xe3, 0x55, 0xa5 + ] + it "checks SecretBox.open matches expected output" $ do + -- once with secretBoxOpen + let m = fromJust $ SecretBox.open c nonce firstkey + + let output = hexifier m <> "\n" + contents <- liftIO $ readFile "test/Crypto/secretbox2.exp" + output `shouldBe` contents +
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}