aboutsummaryrefslogtreecommitdiff
path: root/src/Web/ZeroBin/SJCL.hs
blob: c4bc86e09aeff05bd9adc21a51ea233941b03148 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
{-|
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
  ( Content(..)
  , encrypt
  ) where

import Crypto.Cipher.AES (AES128)
import Crypto.Cipher.Types
       (blockSize, cipherInit, ctrCombine, ecbEncrypt, ivAdd, makeIV)
import Crypto.Error (throwCryptoErrorIO)
import Crypto.Hash.Algorithms (SHA256(..))
import Crypto.KDF.PBKDF2 (prfHMAC)
import qualified Crypto.KDF.PBKDF2 as PBKDF2
import Crypto.Number.Serialize (i2ospOf_)
import Crypto.Random.Entropy (getEntropy)
import qualified Data.Aeson as JSON
import qualified Data.ByteArray as BA
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as C
import Data.Maybe (fromJust)
import Data.Word (Word8)
import GHC.Generics (Generic)
import Web.ZeroBin.Utils (toWeb)

-- | Encrypted content. Each field is a 'toWeb'-encoded byte-string
data Content = Content
  { 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
instance JSON.ToJSON Content where
  toJSON = JSON.genericToJSON JSON.defaultOptions

makeCipher :: ByteString -> IO AES128
makeCipher = throwCryptoErrorIO . cipherInit

-- 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 =
  PBKDF2.generate
    (prfHMAC SHA256)
    PBKDF2.Parameters {PBKDF2.iterCounts = 1000, PBKDF2.outputLength = 16}

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

-- | <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
  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}