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 +20/−0
- Setup.hs +2/−0
- example/StreamingDecrypter.hs +14/−0
- rncryptor.cabal +74/−0
- src/Crypto/RNCryptor/Types.hs +45/−0
- src/Crypto/RNCryptor/V3.hs +196/−0
- test/Main.hs +22/−0
- test/Tests.hs +3/−0
+ 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