zerobin 1.2.0 → 1.5.0
raw patch · 5 files changed
+81/−42 lines, 5 files
Files
- cli/Main.hs +1/−5
- src/Web/ZeroBin.hs +45/−16
- src/Web/ZeroBin/SJCL.hs +23/−14
- src/Web/ZeroBin/Utils.hs +11/−6
- zerobin.cabal +1/−1
cli/Main.hs view
@@ -72,9 +72,5 @@ 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+ Just e -> share bin e cnt >>= putStrLn
src/Web/ZeroBin.hs view
@@ -1,13 +1,29 @@+{-|+High-level functions for posting to 0bin services like+<http://0bin.net> or <http://paste.ec>.++ >>> import Web.ZeroBin+ >>> import Data.ByteString.Char8+ >>> share "http://0bin.net" Day (pack "hello")+"http://0bin.net/paste/ZH6VyKXjDHAiPT8J#C6LLidGyHO7xt3xuDtsNHjZ77luualukEuJ25S6w/K1m"++-}++{-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} module Web.ZeroBin ( Expiration(..),+ ZeroBinError(..), share ) where +import Control.Exception (Exception)+import Control.Exception.Base (throwIO) import Data.ByteString (ByteString) import Data.ByteString.Base64 (encode) import Data.Maybe (fromJust)+import Data.Typeable (Typeable) import GHC.Generics (Generic) import Web.ZeroBin.SJCL (encrypt, Content) import Web.ZeroBin.Utils (makePassword)@@ -23,12 +39,22 @@ } deriving (Generic, Show) instance JSON.FromJSON Response +-- | 0bin error message+data ZeroBinError = ZeroBinError String+ deriving (Show, Typeable)+instance Exception ZeroBinError++-- | Expiration of a paste.+-- "Burn after reading" really means "burn after two readings",+-- because we do not redirect to the paste like a browser does.+-- You can verify your paste before sharing the link.+-- Original <http://0bin.net> does not support 'Week'. data Expiration- = Once - | Day- | Week- | Month- | Never+ = Once -- ^ burn after reading+ | Day -- ^ keep for 24 hours+ | Week -- ^ for 7 days+ | Month -- ^ for 30 days+ | Never -- ^ for 100 years form :: Expiration -> String form Once = "burn_after_reading"@@ -37,7 +63,7 @@ form Month = "1_month" form Never = "never" -post :: String -> Expiration -> Content -> IO (Either String String)+post :: String -> Expiration -> Content -> IO String post bin ex ct = do req' <- HTTP.parseUrl $ bin ++ "/paste/create" let req = HTTP.urlEncodedBody@@ -48,17 +74,20 @@ 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+ "ok" -> return $ bin ++ "/paste/" ++ (fromJust . paste) resp+ _ -> throwIO . ZeroBinError $ (fromJust . message) resp -share :: String -> Expiration -> ByteString -> IO (Either String String)++-- | Encrypts the plain data with a random password,+-- post to 0bin and return the URI of a new paste.+-- Can throw 'ZeroBinError' or 'Network.HTTP.Conduit.HttpException'.+share :: String -- ^ the address of 0bin, e. g. <http://0bin.net> or <https://paste.ec>+ -> Expiration+ -> ByteString -- ^ the plain data to encrypt and paste+ -> IO String -- ^ the URI of paste 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+ cnt <- encrypt pwd (encode txt)+ uri <- post bin ex cnt+ return $ uri ++ "#" ++ pwd
src/Web/ZeroBin/SJCL.hs view
@@ -1,3 +1,12 @@+{-|+Encryption compatible with <https://crypto.stanford.edu/sjcl/ SJCL>++ >>> import Web.ZeroBin.SJCL+ >>> import Data.ByteString.Char8+ >>> encrypt "secret-word" (pack "hello")+Content {iv = "VxyuJRVtKJqhG2iR/sPjAQ", salt = "AhnDuP1CkTCBlQTHgw", ct = "cqr7/pMRXrcROmcgwA"}+-}+ {-# LANGUAGE DeriveGeneric #-} module Web.ZeroBin.SJCL (@@ -23,10 +32,11 @@ import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as C +-- | Encrypted content. Each field is a 'toWeb'-encoded byte-string data Content = Content {- iv :: String- , salt :: String- , ct :: String+ iv :: String -- ^ random initialization vector (IV)+ , salt :: String -- ^ random salt+ , ct :: String -- ^ encrypted data } deriving (Generic, Show) -- FIXME: http://stackoverflow.com/questions/33045350/unexpected-haskell-aeson-warning-no-explicit-implementation-for-tojson@@ -36,16 +46,11 @@ 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+-- 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-+makeKey = PBKDF2.generate (prfHMAC SHA256)+ PBKDF2.Parameters {PBKDF2.iterCounts = 1000, PBKDF2.outputLength = 16} chunks :: Int -> ByteString -> [ByteString] chunks sz = split@@ -58,9 +63,13 @@ 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+-- | <https://crypto.stanford.edu/sjcl/ SJCL>-compatible encryption function.+-- Follows <https://tools.ietf.org/html/rfc3610 RFC3610> with a 8-bytes tag.+-- Uses 16-bytes cipher key generated from the password and a random 'salt'+-- by PBKDF2-HMAC-SHA256 with 1000 iterations.+encrypt :: String -- ^ the password+ -> ByteString -- ^ the plain data to encrypt+ -> IO Content encrypt password plaintext = do ivd <- getEntropy 16 -- XXX it is truncated to get the nonce below slt <- getEntropy 13 -- arbitrary length
src/Web/ZeroBin/Utils.hs view
@@ -1,3 +1,7 @@+{-|+Various utility functions+-}+ module Web.ZeroBin.Utils ( toWeb , makePassword@@ -7,13 +11,14 @@ import Data.ByteString (ByteString) import Data.ByteString.Base64 (encode) import Data.ByteString.Char8 (unpack)-import Data.Char (isAlphaNum) --toWeb :: ByteString -> String+-- | Encodes to base64 and drops padding '='.+toWeb :: ByteString -- ^ the data to encode+ -> String -- ^ base64 string without padding toWeb = takeWhile (/= '=') . unpack . encode -makePassword :: Int -> IO String-makePassword n = (map (\c -> if isAlphaNum c then c else 'X')- . toWeb) `fmap` getEntropy n+-- | Makes a random password+makePassword :: Int -- ^ the number of bytes of entropy+ -> IO String -- ^ random byte-string encoded by 'toWeb'+makePassword n = toWeb `fmap` getEntropy n
zerobin.cabal view
@@ -1,5 +1,5 @@ name: zerobin-version: 1.2.0+version: 1.5.0 synopsis: Post to 0bin services description: Post encrypted content to 0bin sites like http://0bin.net or https://paste.ec