Skip to content

Commit

Permalink
Password-based test vectors
Browse files Browse the repository at this point in the history
  • Loading branch information
adinapoli committed Dec 26, 2016
1 parent 577ffd7 commit 1f7c726
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 48 deletions.
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -10,8 +10,8 @@ encrypted file format by Rob Napier.
* V3 - [Spec](https://github.com/RNCryptor/RNCryptor-Spec/blob/master/RNCryptor-Spec-v3.md)

# TODO
- [X] HMAC Validation
- [ ] Test vectors testing
- [ ] Key-based test vectors
- [ ] Key-derivation test vectors
- [ ] Profiling & optimisations

# Contributors (Sorted by name)
Expand Down
9 changes: 6 additions & 3 deletions rncryptor.cabal
@@ -1,5 +1,5 @@
name: rncryptor
version: 0.2.0.0
version: 0.3.0.0
synopsis: Haskell implementation of the RNCryptor file format
description: Pure Haskell implementation of the RNCrytor spec.
license: MIT
Expand Down Expand Up @@ -45,8 +45,8 @@ test-suite rncryptor-tests
exitcode-stdio-1.0
main-is:
Main.hs
other-modules:
Tests
other-modules: Tests
PasswordBasedVectors
hs-source-dirs:
test
default-language:
Expand All @@ -60,6 +60,9 @@ test-suite rncryptor-tests
, tasty-quickcheck
, tasty-hunit
, io-streams
, base16-bytestring
, cryptonite
, text
, bytestring-arbitrary >= 0.1.0

executable rncryptor-decrypt
Expand Down
71 changes: 51 additions & 20 deletions src/Crypto/RNCryptor/Types.hs
Expand Up @@ -6,30 +6,40 @@ module Crypto.RNCryptor.Types
, RNCryptorContext(ctxHeader, ctxHMACCtx, ctxCipher)
, newRNCryptorContext
, newRNCryptorHeader
, newRNCryptorHeaderFrom
, renderRNCryptorHeader
, makeHMAC
, blockSize
-- * Type synonyms to make the API more descriptive
, Password
, HMAC
, Salt
, EncryptionKey
, EncryptionSalt
, HMACSalt
, IV
) where

import Control.Applicative
import Control.Exception (Exception)
import Control.Monad
import Crypto.Cipher.AES (AES256)
import Crypto.Cipher.Types (Cipher(..))
import Crypto.Error (CryptoFailable(..))
import Control.Exception (Exception)
import Crypto.Hash (Digest(..))
import Crypto.Hash.Algorithms (SHA1(..), SHA256(..))
import Crypto.Hash.IO (HashAlgorithm(..))
import Crypto.KDF.PBKDF2 (generate, prfHMAC, Parameters(..))
import Crypto.MAC.HMAC (HMAC(..), Context, initialize, hmac)
import Data.ByteArray (ByteArray, convert)
import Data.ByteString (cons, ByteString, unpack)
import Crypto.Cipher.AES (AES256)
import Crypto.Cipher.Types (Cipher(..))
import Crypto.Error (CryptoFailable(..))
import Crypto.Hash (Digest(..))
import Crypto.Hash.Algorithms (SHA1(..), SHA256(..))
import Crypto.Hash.IO (HashAlgorithm(..))
import Crypto.KDF.PBKDF2 (generate, prfHMAC, Parameters(..))
import Crypto.MAC.HMAC (Context, initialize, hmac)
import qualified Crypto.MAC.HMAC as Crypto
import Data.ByteArray (ByteArray, convert)
import Data.ByteString (cons, ByteString, unpack)
import qualified Data.ByteString.Char8 as C8
import Data.Monoid
import Data.Typeable
import Data.Word
import System.Random
import Test.QuickCheck (Arbitrary(..), vector)
import Test.QuickCheck (Arbitrary(..), vector)


data RNCryptorException =
Expand All @@ -39,22 +49,30 @@ data RNCryptorException =
deriving (Typeable, Eq)

