packages feed

haste-lib-0.6.0.0: src/Haste/Crypto.hs

{-# LANGUAGE OverloadedStrings, GeneralizedNewtypeDeriving #-}
{-# LANGUAGE FlexibleInstances, UndecidableInstances #-}
module Haste.Crypto
  ( MonadConc (..), MonadIO (..), Binary (..), ArrView (..)
  , Word8, Word16, Word32, Word64
    -- * Crypto-strength random number generation
  , CryptoRandom (..)
  , cryptoRandomSalt
  , cryptoRandomUArray
  , cryptoRandomIOUArray

    -- * SubtleCrypto API. Presently only supports symmetric encryption, due to
    --   lack of browser support for other components.
  , Cipher (..), CipherMode (..), KeyLength (..), SymmetricKey, IV (..)
  , Salt (..), Password
  , generateKey, generateIV, keyFromBytes, getKeyBytes, deriveKey, keyCipher
  , encrypt, decrypt
  , encryptWithIV, decryptWithIV
  , encryptUArray, decryptUArray
  ) where
import Control.Monad
import Control.Monad.IO.Class
import Haste
import Haste.Binary
import Haste.Concurrent
import Haste.Foreign (toAny, fromAny)
import Data.Word
import Data.Int
import Data.Array.IO
import Data.Array.Unboxed
import System.IO.Unsafe

import Haste.Crypto.Types
import Haste.Crypto.Prim

-- crypto.getRandomBytes bindings

class CryptoRandom a where
  -- | Generate an element of type @a@ using the browser's crypto strength
  --   random bit generator.
  cryptoRandom :: MonadIO m => m a

  -- | Generate @n@ random elements of type @a@.
  cryptoRandoms :: MonadIO m => Word32 -> m [a]
  cryptoRandoms n = mapM (const cryptoRandom) [1..n]

instance ArrView a => CryptoRandom a where
  cryptoRandom = randomPrimitive
  cryptoRandoms = cryptoRandomIOUArray >=> liftIO . getElems

-- | Generate a random immutable array of @n@ elements.
cryptoRandomUArray :: (MonadIO m, ArrView a) => Word32 -> m (UArray Word32 a)
cryptoRandomUArray = cryptoRandomIOUArray >=> liftIO . unsafeFreeze

-- | Generate a random mutable array of @n@ elements.
cryptoRandomIOUArray :: (MonadIO m, ArrView a) => Word32 -> m (IOUArray Word32 a)
cryptoRandomIOUArray n = liftIO $ do
  a <- newArray_ (0, n-1)
  randomBits a
  return a

-- | Generate a random salt of the given length.
{-# INLINE cryptoRandomSalt #-}
cryptoRandomSalt :: MonadIO m => Word32 -> m Salt
cryptoRandomSalt = fmap Salt . cryptoRandomUArray

-- | Generate a random primitive using @window.crypto@.
randomPrimitive :: (MonadIO m, ArrView a) => m a
randomPrimitive = cryptoRandomIOUArray 1 >>= liftIO . flip readArray 0

-- | Freeze a mutable array into an immutable one. Only safe if the source
--   array is guaranteed to never be mutated post-freeze.
unsafeFreeze :: (Ix i, ArrView e) => IOUArray i e -> IO (UArray i e)
unsafeFreeze = pure . toAny >=> fromAny



-- Partial SubtleCrypto bindings

-- | Generate a key for the given cipher.
generateKey :: MonadConc m => Cipher -> m SymmetricKey
generateKey = promise . generateKey'

-- | Get the specified key as an unboxed byte array.
--   Only the bytes of the key itself are returned; exported keys do not
--   include information about which cipher they are for.
getKeyBytes :: MonadConc m => SymmetricKey -> m (UArray Word32 Word8)
getKeyBytes = promise . keyBytes'

-- | Import a key from an unboxed byte array.
keyFromBytes :: MonadConc m => Cipher -> UArray Word32 Word8 -> m (Either JSString SymmetricKey)
keyFromBytes c k = promiseE $ keyFromBytes' c k

-- | Encrypt a message using the given cipher, key and initialization vector.
encryptUArray :: (Ix i, ArrView e, MonadConc m)
        => SymmetricKey -> IV -> UArray i e -> m (UArray Word32 Word8)
encryptUArray k iv msg = promise (encrypt' k iv msg)

-- | Encrypt a message using the given cipher, key and initialization vector.
decryptUArray :: (Ix i, ArrView e, MonadConc m)
        => SymmetricKey -> IV -> UArray i e -> m (Either JSString (UArray Word32 Word8))
decryptUArray k iv msg = promiseE $ decrypt' k iv msg

-- | Encrypt a message using a custom initialization vector. The IV will
--   not be prepended to the output.
encryptWithIV :: (Binary a, MonadConc m)
              => IV
              -> SymmetricKey
              -> a
              -> m Blob
encryptWithIV iv k val = do
  msg <- getBlobData $ encode val
  fromUArray <$> encryptUArray k iv (toUArray msg :: UArray Word32 Word8)

-- | Decrypt a message using a custom initialization vector.
decryptWithIV :: (Binary a, MonadConc m)
              => IV
              -> SymmetricKey
              -> Blob
              -> m (Either JSString a)
decryptWithIV iv k blob = do
  msg <- getBlobData blob
  emsg' <- decryptUArray k iv (toUArray msg :: UArray Word32 Word8)
  case emsg' of
    Left err   -> return $ Left err
    Right msg' -> do
      eres <- decodeBlob $ fromUArray msg'
      case eres of
        Right res -> return $ Right res
        Left err  -> return $ Left $ toJSString err

-- | Generate an byte initialization vector of appropriate length for the
--   given cipher.
generateIV :: MonadIO m => Cipher -> m IV
generateIV = fmap IV . cryptoRandomUArray . ivLength

-- | The default initialization vector length for the available ciphers.
ivLength :: Cipher -> Word32
ivLength (AES CBC Bits128) = 16
ivLength (AES CBC Bits256) = 32
ivLength (AES GCM _)       = 12

-- | Encrypt and encrypt a 'Binary' value using a random initialization vector.
--   The IV will be prepended to the message; when using GCM mode, the IV will
--   be 12 bytes, otherwise it will match the length of the key.
--
--   An unboxed array can be obtained from the
--   resulting 'Blob':
--
--       toUArray <$> getBlobData blob
encrypt :: (Binary a, MonadConc m)
        => SymmetricKey
        -> a
        -> m Blob
encrypt k msg = do
  iv <- generateIV (keyCipher k)
  msg' <- encryptWithIV iv k msg
  let derp = runPut (putBlob (fromUArray (ivBytes iv)) >> putBlob msg')
  return derp

-- | Decrypt and decode a 'Binary' value from a 'Blob'. The initialization
--   vector will be read from the beginning of the message; see 'encrypt' for
--   information about IV lengths.
--   A blob can be obtained from an unboxed array using 'fromUArray'.
decrypt :: (Binary a, MonadConc m)
        => SymmetricKey
        -> Blob
       -> m (Either JSString a)
decrypt k blob = do
  bd <- getBlobData blob
  case (getIV bd, getMsg bd) of
    (Left e, _)           -> return $ Left $ catJSStr "" ["couldn't read IV: ", e]
    (_, Left e)           -> return $ Left $ catJSStr "" ["couldn't read data: ", e]
    (Right iv, Right msg) -> do
      emsg' <- decryptUArray k (IV $ toUArray iv) (toUArray msg :: UArray Word32 Word8)
      case emsg' of
        Left err   -> return $ Left $ catJSStr "" ["decryption failed: ", err]
        Right msg' -> do
          eres <- decodeBlob $ fromUArray msg'
          case eres of
            Left err  -> return $ Left $ catJSStr "" ["couldn't parse value: ", err]
            Right res -> return $ Right res
  where
    ivLen = fromIntegral $ ivLength (keyCipher k)
    getIV = runGet $ getBytes ivLen
    getMsg = runGet $ skip ivLen >> getBytes (blobSize blob - ivLen)

-- | Derive a key from the given password and salt, using the PBKDF2 key
--   derivation over SHA-256, with the given number of iterations.
--
--   One million iterations take roughly half a second on a modern laptop.
--   Returns @Nothing@ on browsers where the operation is not supported.
--   Presently not supported by IE/Edge.
deriveKey :: MonadConc m => Cipher -> Int -> Salt -> Password -> m (Maybe SymmetricKey)
deriveKey c n s pass = promise $ deriveKey' c s n (encodeUtf8 pass)

encodeUtf8 :: JSString -> UArray Word32 Word8
encodeUtf8 = unsafePerformIO . encodeUtf8'