packages feed

rncryptor (empty) → 0.0.1.0

raw patch · 8 files changed

+376/−0 lines, 8 filesdep +QuickCheckdep +basedep +base64-bytestringsetup-changed

Dependencies added: QuickCheck, base, base64-bytestring, bytestring, cipher-aes, io-streams, mtl, pbkdf, rncryptor, tasty, tasty-hunit, tasty-quickcheck

Files

+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2014 Alfredo Di Napoli++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.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ example/StreamingDecrypter.hs view
@@ -0,0 +1,14 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import Crypto.RNCryptor.V3+import qualified System.IO.Streams as S+import System.Environment+import qualified Data.ByteString.Char8 as B++main :: IO ()+main = do+  args <- getArgs+  case args of+    key:_ -> decryptStream (B.pack key) S.stdin S.stdout+    _ -> putStrLn "usage: rncryptor-stream <key>"
+ rncryptor.cabal view
@@ -0,0 +1,74 @@+name:                rncryptor+version:             0.0.1.0+synopsis:            Haskell implementation of the RNCryptor file format+description:         Pure Haskell implementation of the RNCrytor spec.+license:             MIT+license-file:        LICENSE+author:              Alfredo Di Napoli+maintainer:          alfredo.dinapoli@gmail.com+category:            Network+build-type:          Simple+tested-with:         GHC == 7.6, GHC == 7.8+cabal-version:       >=1.10++source-repository head+  type:     git+  location: https://github.com/adinapoli/rncryptor-hs++library+  exposed-modules:+    Crypto.RNCryptor.V3+    Crypto.RNCryptor.Types+  other-modules:+  build-depends:+      base >=4.6 && < 5+    , bytestring >= 0.9.0+    , mtl >= 2.1+    , base64-bytestring >= 1.0.0.1+    , QuickCheck >= 2.6 && < 2.8+    , io-streams >= 1.2.0.0+    , cipher-aes >= 0.2.0+    , pbkdf >= 1.1.1.1+  hs-source-dirs:+    src+  default-language:+    Haskell2010+  ghc-options:+    -funbox-strict-fields++test-suite rncryptor-tests+  type:+    exitcode-stdio-1.0+  main-is:+    Main.hs+  other-modules:+    Tests+  hs-source-dirs:+    test+  default-language:+    Haskell2010+  build-depends:+      rncryptor -any+    , base+    , bytestring+    , QuickCheck+    , tasty >= 0.9.0.1+    , tasty-quickcheck+    , tasty-hunit++executable rncryptor-stream+  build-depends:+      base+    , bytestring+    , io-streams+    , base64-bytestring+    , cipher-aes+    , rncryptor -any+  hs-source-dirs:+    example+  main-is:+    StreamingDecrypter.hs+  default-language:+    Haskell2010+  ghc-options:+    -funbox-strict-fields
+ src/Crypto/RNCryptor/Types.hs view
@@ -0,0 +1,45 @@++module Crypto.RNCryptor.Types +     ( RNCryptorHeader(..)+     , RNCryptorContext(ctxHeader, ctxCipher)+     , newRNCryptorContext+     ) where++import Data.ByteString (ByteString)+import Data.Word+import Crypto.Cipher.AES+import Crypto.PBKDF.ByteString+++data RNCryptorHeader = RNCryptorHeader {+        rncVersion :: !Word8+      -- ^ Data format version. Currently 3.+      , rncOptions :: !Word8+      -- ^ bit 0 - uses password+      , rncEncryptionSalt :: !ByteString+      -- ^ iff option includes "uses password"+      , rncHMACSalt :: !ByteString+      -- ^ iff options includes "uses password"+      , rncIV :: !AESIV+      -- ^ The initialisation vector+      -- The ciphertext is variable and encrypted in CBC mode+      , rncHMAC :: (ByteString -> ByteString)+      -- ^ The HMAC (32 bytes). This field is a continuation+      -- as the HMAC is at the end of the file.+      }+++--------------------------------------------------------------------------------+-- A convenient datatype to avoid carrying around the AES cypher,+-- the encrypted key and so on and so forth.+data RNCryptorContext = RNCryptorContext {+        ctxHeader :: RNCryptorHeader+      , ctxCipher :: AES+      }++--------------------------------------------------------------------------------+newRNCryptorContext :: ByteString -> RNCryptorHeader -> RNCryptorContext+newRNCryptorContext userKey hdr =+  let eKey = sha1PBKDF2 userKey (rncEncryptionSalt hdr) 10000 32+      cipher = initAES eKey+  in RNCryptorContext hdr cipher
+ src/Crypto/RNCryptor/V3.hs view
@@ -0,0 +1,196 @@+{-# LANGUAGE BangPatterns #-}+module Crypto.RNCryptor.V3+  ( pkcs7Padding+  , parseHeader+  , decrypt+  , decryptBlock+  , decryptStream+  ) where++import           Data.ByteString (ByteString)+import qualified Data.ByteString as B+import           Data.Word+import           Control.Monad.State+import           Crypto.RNCryptor.Types+import           Crypto.Cipher.AES+import           Data.Monoid+import qualified System.IO.Streams as S+++--------------------------------------------------------------------------------+-- | Computes the padding as per PKCS#7. The specification can be found +-- here: http://tools.ietf.org/html/rfc5652#section-6.3+pkcs7Padding :: Int+             -- ^ The block size (e.g. 16 bytes)+             -> Int+             -- ^ The input size+             -> ByteString+             -- ^ The resulting padding+pkcs7Padding k l =+  let octetsSize = k - (l `mod` k)+  in  B.pack $ replicate octetsSize (fromInteger . toInteger $ octetsSize)++--------------------------------------------------------------------------------+-- | Parse the input 'ByteString' to extract the 'RNCryptorHeader', as +-- defined in the V3 spec. The incoming 'ByteString' is expected to have+-- at least 34 bytes available. As the HMAC can be found only at the very+-- end of an encrypted file, 'RNCryptorHeader' provides by default a function+-- to parse the HMAC, callable at the right time during streaming/parsing.+parseHeader :: ByteString -> RNCryptorHeader+parseHeader input = flip evalState input $ do+  v <- parseVersion+  o <- parseOptions+  eSalt <- parseEncryptionSalt+  hmacSalt <- parseHMACSalt+  iv <- parseIV+  return RNCryptorHeader {+      rncVersion = v+    , rncOptions = o+    , rncEncryptionSalt = eSalt+    , rncHMACSalt = hmacSalt+    , rncIV = iv+    , rncHMAC = parseHMAC+    }++--------------------------------------------------------------------------------+parseSingleWord8 :: String -> State ByteString Word8+parseSingleWord8 err = do+  bs <- get+  let (v,vs) = B.splitAt 1 bs+  put vs+  case B.unpack v of+    x:[] -> return x+    _ -> fail err++--------------------------------------------------------------------------------+parseBSOfSize :: Int -> String -> State ByteString ByteString+parseBSOfSize sz err = do+  bs <- get+  let (v,vs) = B.splitAt sz bs+  put vs+  case B.unpack v of+    [] -> fail err+    _ -> return v++--------------------------------------------------------------------------------+parseVersion :: State ByteString Word8+parseVersion = parseSingleWord8 "parseVersion: not enough bytes."++--------------------------------------------------------------------------------+parseOptions :: State ByteString Word8+parseOptions = parseSingleWord8 "parseOptions: not enough bytes."++--------------------------------------------------------------------------------+parseEncryptionSalt :: State ByteString ByteString+parseEncryptionSalt = parseBSOfSize 8 "parseEncryptionSalt: not enough bytes."++--------------------------------------------------------------------------------+parseHMACSalt :: State ByteString ByteString+parseHMACSalt = parseBSOfSize 8 "parseHMACSalt: not enough bytes."++--------------------------------------------------------------------------------+parseIV :: State ByteString AESIV+parseIV = fmap aesIV_ (parseBSOfSize 16 "parseIV: not enough bytes.")++--------------------------------------------------------------------------------+parseHMAC :: ByteString -> ByteString+parseHMAC leftover = flip evalState leftover $ parseBSOfSize 32 "parseHMAC: not enough bytes."++--------------------------------------------------------------------------------+blockSize :: Int+blockSize = 16++--------------------------------------------------------------------------------+-- | This was taken directly from the Python implementation, see "post_decrypt_data",+-- even though it doesn't seem to be a usual PKCS#7 removal:+-- data = data[:-bord(data[-1])]+-- https://github.com/RNCryptor/RNCryptor-python/blob/master/RNCryptor.py#L69+removePaddingSymbols :: ByteString -> ByteString+removePaddingSymbols input = +  let lastWord = B.last input+  in B.take (B.length input - fromEnum lastWord) input++--------------------------------------------------------------------------------+-- | Decrypt a raw Bytestring block. The function returns the clear text block+-- plus a new 'RNCryptorContext', which is needed because the IV needs to be+-- set to the last 16 bytes of the previous cipher text. (Thanks to Rob Napier+-- for the insight).+decryptBlock :: RNCryptorContext+             -> ByteString+             -> (RNCryptorContext, ByteString)+decryptBlock ctx cipherText = +  let clearText  = decryptCBC (ctxCipher ctx) (rncIV . ctxHeader $ ctx) cipherText+      !sz        = B.length cipherText+      !newHeader = (ctxHeader ctx) { rncIV = aesIV_ (B.drop (sz - 16) cipherText) }+      in (ctx { ctxHeader = newHeader }, clearText)++--------------------------------------------------------------------------------+-- | Decrypt an encrypted message. Please be aware that this is a user-friendly+-- but dangerous function, in the sense that it will load the *ENTIRE* input in+-- memory. It's mostly suitable for small inputs like passwords. For large+-- inputs, where size exceeds the available memory, please use 'decryptStream'.+decrypt :: ByteString -> ByteString -> ByteString+decrypt input pwd =+  let (rawHdr, rest) = B.splitAt 34 input+      -- remove the hmac at the end of the file+      (toDecrypt, _) = B.splitAt (B.length rest - 32) rest+      hdr = parseHeader rawHdr+      ctx = newRNCryptorContext pwd hdr+      clearText = decryptCBC (ctxCipher ctx) (rncIV . ctxHeader $ ctx) toDecrypt+  in  removePaddingSymbols clearText+++--------------------------------------------------------------------------------+-- | The 'DecryptionState' the streamer can be at. This is needed to drive the+-- computation as well as reading leftovers unread back in case we need to+-- chop the buffer read, if not multiple of the 'blockSize'.+data DecryptionState =+    Continue+  | FetchLeftOver !Int+  | DrainSource deriving (Show, Eq)++--------------------------------------------------------------------------------+decryptStream :: ByteString+              -> S.InputStream ByteString+              -> S.OutputStream ByteString+              -> IO ()+decryptStream userKey inS outS = do+  rawHdr <- S.readExactly 34 inS+  let hdr = parseHeader rawHdr+  let ctx = newRNCryptorContext userKey hdr+  go Continue mempty ctx+  where+    slack input = let bsL = B.length input in (bsL, bsL `mod` blockSize)++    go :: DecryptionState -> ByteString -> RNCryptorContext -> IO ()+    go dc !iBuffer ctx = do+      nextChunk <- case dc of+        FetchLeftOver size -> do+          lo <- S.readExactly size inS+          p  <- S.read inS+          return $ fmap (mappend lo) p+        _ -> S.read inS+      case nextChunk of+        Nothing -> finaliseDecryption iBuffer ctx+        (Just v) -> do+          let (sz, sl) = slack v+          case dc of+            DrainSource -> go DrainSource (iBuffer <> v) ctx+            _ -> do+              whatsNext <- S.peek inS+              case whatsNext of+                Nothing -> finaliseDecryption (iBuffer <> v) ctx+                Just nt ->+                  case sz + B.length nt < 4096 of+                    True  -> go DrainSource (iBuffer <> v) ctx+                    False -> do+                      -- If I'm here, it means I can safely decrypt this chunk+                      let (toDecrypt, rest) = B.splitAt (sz - sl) v+                      let (newCtx, clearT) = decryptBlock ctx toDecrypt+                      S.write (Just $ clearT) outS+                      S.unRead rest inS+                      go (FetchLeftOver sl) iBuffer newCtx++    finaliseDecryption lastBlock ctx = do+      let (rest, _) = B.splitAt (B.length lastBlock - 32) lastBlock --strip the hmac+      S.write (Just $ removePaddingSymbols (snd $ decryptBlock ctx rest)) outS
+ test/Main.hs view
@@ -0,0 +1,22 @@+{-# LANGUAGE OverloadedStrings #-}+module Main where++import           System.Environment+import           Data.Monoid+import           Tests+import           Test.Tasty+import           Test.Tasty.HUnit+import           Test.Tasty.QuickCheck++----------------------------------------------------------------------+withQuickCheckDepth :: TestName -> Int -> [TestTree] -> TestTree+withQuickCheckDepth tn depth tests =+  localOption (QuickCheckTests depth) (testGroup tn tests)++----------------------------------------------------------------------+main :: IO ()+main = do+  defaultMainWithIngredients defaultIngredients $+    testGroup "RNCryptor tests" $ [+         testGroup "RNCryptor properties" []+     ]
+ test/Tests.hs view
@@ -0,0 +1,3 @@+module Tests where++import Test.Tasty.HUnit