instance Show RNCryptorException where
show (InvalidHMACException untrusted computed) = "InvalidHMACException: Untrusted HMAC was " <> show (unpack untrusted)
<> ", but the computed one is " <> show (unpack computed) <> "."
show (InvalidHMACException untrusted computed) =
"InvalidHMACException: Untrusted HMAC was " <> show (unpack untrusted)
<> ", but the computed one is " <> show (unpack computed) <> "."

instance Exception RNCryptorException

type Password = ByteString
type HMAC = ByteString
type EncryptionKey = ByteString
type Salt = ByteString
type EncryptionSalt = Salt
type HMACSalt = Salt
type IV = ByteString

data RNCryptorHeader = RNCryptorHeader {
rncVersion :: !Word8
-- ^ Data format version. Currently 3.
, rncOptions :: !Word8
-- ^ bit 0 - uses password
, rncEncryptionSalt :: !ByteString
, rncEncryptionSalt :: !EncryptionSalt
-- ^ iff option includes "uses password"
, rncHMACSalt :: !ByteString
, rncHMACSalt :: !HMACSalt
-- ^ iff options includes "uses password"
, rncIV :: !ByteString
, rncIV :: !IV
-- ^ The initialisation vector
-- The ciphertext is variable and encrypted in CBC mode
}
Expand Down Expand Up @@ -94,12 +112,12 @@ makeKey :: ByteString -> ByteString -> ByteString
makeKey = generate (prfHMAC SHA1) (Parameters 10000 32)

--------------------------------------------------------------------------------
makeHMAC :: ByteString -> ByteString -> ByteString -> ByteString
makeHMAC :: ByteString -> Password -> ByteString -> HMAC
makeHMAC hmacSalt userKey secret =
let key = makeKey userKey hmacSalt
hmacSha256 = hmac key secret
in
convert (hmacSha256 :: HMAC SHA256)
convert (hmacSha256 :: Crypto.HMAC SHA256)

--------------------------------------------------------------------------------
-- | Generates a new 'RNCryptorHeader', suitable for encryption.
Expand All @@ -118,6 +136,19 @@ newRNCryptorHeader = do
, rncIV = iv
}

--------------------------------------------------------------------------------
newRNCryptorHeaderFrom :: EncryptionSalt -> HMACSalt -> IV -> RNCryptorHeader
newRNCryptorHeaderFrom eSalt hmacSalt iv = do
let version = toEnum 3
let options = toEnum 1
RNCryptorHeader {
rncVersion = version
, rncOptions = options
, rncEncryptionSalt = eSalt
, rncHMACSalt = hmacSalt
, rncIV = iv
}

--------------------------------------------------------------------------------
-- | Concatenates this 'RNCryptorHeader' into a raw sequence of bytes, up to the
-- IV. This means you need to append the ciphertext plus the HMAC to finalise
Expand All @@ -142,7 +173,7 @@ cipherInitNoError k = case cipherInit k of
CryptoFailed e -> error ("cipherInitNoError: " <> show e)

--------------------------------------------------------------------------------
newRNCryptorContext :: ByteString -> RNCryptorHeader -> RNCryptorContext
newRNCryptorContext :: Password -> RNCryptorHeader -> RNCryptorContext
newRNCryptorContext userKey hdr =
let hmacSalt = rncHMACSalt hdr
hmacKey = makeKey userKey hmacSalt
Expand Down
33 changes: 23 additions & 10 deletions src/Crypto/RNCryptor/V3/Encrypt.hs
Expand Up @@ -3,6 +3,7 @@ module Crypto.RNCryptor.V3.Encrypt
( encrypt
, encryptBlock
, encryptStream
, encryptStreamWithContext
) where

import Crypto.Cipher.AES (AES256)
Expand All @@ -21,7 +22,7 @@ import qualified System.IO.Streams as S
encryptBytes :: AES256 -> ByteString -> ByteString -> ByteString
encryptBytes a iv = cbcEncrypt a iv'
where
iv' = fromMaybe (error "encryptBytes: makeIV failed.") $ makeIV iv
iv' = fromMaybe (error $ "encryptBytes: makeIV failed (iv was: " <> show (B.unpack iv) <> ")") $ makeIV iv

