diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/example/StreamingDecrypter.hs b/example/StreamingDecrypter.hs
new file mode 100644
--- /dev/null
+++ b/example/StreamingDecrypter.hs
@@ -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>"
diff --git a/rncryptor.cabal b/rncryptor.cabal
new file mode 100644
--- /dev/null
+++ b/rncryptor.cabal
@@ -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
diff --git a/src/Crypto/RNCryptor/Types.hs b/src/Crypto/RNCryptor/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/RNCryptor/Types.hs
@@ -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
diff --git a/src/Crypto/RNCryptor/V3.hs b/src/Crypto/RNCryptor/V3.hs
new file mode 100644
--- /dev/null
+++ b/src/Crypto/RNCryptor/V3.hs
@@ -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
diff --git a/test/Main.hs b/test/Main.hs
new file mode 100644
--- /dev/null
+++ b/test/Main.hs
@@ -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" []
+     ]
diff --git a/test/Tests.hs b/test/Tests.hs
new file mode 100644
--- /dev/null
+++ b/test/Tests.hs
@@ -0,0 +1,3 @@
+module Tests where
+
+import Test.Tasty.HUnit
