zerobin (empty) → 1.1.1
raw patch · 9 files changed
+460/−0 lines, 9 filesdep +aesondep +basedep +base64-bytestringsetup-changed
Dependencies added: aeson, base, base64-bytestring, bytestring, cryptonite, docopt, http-conduit, memory, process, raw-strings-qq, zerobin
Files
- LICENSE +20/−0
- README.md +77/−0
- Setup.hs +2/−0
- cli/Main.hs +80/−0
- nodejs/Main.hs +32/−0
- src/ZeroBin.hs +64/−0
- src/ZeroBin/SJCL.hs +91/−0
- src/ZeroBin/Utils.hs +19/−0
- zerobin.cabal +75/−0
+ LICENSE view
@@ -0,0 +1,20 @@+Copyright (c) 2015, Zalora South East Asia Pte. Ltd++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.
+ README.md view
@@ -0,0 +1,77 @@+About+=====+This is a library and a command-line utility+to share secrets via ["zerobin"](https://github.com/sametmax/0bin)+sites like https://paste.ec+using client-side encryption with [SJCL](https://crypto.stanford.edu/sjcl/).++This library reimplements encryption part of [SJCL](https://crypto.stanford.edu/sjcl/)+allowing you to post secrets from Haskell programs and shell script.++Requirements+============+ZeroBin is written in Haskell with [GHC](http://www.haskell.org/ghc/).+All required Haskell libraries are listed in [zerobin.cabal](zerobin.cabal).+Use [cabal-install](http://www.haskell.org/haskellwiki/Cabal-Install)+to fetch and build all pre-requisites automatically.++Installation+============+ $ git clone https://github.com/zalora/zerobin.git+ $ cd zerobin+ $ cabal install++Command-line utility+====================+The command-line utility `zerobin` encrypts text or file,+post the encrypted data to https://paste.ec and+prints URI to be shared or error message:++ $ zerobin 'heinrich hertz'+ https://paste.ec/paste/1j3GBy-7#dg0PXHFglISOhXzRnU4KLWbSAh5jX5KjX4wZEiYM8QA6+++Type `zerobin --help` to see usage summary:++ Usage:+ zerobin [options] TEXT++ Options:+ -b, --bin=BIN 0bin service [default: https://paste.ec]+ -f, --file Paste the content of file TEXT instead of plain TEXT+ -e, --expire=E Set expiration of paste: once, day, week, month [default: day]++ -h, --help Show this message++ Examples:+ zerobin hello paste "hello" for a day+ zerobin -f /etc/fstab paste file /etc/fstab for a day+ zerobin -e once hello paste "hello", it will burn after reading+ zerobin -b http://0bin.net hello paste to 0bin.net+++Hacking+=======+There is a simple test program in the [./nodejs](./nodejs) directory.+It uses this library to encrypt a message and original SJCL+running by [Node.js](https://nodejs.org) to decrypt:++ $ git clone https://github.com/zalora/zerobin.git+ $ cd zerobin+ $ cabal install --dependencies-only+ $ cabal install -f nodejs --ghc-option="-Werror"+ $ # get nodejs and npm, e. g. on Debian; sudo apt-get install nodejs npm+ $ npm install sjcl+ $ ./dist/build/zerobin-nodejs/zerobin-nodejs+ heinrich hertz++Features/Bugs/TODOs+===================+1. [0bin](https://github.com/sametmax/0bin) supports images,+ `zerobin` can encrypt anything, but only plain text will be decrypted.+2. "Burn after reading" (`-e once`) really means "burn after two readings",+ because we do not redirect like browser does.+ You can verify your paste before sharing the link ;-)+3. http://0bin.net does not support `-e week`++
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ cli/Main.hs view
@@ -0,0 +1,80 @@+{-# LANGUAGE QuasiQuotes #-}++module Main where++import Data.Version (showVersion)+import Paths_zerobin (version) -- from cabal+import System.Environment (getArgs)+import System.Exit (exitFailure)+import System.IO (stderr, hPutStrLn)+import Text.RawString.QQ (r)+import ZeroBin (share, Expiration(..))+import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as C+import qualified System.Console.Docopt.NoTH as O++usage :: String+usage = "zerobin " ++ showVersion version+ ++ " pastes to 0bin services" ++ [r|+zerobin prints URI to be shared or error message+See http://0bin.net and https://paste.ec++Usage:+ zerobin [options] TEXT++Options:+ -b, --bin=BIN 0bin service [default: https://paste.ec]+ -f, --file Paste the content of file TEXT instead of plain TEXT+ -e, --expire=E Set expiration of paste: once, day, week, month [default: day]++ -h, --help Show this message++Examples:+ zerobin hello paste "hello" for a day+ zerobin -f /etc/fstab paste file /etc/fstab for a day+ zerobin -e once hello paste "hello", it will burn after reading+ zerobin -b http://0bin.net hello paste to 0bin.net+|]+++getExpiration :: String -> Maybe Expiration+getExpiration e =+ case e of+ "once" -> Just Once+ "day" -> Just Day+ "week" -> Just Week+ "month" -> Just Month+ _ -> Nothing++die :: String -> IO ()+die msg = do+ hPutStrLn stderr $ "zerobin: " ++ msg+ exitFailure++getContent :: Bool -> String -> IO BS.ByteString+getContent asFile text =+ if not asFile+ then return $ C.pack text+ else BS.readFile text+++main :: IO ()+main = do+ doco <- O.parseUsageOrExit usage+ args <- O.parseArgsOrExit doco =<< getArgs+ if args `O.isPresent` O.longOption "help"+ then putStrLn $ O.usage doco+ else do+ let get = O.getArgOrExitWith doco+ bin <- args `get` O.longOption "bin"+ expire <- args `get` O.longOption "expire"+ text <- args `get` O.argument "TEXT"+ cnt <- getContent (args `O.isPresent` O.longOption "file") text+ case getExpiration expire of+ Nothing -> die "invalid value for expiration"+ Just e -> do+ rc <- share bin e cnt+ case rc of+ Left err -> die err+ Right uri -> putStrLn uri+
+ nodejs/Main.hs view
@@ -0,0 +1,32 @@+{-# LANGUAGE OverloadedStrings #-}++module Main where++import System.Environment (getArgs)+import System.Process (callProcess)+import ZeroBin.SJCL (encrypt)+import ZeroBin.Utils (makePassword)+import qualified Data.Aeson as JSON+import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as C+import qualified Data.ByteString.Lazy as L++-- nodejs is a Debian's thing, others may have simple "node"++getText :: IO BS.ByteString+getText = do+ args <- map C.pack `fmap` getArgs+ if null args+ then return "heinrich hertz"+ else return . BS.intercalate " " $ args++main :: IO ()+main = do+ password <- makePassword 32+ text <- getText+ cont <- encrypt password text+ callProcess "nodejs" [ "nodejs/decrypt.js"+ , password+ , C.unpack . L.toStrict $ JSON.encode cont+ ]+
+ src/ZeroBin.hs view
@@ -0,0 +1,64 @@+{-# LANGUAGE DeriveGeneric #-}++module ZeroBin (+ Expiration(..),+ share+) where++import Data.ByteString (ByteString)+import Data.ByteString.Base64 (encode)+import Data.Maybe (fromJust)+import GHC.Generics (Generic)+import ZeroBin.SJCL (encrypt, Content)+import ZeroBin.Utils (makePassword)+import qualified Data.Aeson as JSON+import qualified Data.ByteString.Char8 as C+import qualified Data.ByteString.Lazy as L+import qualified Network.HTTP.Conduit as HTTP++data Response = Response {+ status :: String+ , message :: Maybe String+ , paste :: Maybe String+ } deriving (Generic, Show)+instance JSON.FromJSON Response++data Expiration+ = Once + | Day+ | Week+ | Month+ | Never++form :: Expiration -> String+form Once = "burn_after_reading"+form Day = "1_day"+form Week = "1_week"+form Month = "1_month"+form Never = "never"++post :: String -> Expiration -> Content -> IO (Either String String)+post bin ex ct = do+ req' <- HTTP.parseUrl $ bin ++ "/paste/create"+ let req = HTTP.urlEncodedBody+ [ (C.pack "expiration" , C.pack $ form ex)+ , (C.pack "content" , L.toStrict $ JSON.encode ct)+ ] req'+ manager <- HTTP.newManager HTTP.tlsManagerSettings+ response <- HTTP.httpLbs req manager+ let resp = fromJust . JSON.decode $ HTTP.responseBody response+ case status resp of+ "ok" -> return . Right $+ bin ++ "/paste/" ++ (fromJust . paste) resp+ _ -> return . Left $+ (fromJust . message) resp++share :: String -> Expiration -> ByteString -> IO (Either String String)+share bin ex txt = do+ pwd <- makePassword 33+ c <- encrypt pwd (encode txt)+ append pwd `fmap` post bin ex c+ where+ append _ (Left e) = Left e+ append p (Right u) = Right $ u ++ "#" ++ p+
+ src/ZeroBin/SJCL.hs view
@@ -0,0 +1,91 @@+{-# LANGUAGE DeriveGeneric #-}++module ZeroBin.SJCL (+ Content(..),+ encrypt+) where++import Crypto.Cipher.AES (AES256)+import Crypto.Cipher.Types (ivAdd, blockSize, cipherInit, ecbEncrypt, ctrCombine, makeIV)+import Crypto.Error (throwCryptoErrorIO)+import Crypto.Hash.Algorithms (SHA256(..))+import Crypto.KDF.PBKDF2 (prfHMAC)+import Crypto.Number.Serialize (i2ospOf_)+import Crypto.Random.Entropy (getEntropy)+import Data.ByteString (ByteString)+import Data.Maybe (fromJust)+import Data.Word (Word8)+import GHC.Generics (Generic)+import ZeroBin.Utils (toWeb)+import qualified Crypto.KDF.PBKDF2 as PBKDF2+import qualified Data.Aeson as JSON+import qualified Data.ByteArray as BA+import qualified Data.ByteString as BS+import qualified Data.ByteString.Char8 as C++data Content = Content {+ iv :: String+ , salt :: String+ , ct :: String+ } deriving (Generic, Show)++-- FIXME: http://stackoverflow.com/questions/33045350/unexpected-haskell-aeson-warning-no-explicit-implementation-for-tojson+instance JSON.ToJSON Content where+ toJSON = JSON.genericToJSON JSON.defaultOptions++makeCipher :: ByteString -> IO AES256+makeCipher = throwCryptoErrorIO . cipherInit++-- SJCL uses PBKDF2-HMAC-SHA256 with 1000 iterations, 32 bytes length,+-- but the output is truncated down to 16 bytes.+-- https://github.com/bitwiseshiftleft/sjcl/blob/master/core/pbkdf2.js+-- TODO: this is default, we can specify it explicitly+-- for forward compatibility+makeKey :: ByteString -> ByteString -> ByteString+makeKey pwd slt = BS.take 16 $ PBKDF2.generate (prfHMAC SHA256)+ PBKDF2.Parameters {PBKDF2.iterCounts = 1000, PBKDF2.outputLength = 32}+ pwd slt+++chunks :: Int -> ByteString -> [ByteString]+chunks sz = split+ where split b | cl <= sz = [b'] -- padded+ | otherwise = b1 : split b2+ where cl = BS.length b+ (b1, b2) = BS.splitAt sz b+ b' = BS.take sz $ BS.append b (BS.replicate sz 0)++lengthOf :: Int -> Word8+lengthOf = ceiling . (logBase 256 :: Float -> Float) . fromIntegral++-- Ref. https://tools.ietf.org/html/rfc3610+-- SJCL uses 64-bit tag (8 bytes)+encrypt :: String -> ByteString -> IO Content+encrypt password plaintext = do+ ivd <- getEntropy 16 -- XXX it is truncated to get the nonce below+ slt <- getEntropy 13 -- arbitrary length+ cipher <- makeCipher $ makeKey (C.pack password) slt+ let tlen = 8 :: Word8+ l = BS.length plaintext+ eL = max 2 (lengthOf l)+ nonce = BS.take (15 - fromIntegral eL) ivd+ b0 = BS.concat [+ BS.pack [8*((tlen-2) `div` 2) + (eL-1)],+ nonce,+ i2ospOf_ (fromIntegral eL) (fromIntegral l)+ ]+ mac = foldl (\ a b -> ecbEncrypt cipher $ BA.xor a b)+ (ecbEncrypt cipher b0)+ (chunks (blockSize cipher) plaintext)+ tag = BS.take (fromIntegral tlen) mac+ a0 = BS.concat [+ BS.pack [eL - 1],+ nonce,+ BS.replicate (fromIntegral eL) 0+ ]+ a1iv = ivAdd (fromJust . makeIV $ a0) 1+ ciphtext = BS.append+ (ctrCombine cipher a1iv plaintext)+ (BA.xor (ecbEncrypt cipher a0) tag)+ return Content { iv = toWeb ivd, salt = toWeb slt, ct = toWeb ciphtext }+
+ src/ZeroBin/Utils.hs view
@@ -0,0 +1,19 @@+module ZeroBin.Utils (+ toWeb+, makePassword+) where++import Crypto.Random.Entropy (getEntropy)+import Data.ByteString (ByteString)+import Data.ByteString.Base64 (encode)+import Data.ByteString.Char8 (unpack)+import Data.Char (isAlphaNum)+++toWeb :: ByteString -> String+toWeb = takeWhile (/= '=') . unpack . encode++makePassword :: Int -> IO String+makePassword n = (map (\c -> if isAlphaNum c then c else 'X')+ . toWeb) `fmap` getEntropy n+
+ zerobin.cabal view
@@ -0,0 +1,75 @@+name: zerobin+version: 1.1.1+synopsis: Post to 0bin services+description: Post encrypted content to 0bin sites+ like http://0bin.net or https://paste.ec+license: MIT+license-file: LICENSE+author: Igor Pashev+maintainer: Igor Pashev <pashev.igor@gmail.com>+copyright: 2015, Zalora South East Asia Pte. Ltd+category: Cryptography, Web+build-type: Simple+extra-source-files: README.md+cabal-version: >= 1.20++source-repository head+ type: git+ location: https://github.com/zalora/zerobin.git++flag nodejs+ description: Build a test program for decrypting with Node.js and SJCL.+ You need Node.js and SJCL installed (via NPM for example)+ default: False++flag cli+ description: Build a command-line utility. You can use it in shell scripts+ default: True++library+ default-language: Haskell2010+ ghc-options: -Wall+ hs-source-dirs: src+ build-depends:+ aeson >= 0.10,+ base >= 4.7 && < 5,+ base64-bytestring >= 1.0,+ bytestring >= 0.10.6.0,+ cryptonite >= 0.8,+ http-conduit >= 2.1.8,+ memory >= 0.10+ exposed-modules:+ ZeroBin,+ ZeroBin.SJCL,+ ZeroBin.Utils++executable zerobin+ default-language: Haskell2010+ ghc-options: -Wall -static+ hs-source-dirs: cli+ main-is: Main.hs+ if flag(cli)+ build-depends:+ base >= 4.7 && < 5,+ bytestring >= 0.10.6.0,+ docopt >= 0.7.0.4,+ zerobin,+ raw-strings-qq >= 1.0.2+ else+ buildable: False++executable zerobin-nodejs+ default-language: Haskell2010+ ghc-options: -Wall+ hs-source-dirs: nodejs+ main-is: Main.hs+ if flag(nodejs)+ build-depends:+ aeson >= 0.10,+ base >= 4.7 && < 5,+ bytestring >= 0.10.6.0,+ zerobin,+ process >= 1.3.0.0+ else+ buildable: False+