--------------------------------------------------------------------------------
-- | Encrypt a raw Bytestring block. The function returns the encrypt text block
Expand All @@ -31,7 +32,7 @@ encryptBytes a iv = cbcEncrypt a iv'
encryptBlock :: RNCryptorContext
-> ByteString
-> (RNCryptorContext, ByteString)
encryptBlock ctx clearText =
encryptBlock ctx clearText =
let cipherText = encryptBytes (ctxCipher ctx) (rncIV . ctxHeader $ ctx) clearText
!newHmacCtx = update (ctxHMACCtx ctx) cipherText
!sz = B.length clearText
Expand All @@ -53,7 +54,25 @@ encrypt ctx input =

--------------------------------------------------------------------------------
-- | Efficiently encrypt an incoming stream of bytes.
encryptStream :: ByteString
encryptStreamWithContext :: RNCryptorContext
-- ^ The RNCryptorContext
-> S.InputStream ByteString
-- ^ The input source (mostly likely stdin)
-> S.OutputStream ByteString
-- ^ The output source (mostly likely stdout)
-> IO ()
encryptStreamWithContext ctx inS outS = do
S.write (Just (renderRNCryptorHeader $ ctxHeader ctx)) outS
processStream ctx inS outS encryptBlock finaliseEncryption
where
finaliseEncryption lastBlock lastCtx = do
let (ctx', cipherText) = encryptBlock lastCtx (lastBlock <> pkcs7Padding blockSize (B.length lastBlock))
S.write (Just cipherText) outS
S.write (Just (convert $ finalize (ctxHMACCtx ctx'))) outS

--------------------------------------------------------------------------------
-- | Efficiently encrypt an incoming stream of bytes.
encryptStream :: Password
-- ^ The user key (e.g. password)
-> S.InputStream ByteString
-- ^ The input source (mostly likely stdin)
Expand All @@ -65,10 +84,4 @@ encryptStream userKey inS outS = do
let ctx = newRNCryptorContext userKey hdr
msgHdr = renderRNCryptorHeader hdr
ctx' = ctx { ctxHMACCtx = update (ctxHMACCtx ctx) msgHdr }
S.write (Just msgHdr) outS
processStream ctx' inS outS encryptBlock finaliseEncryption
where
finaliseEncryption lastBlock ctx = do
let (ctx', cipherText) = encryptBlock ctx (lastBlock <> pkcs7Padding blockSize (B.length lastBlock))
S.write (Just cipherText) outS
S.write (Just (convert $ finalize (ctxHMACCtx ctx'))) outS
encryptStreamWithContext ctx' inS outS
19 changes: 14 additions & 5 deletions test/Main.hs
@@ -1,10 +1,11 @@
{-# LANGUAGE OverloadedStrings #-}
module Main where

import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck
import Tests
import qualified PasswordBasedVectors as Pwd
import Test.Tasty
import Test.Tasty.HUnit
import Test.Tasty.QuickCheck
import Tests

----------------------------------------------------------------------
withQuickCheckDepth :: TestName -> Int -> [TestTree] -> TestTree
Expand All @@ -16,7 +17,15 @@ main :: IO ()
main = do
defaultMainWithIngredients defaultIngredients $
testGroup "RNCryptor tests" $ [
testCase "Swift-encrypted input can be decrypted" testForeignEncryption
testCase "Swift-encrypted input can be decrypted" testForeignEncryption
, testGroup "Password-Based Test Vectors" [
testCase "All fields empty or zero" Pwd.allEmptyOrZero
, testCase "One byte" Pwd.oneByte
, testCase "Exactly one block" Pwd.exactlyOneBlock
, testCase "More than one block" Pwd.moreThanOneBlock
, testCase "Multibyte password" Pwd.multibytePassword
, testCase "Longer text and password" Pwd.longerTextAndPassword
]
, withQuickCheckDepth "RNCryptor properties" 100 [
testProperty "encrypt/decrypt roundtrip" testEncryptDecryptRoundtrip
, testProperty "encrypt/decrypt streaming roundtrip" testStreamingEncryptDecryptRoundtrip
Expand Down
127 changes: 127 additions & 0 deletions test/PasswordBasedVectors.hs
@@ -0,0 +1,127 @@
{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module PasswordBasedVectors where

import Crypto.MAC.HMAC (update)
import Crypto.RNCryptor.Types
import Crypto.RNCryptor.V3.Encrypt
import Data.ByteString as B
import Data.ByteString.Base16
import Data.Monoid
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified System.IO.Streams.ByteString as S
import qualified System.IO.Streams.List as S
import Test.Tasty.HUnit

--------------------------------------------------------------------------------
data TestVector = TestVector {
password :: !Password
, enc_salt_hex :: !EncryptionSalt
, hmac_salt_hex :: !HMACSalt
, iv_hex :: !IV
, plaintext_hex :: !ByteString
, ciphertext_hex :: !ByteString
}

--------------------------------------------------------------------------------
unhex :: ByteString -> ByteString
unhex = fst . decode

--------------------------------------------------------------------------------
withTestVector :: TestVector -> Assertion
withTestVector TestVector{..} = do
let header = newRNCryptorHeaderFrom (unhex enc_salt_hex) (unhex hmac_salt_hex) (unhex iv_hex)
let ctx = newRNCryptorContext password header
encrypt ctx (unhex plaintext_hex) @?= (unhex ciphertext_hex)
-- Test the streaming API
inS <- S.fromByteString (unhex plaintext_hex)
(outS, flush) <- S.listOutputStream
let ctx' = ctx { ctxHMACCtx = update (ctxHMACCtx ctx) (renderRNCryptorHeader header) }
encryptStreamWithContext ctx' inS outS
result <- flush
(B.unpack $ B.concat result) @?= (B.unpack $ unhex ciphertext_hex)

--------------------------------------------------------------------------------
allEmptyOrZero :: Assertion
allEmptyOrZero = withTestVector $ TestVector {
password = "a"
, enc_salt_hex = "0000000000000000"
, hmac_salt_hex = "0000000000000000"
, iv_hex = "00000000000000000000000000000000"
, plaintext_hex = ""
, ciphertext_hex = "03010000000000000000000000000000000000000000000000000000000000000000b3039be31cd7ece5e754"
<> "f5c8da17003666313ae8a89ddcf8e3cb41fdc130b2329dbe07d6f4d32c34e050c8bd7e933b12"
}

--------------------------------------------------------------------------------
oneByte :: Assertion
oneByte = withTestVector $ TestVector {
password = "thepassword"
, enc_salt_hex = "0001020304050607"
, hmac_salt_hex = "0102030405060708"
, iv_hex = "02030405060708090a0b0c0d0e0f0001"
, plaintext_hex = "01"
, ciphertext_hex = "03010001020304050607010203040506070802030405060708090a0b0c0d0e0f0001a1f8"
<> "730e0bf480eb7b70f690abf21e029514164ad3c474a51b30c7eaa1ca545b7de3de5b010acbad0a9a13857df696a8"
}

--------------------------------------------------------------------------------
exactlyOneBlock :: Assertion
exactlyOneBlock = withTestVector $ TestVector {
password = "thepassword"
, enc_salt_hex = "0102030405060700"
, hmac_salt_hex = "0203040506070801"
, iv_hex = "030405060708090a0b0c0d0e0f000102"
, plaintext_hex = "0123456789abcdef"
, ciphertext_hex = "030101020304050607000203040506070801030405060708090a0b0c0d0e0f0001020e437"
<> "fe809309c03fd53a475131e9a1978b8eaef576f60adb8ce2320849ba32d742900438ba897d22210c76c35c849df"
}

--------------------------------------------------------------------------------
moreThanOneBlock :: Assertion
moreThanOneBlock = withTestVector $ TestVector {
password = "thepassword"
, enc_salt_hex = "0203040506070001"
, hmac_salt_hex = "0304050607080102"
, iv_hex = "0405060708090a0b0c0d0e0f00010203"
, plaintext_hex = "0123456789abcdef01234567"
, ciphertext_hex = "0301020304050607000103040506070801020405060708090a0b0c0d0e0f00010203e01bbda5df2ca8adace3"
<> "8f6c588d291e03f951b78d3417bc2816581dc6b767f1a2e57597512b18e1638f21235fa5928c"
}

--------------------------------------------------------------------------------
multibytePassword :: Assertion
multibytePassword = withTestVector $ TestVector {
password = T.encodeUtf8 (T.pack "中文密码")
, enc_salt_hex = "0304050607000102"
, hmac_salt_hex = "0405060708010203"
, iv_hex = "05060708090a0b0c0d0e0f0001020304"
, plaintext_hex = "23456789abcdef0123456701"
, ciphertext_hex = "03010304050607000102040506070801020305060708090a0b0c0d0e0f00010203048a9e08bdec1c4bfe13e8"
<> "1fb85f009ab3ddb91387e809c4ad86d9e8a6014557716657bd317d4bb6a7644615b3de402341"
}

--------------------------------------------------------------------------------
longerTextAndPassword :: Assertion
longerTextAndPassword = withTestVector $ TestVector {
password = "It was the best of times, it was the worst of times; it was the age of wisdom, it was the age of foolishness;"
, enc_salt_hex = "0405060700010203"
, hmac_salt_hex = "0506070801020304"
, iv_hex = "060708090a0b0c0d0e0f000102030405"
, plaintext_hex = "697420776173207468652065706f6368206f662062656c6965662c20697420776173207468652065706f6368206f6620696e6"
<> "3726564756c6974793b206974207761732074686520736561736f6e206f66204c696768742c20697420776173207468652073"
<> "6561736f6e206f66204461726b6e6573733b206974207761732074686520737072696e67206f6620686f70652c20697420776"
<> "173207468652077696e746572206f6620646573706169723b207765206861642065766572797468696e67206265666f726520"
<> "75732c20776520686164206e6f7468696e67206265666f72652075733b207765207765726520616c6c20676f696e672064697"
<> "26563746c7920746f2048656176656e2c207765207765726520616c6c20676f696e6720746865206f74686572207761792e0a0a"
, ciphertext_hex = "030104050607000102030506070801020304060708090a0b0c0d0e0f000102030405d564c7a99da921a6e7c4078a82641d9"
<> "5479551283167a2c81f31ab80c9d7d8beb770111decd3e3d29bbdf7ebbfc5f10ac87e7e55bfb5a7f487bcd39835705e83b9"
<> "c049c6d6952be011f8ddb1a14fc0c925738de017e62b1d621ccdb75f2937d0a1a70e44d843b9c61037dee2998b2bbd740b9"
<> "10232eea71961168838f6995b9964173b34c0bcd311a2c87e271630928bae301a8f4703ac2ae4699f3c285abf1c55ac324b"
<> "073a958ae52ee8c3bd68f919c09eb1cd28142a1996a9e6cbff5f4f4e1dba07d29ff66860db9895a48233140ca249419d630"
<> "46448db1b0f4252a6e4edb947fd0071d1e52bc15600622fa548a6773963618150797a8a80e592446df5926d0bfd32b544b7"
<> "96f3359567394f77e7b171b2f9bc5f2caf7a0fac0da7d04d6a86744d6e06d02fbe15d0f580a1d5bd16ad91348003611358d"
<> "cb4ac9990955f6cbbbfb185941d4b4b71ce7f9ba6efc1270b7808838b6c7b7ef17e8db919b34fac"
}

0 comments on commit 1f7c726

Please sign in to comment.