packages feed

encryptable (empty) → 0.1

raw patch · 10 files changed

+438/−0 lines, 10 filesdep +QuickCheckdep +basedep +bytestringsetup-changed

Dependencies added: QuickCheck, base, bytestring, cryptonite, encryptable, esqueleto, generic-arbitrary, hspec, persistent, persistent-template, quickcheck-instances, text, universum

Files

+ ChangeLog.md view
@@ -0,0 +1,3 @@+# Version 0.1++- Initial version.
+ LICENSE view
@@ -0,0 +1,30 @@+Coingaming (c) 2020++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of Author name here nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,3 @@+# encryptable++Typed encryption with persistent support. You can find documentation at [hackage](http://hackage.haskell.org/package/encryptable/docs/Data-Encryptable.html).
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ encryptable.cabal view
@@ -0,0 +1,78 @@+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.34.2.+--+-- see: https://github.com/sol/hpack++name:           encryptable+version:        0.1+synopsis:       Typed encryption with persistent support+description:    You can find documentation at <http://hackage.haskell.org/package/encryptable/docs/Data-Encryptable.html hackage>+category:       Cryptography, Database, Yesod+homepage:       https://github.com/coingaming/encryptable#readme+bug-reports:    https://github.com/coingaming/encryptable/issues+author:         Ilja Tkachuk <tkachuk.labs@gmail.com>+maintainer:     Ilja Tkachuk <tkachuk.labs@gmail.com>+copyright:      2020 Coingaming <hello@coingaming.io>+license:        BSD3+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md+    ChangeLog.md++source-repository head+  type: git+  location: https://github.com/coingaming/encryptable++library+  exposed-modules:+      Data.Encryptable+  other-modules:+      Paths_encryptable+  hs-source-dirs:+      src+  default-extensions: NoImplicitPrelude MultiParamTypeClasses LambdaCase OverloadedStrings ScopedTypeVariables+  ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-monomorphism-restriction+  build-depends:+      QuickCheck >=2.13.2 && <2.14+    , base >=4.7 && <5+    , bytestring >=0.10.8 && <0.11+    , cryptonite >=0.25 && <0.27+    , esqueleto >=3.0.0 && <3.4+    , generic-arbitrary >=0.1.0 && <0.2+    , hspec >=2.7.1 && <2.8+    , persistent >=2.9.2 && <2.10+    , persistent-template >=2.6.0 && <2.7+    , quickcheck-instances >=0.3.22 && <0.4+    , text >=1.2.3 && <1.3+    , universum >=1.5.0 && <1.6+  default-language: Haskell2010++test-suite encryptable-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      EncryptableSpec+      TestEnv+      UserSpec+      Paths_encryptable+  hs-source-dirs:+      test+  default-extensions: NoImplicitPrelude MultiParamTypeClasses LambdaCase OverloadedStrings ScopedTypeVariables+  ghc-options: -Weverything -Wno-missing-exported-signatures -Wno-missing-import-lists -Wno-missed-specialisations -Wno-all-missed-specialisations -Wno-unsafe -Wno-safe -Wno-missing-local-signatures -Wno-monomorphism-restriction -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      QuickCheck >=2.13.2 && <2.14+    , base >=4.7 && <5+    , bytestring >=0.10.8 && <0.11+    , cryptonite >=0.25 && <0.27+    , encryptable+    , esqueleto >=3.0.0 && <3.4+    , generic-arbitrary >=0.1.0 && <0.2+    , hspec >=2.7.1 && <2.8+    , persistent >=2.9.2 && <2.10+    , persistent-template >=2.6.0 && <2.7+    , quickcheck-instances >=0.3.22 && <0.4+    , text >=1.2.3 && <1.3+    , universum >=1.5.0 && <1.6+  default-language: Haskell2010
+ src/Data/Encryptable.hs view
@@ -0,0 +1,175 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | Data encryption is common security-related practice in+-- database usage. One of the negative side effects of encryption is+-- that typed data in its encrypted form becomes untyped and+-- usually exists in form of 'ByteString' or similar blind type.+-- Operations with untyped data are very error-prone and should be avoided.+-- This library proposes the way to fix it.+--+-- Let's have an example of @User@ sum type where his @Login@+-- is not sensitive type, but @Address@ is sensitive.+-- @Address@ should never be shown and should be stored only in+-- encrypted form.+--+-- @+-- newtype Login+--   = Login Text+--   deriving newtype (Eq, Arbitrary, Show, PersistField, PersistFieldSql)+--+-- newtype Address+--   = Address Text+--   deriving newtype (Eq, Arbitrary, Encryptable ByteString UnicodeException)+--+-- instance Show Address where+--   show = const \"SECRET\"+--+-- data User = User {login :: Login, address :: Address}+--   deriving (Eq, Generic, Show)+--+-- instance Arbitrary User where+--   arbitrary = genericArbitrary+--   shrink = genericShrink+-- @+--+-- Note how easy we derived @Encryptable ByteString UnicodeException@ class+-- instance for @Address@ type. @Address@ is newtype around 'Text' which already+-- have this instance - so we just got it for free. @GeneralizedNewtypeDeriving@+-- is a very powerful tool, indeed. Having this instance means that now we can+-- encrypt @Address@ to 'ByteString' form and decrypt back with possible+-- 'UnicodeException' error (because not every encrypted 'ByteString' represents+-- valid @Address@). You can find more details in 'Encrypted', 'Encryptable'+-- and 'Encryptor' documentation.+--+-- Now let's define @UserStorage@ type, representation of @User@+-- stored in database. We will use @Persistent@ library DSL for this.+--+-- @+-- share+--   [mkPersist sqlSettings]+--   [persistLowerCase|+--     UserStorage+--       login Login+--       address (Encrypted ByteString UnicodeException Address)+--       UniqueUserStorage login+--   |]+-- @+--+-- In spite of @address@ database table column type is still just @bytes@,+-- compiler knows that these bytes in reality are encrypted representation+-- of @Address@ value.+--+-- Just for fun let's implement class instance to encrypt @User@ value+-- into @UserStorage@ value.+--+-- @+-- instance Encryptable UserStorage UnicodeException User where+--   encrypt c i x = Encrypted $ UserStorage (login x) $ encrypt c i (address x)+--   decrypt c i x0 = do+--     let x = coerce x0+--     a <- decrypt c i $ userStorageAddress x+--     return $ User (userStorageLogin x) a+-- @+--+-- And then we can test property - @User@ can be encrypted into+-- @UserStorage@ form and decrypted back.+--+-- @+-- spec :: Spec+-- spec = before newEnv+--   $ it "UserStorage/User"+--   $ \env -> property $ \x -> do+--     let c = cipher env+--     let i = iv env+--     decrypt c i (encrypt c i x :: Encrypted UserStorage UnicodeException User)+--       \`shouldBe\` Right x+-- @+module Data.Encryptable+  ( -- * Type+    Encrypted (..),++    -- * Class+    Encryptable (..),+    Encryptor (..),++    -- * Utility+    reType,++    -- * Re-export+    CryptoFailable (..),+    BlockCipher,+    AES256,+    IV,+    cipherInit,+    makeIV,+    getRandomBytes,+  )+where++import Crypto.Cipher.AES (AES256)+import Crypto.Cipher.Types (BlockCipher, IV, cipherInit, ctrCombine, makeIV)+import Crypto.Error (CryptoFailable (..))+import Crypto.Random (getRandomBytes)+import qualified Data.ByteString.Lazy as BL (ByteString, fromStrict, toStrict)+import Data.Coerce (coerce)+import qualified Data.Text.Encoding as TE (decodeUtf8', encodeUtf8)+import qualified Data.Text.Lazy as TL (Text, fromStrict, toStrict)+import Database.Esqueleto (PersistField, PersistFieldSql)+import Universum++-- | Value of this type represents+-- value of type __a__ (phantom) encrypted in form of+-- value of type __b__ (non-phantom) which can cause+-- error of type __e__ (phantom) in case where+-- __a__ constructor fails after decryption.+-- This design promotes usage of smart constructors.+newtype Encrypted b e a = Encrypted b+  deriving (PersistField, PersistFieldSql)++-- | Class represents the idea of+-- typed symmetric encryption and decryption+class Encryptable b e a where+  encrypt :: (BlockCipher c) => c -> IV c -> a -> Encrypted b e a+  decrypt :: (BlockCipher c) => c -> IV c -> Encrypted b e a -> Either e a++-- | Class represents one particular case of 'Encryptable'+-- where 'BlockCipher' and 'IV' (initial vector) are+-- hidden inside __m__ which often is+-- some sort of "application" monad which implements+-- this 'Encryptor' class. Promotes finally tagless style.+class Encryptor m where+  encryptM :: (Encryptable b e a) => a -> m (Encrypted b e a)+  decryptM :: (Encryptable b e a) => Encrypted b e a -> m (Either e a)++instance Encryptable ByteString e ByteString where+  encrypt c i = Encrypted . ctrCombine c i+  decrypt c i = Right . ctrCombine c i . coerce++instance Encryptable ByteString e BL.ByteString where+  encrypt c i = reType . encrypt c i . BL.toStrict+  decrypt c i = second BL.fromStrict . decrypt c i . reType++instance Encryptable ByteString UnicodeException Text where+  encrypt c i = reType . encrypt c i . TE.encodeUtf8+  decrypt c i x = decrypt c i (reType x) >>= TE.decodeUtf8'++instance Encryptable ByteString UnicodeException TL.Text where+  encrypt c i = reType . encrypt c i . TL.toStrict+  decrypt c i = second TL.fromStrict . decrypt c i . reType++instance (Traversable f, Encryptable b e a) => Encryptable (f b) e (f a) where+  encrypt c i =+    Encrypted . ((coerce . (encrypt c i :: a -> Encrypted b e a) :: a -> b) <$>)+  decrypt c i xs =+    mapM (decrypt c i . Encrypted) (coerce xs :: f b)++-- | Casts original phantom type __a__ of 'Encrypted'+-- value to some other type __c__. Useful for building+-- 'Encryptable' instances on top of other already+-- existing 'Encryptable' instances.+reType :: Encrypted b e a -> Encrypted b e c+reType = Encrypted . coerce
+ test/EncryptableSpec.hs view
@@ -0,0 +1,30 @@+module EncryptableSpec+  ( spec,+  )+where++import qualified Data.ByteString.Lazy as BL+import qualified Data.Text.Lazy as TL+import Test.Hspec+import Test.QuickCheck+import Test.QuickCheck.Instances ()+import TestEnv+import Universum++spec :: Spec+spec = before newEnv $ do+  --+  -- TODO : Traversable and Persistent properties+  --+  it "ByteString/ByteString" $ \env -> property $ \x ->+    reCryptBS env x+      `shouldBe` (Right x :: Either () ByteString)+  it "ByteString/BL.ByteString" $ \env -> property $ \x ->+    reCryptBS env x+      `shouldBe` (Right x :: Either () BL.ByteString)+  it "ByteString/Text" $ \env -> property $ \x ->+    reCryptBS env x+      `shouldBe` (Right x :: Either UnicodeException Text)+  it "ByteString/TL.Text" $ \env -> property $ \x ->+    reCryptBS env x+      `shouldBe` (Right x :: Either UnicodeException TL.Text)
+ test/Spec.hs view
@@ -0,0 +1,2 @@+{-# OPTIONS_GHC -Wno-missing-export-lists #-}+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ test/TestEnv.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++module TestEnv+  ( Env (..),+    newEnv,+    reCryptBS,+    Login (..),+    Address (..),+  )+where++import Data.Encryptable+import Database.Esqueleto (PersistField, PersistFieldSql)+import Test.QuickCheck+import Test.QuickCheck.Instances ()+import Universum+import Prelude (Show (..))++data Env = Env {cipher :: AES256, iv :: IV AES256}++newEnv :: IO Env+newEnv = do+  c0 :: ByteString <- getRandomBytes 32+  i0 :: ByteString <- getRandomBytes 16+  c <- case cipherInit c0 of+    CryptoPassed x -> return x+    CryptoFailed _ -> fail "BAD_CIPHER"+  i <- case makeIV i0 of+    Just x -> return x+    Nothing -> fail "BAD_IV"+  return $ Env c i++reCryptBS ::+  forall a e.+  (Encryptable ByteString e a) =>+  Env ->+  a ->+  Either e a+reCryptBS env x =+  decrypt c i (encrypt c i x :: Encrypted ByteString e a)+  where+    c = cipher env+    i = iv env++newtype Login+  = Login Text+  deriving newtype (Eq, Arbitrary, Show, PersistField, PersistFieldSql)++newtype Address+  = Address Text+  deriving newtype (Eq, Arbitrary, Encryptable ByteString UnicodeException)++instance Show Address where+  show = const "SECRET"
+ test/UserSpec.hs view
@@ -0,0 +1,59 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE EmptyDataDecls #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE GADTs #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE TypeFamilies #-}++module UserSpec+  ( spec,+    -- just supressing warning+    UserStorageId,+  )+where++import Data.Coerce (coerce)+import Data.Encryptable+import Database.Persist.TH+import Test.Hspec+import Test.QuickCheck+import Test.QuickCheck.Arbitrary.Generic+import Test.QuickCheck.Instances ()+import TestEnv+import Universum++data User = User {login :: Login, address :: Address}+  deriving (Eq, Generic, Show)++instance Arbitrary User where+  arbitrary = genericArbitrary+  shrink = genericShrink++share+  [mkPersist sqlSettings]+  [persistLowerCase|+    UserStorage+      login Login+      address (Encrypted ByteString UnicodeException Address)+      UniqueUserStorage login+  |]++instance Encryptable UserStorage UnicodeException User where+  encrypt c i x = Encrypted $ UserStorage (login x) $ encrypt c i (address x)+  decrypt c i x0 = do+    let x = coerce x0+    a <- decrypt c i $ userStorageAddress x+    return $ User (userStorageLogin x) a++spec :: Spec+spec = before newEnv+  $ it "UserStorage/User"+  $ \env -> property $ \x -> do+    let c = cipher env+    let i = iv env+    decrypt c i (encrypt c i x :: Encrypted UserStorage UnicodeException User)+      `shouldBe` Right x