packages feed

amazonka-s3-encryption-2.0: src/Amazonka/S3/Encryption.hs

-- |
-- Module      : Amazonka.S3.Encryption
-- Copyright   : (c) 2013-2023 Brendan Hay
-- License     : Mozilla Public License, v. 2.0.
-- Maintainer  : Brendan Hay <brendan.g.hay+amazonka.com>
-- Stability   : provisional
-- Portability : non-portable (GHC extensions)
--
-- Addons for <http://hackage.haskell.org/package/amazonka-s3 amazonka-s3> to
-- support client-side encryption.
--
-- Your client-side master keys and your unencrypted data are never sent to AWS;
-- therefore, it is important that you safely manage your encryption keys. If
-- you lose them, you won't be able to decrypt your data.
-- When generating a symmetric key, you should ensure
-- that the key length is compatible with the underlying 'AES256' cipher.
--
-- The encryption procedure is:
--
-- * A one-time-use symmetric key a.k.a. a data encryption key (or data key) and
-- initialisation vector (IV) are generated locally. This data key and IV are used
-- to encrypt the data of a single S3 object using an AES256 cipher in CBC mode,
-- with PKCS5 padding. (For each object sent, a completely separate data key and IV are generated.)
--
-- * The generated data encryption key used above is encrypted using a symmetric
-- AES256 cipher in ECB mode, asymmetric RSA, or KMS facilities, depending on the
-- client-side master key you provide.
--
-- * The encrypted data is uploaded and the encrypted data key and material description
-- are attached as object metadata (either headers or a separate instruction file).
-- If KMS is used, the material description helps determine which client-side master
-- key to later use for decryption, otherwise the configured client-side key at
-- time of decryption is used.
--
-- For decryption:
--
-- The encrypted object is downloaded from Amazon S3 along with any metadata.
-- If KMS was used to encrypt the data then the master key id is taken from the
-- metadata material description, otherwise the client-side master key in the
-- current environment is used to decrypt the data key, which in turn is used
-- to decrypt the object data.
--
-- The client-side master key you provide can be either a symmetric key, an
-- asymmetric public/private key pair, or a KMS master key.
--
-- The stored metadata format is designed to be compatible with the official Java
-- AWS SDK (both V1 and V2 envelopes), but only a limited set of the possible
-- encryption options are supported. Therefore assuming defaults, objects stored
-- with this library should be retrievable by any of the other official SDKs, and
-- vice versa.
module Amazonka.S3.Encryption
  ( -- * Usage
    -- $usage

    -- * Specifying Master Keys
    -- $master-key
    Key (..),
    kmsKey,
    asymmetricKey,
    symmetricKey,
    newSecret,

    -- * Request Encryption/Decryption
    -- $requests
    encrypt,
    decrypt,
    initiate,

    -- ** Instruction Files
    -- $instructions
    encryptInstructions,
    decryptInstructions,
    initiateInstructions,
    cleanupInstructions,

    -- *** Default Instruction Extension
    Ext (..),
    defaultExtension,

    -- * Handling Errors
    -- $errors
    EncryptionError (..),
    AsEncryptionError (..),
  )
where

import Amazonka as AWS
import Amazonka.Prelude
import Amazonka.S3
import Amazonka.S3.Encryption.Decrypt
import Amazonka.S3.Encryption.Encrypt
import Amazonka.S3.Encryption.Envelope
import Amazonka.S3.Encryption.Instructions
import Amazonka.S3.Encryption.Types
import Control.Lens
import Crypto.PubKey.RSA.Types as RSA
import Crypto.Random
import Data.Typeable (Typeable)

-- | Specify a KMS master key to use, with an initially empty material description.
--
-- /See:/ 'description', 'material'.
kmsKey :: Text -> Key
kmsKey k = KMS k mempty

-- | Specify the asymmetric key used for RSA encryption.
--
-- /See:/ 'description', 'material'.
asymmetricKey :: PrivateKey -> Key
asymmetricKey k = Asymmetric (KeyPair k) mempty

-- | Specify the shared secret to use for symmetric key encryption.
-- This must be compatible with the AES256 key size, 32 bytes.
--
-- Throws 'EncryptionError', specifically 'CipherFailure'.
--
-- /See:/ 'newSecret', 'description', 'material'.
symmetricKey :: MonadIO m => ByteString -> m Key
symmetricKey = fmap (`Symmetric` mempty) . createCipher

-- | Generate a random shared secret that is of the correct length to use with
-- 'symmetricKey'. This will need to be stored securely to enable decryption
-- of any requests that are encrypted using this secret.
newSecret :: MonadRandom m => m ByteString
newSecret = getRandomBytes aesKeySize

-- | Encrypt an object, storing the encryption envelope in @x-amz-meta-*@
-- headers.
--
-- Throws 'EncryptionError', 'AWS.Error'.
encrypt ::
  MonadResource m =>
  Key ->
  Env ->
  PutObject ->
  m PutObjectResponse
encrypt key env x = do
  (a, _) <- encrypted key env x
  send env (set location Metadata a)

-- | Encrypt an object, storing the encryption envelope in an adjacent instruction
-- file with the same 'ObjectKey' and 'defaultExtension'.
-- This makes two HTTP requests, storing the instruction file first and upon success,
-- storing the actual object.
--
-- Throws 'EncryptionError', 'AWS.Error'.
encryptInstructions ::
  MonadResource m =>
  Key ->
  Env ->
  PutObject ->
  m PutObjectResponse
encryptInstructions key env x = do
  (a, b) <- encrypted key env x
  _ <- send env b
  send env a

-- | Initiate an encrypted multipart upload, storing the encryption envelope
-- in the @x-amz-meta-*@ headers.
--
-- The returned 'UploadPart' @->@ 'Encrypted' 'UploadPart' function is used to encrypt
-- each part of the object. The same caveats for multipart upload apply, it is
-- assumed that each part is uploaded in order and each part needs to be
-- individually encrypted.
--
-- For example:
--
-- @
-- (a', f) <- initiate (a :: CreateMultipartUpload)
-- b'      <- send (f b :: Encrypted UploadPart)
-- @
--
-- Throws 'EncryptionError', 'AWS.Error'.
initiate ::
  MonadResource m =>
  Key ->
  Env ->
  CreateMultipartUpload ->
  m
    ( CreateMultipartUploadResponse,
      UploadPart -> Encrypted UploadPart
    )
initiate key env x = do
  (a, _) <- encrypted key env x
  rs <- send env (set location Metadata a)
  return (rs, encryptPart a)

-- | Initiate an encrypted multipart upload, storing the encryption envelope
-- in an adjacent instruction file with the same 'ObjectKey' and 'defaultExtension'.
--
-- The returned 'UploadPart' @->@ 'Encrypted' 'UploadPart' function is used to encrypt
-- each part of the object. The same caveats for multipart upload apply, it is
-- assumed that each part is uploaded in order and each part needs to be
-- individually encrypted.
--
-- Throws 'EncryptionError', 'AWS.Error'.
initiateInstructions ::
  MonadResource m =>
  Key ->
  Env ->
  CreateMultipartUpload ->
  m
    ( CreateMultipartUploadResponse,
      UploadPart -> Encrypted UploadPart
    )
initiateInstructions key env x = do
  (a, b) <- encrypted key env x
  rs <- send env a
  _ <- send env b
  return (rs, encryptPart a)

-- | Retrieve an object, parsing the envelope from any @x-amz-meta-*@ headers
-- and decrypting the response body.
--
-- Throws 'EncryptionError', 'AWS.Error'.
decrypt ::
  MonadResource m =>
  Key ->
  Env ->
  GetObject ->
  m GetObjectResponse
decrypt key env x = do
  let (a, _) = decrypted x
  Decrypted f <- send env a
  f key env Nothing

-- | Retrieve an object and its adjacent instruction file. The instruction
-- are retrieved and parsed first.
-- Performs two HTTP requests.
--
-- Throws 'EncryptionError', 'AWS.Error'.
decryptInstructions ::
  MonadResource m =>
  Key ->
  Env ->
  GetObject ->
  m GetObjectResponse
decryptInstructions key env x = do
  let (a, b) = decrypted x
  Instructions g <- send env b
  Decrypted f <- send env a
  g key env >>= f key env . Just

-- | Given a request to execute, such as 'AbortMultipartUpload' or 'DeleteObject',
-- remove the adjacent instruction file, if it exists with the 'defaultExtension'.
-- Performs two HTTP requests.
--
-- Throws 'EncryptionError', 'AWS.Error'.
cleanupInstructions ::
  ( MonadResource m,
    RemoveInstructions a,
    Typeable (AWSResponse a),
    Typeable a
  ) =>
  Env ->
  a ->
  m (AWSResponse a)
cleanupInstructions env x = do
  rs <- send env x
  _ <- send env (deleteInstructions x)
  return rs

-- $usage
-- When sending requests that make use of a master key, an extension to the underlying
-- 'AWS' environment is required. You can specify this environment as follows:
--
-- @
-- import Amazonka
-- import Amazonka.S3
-- import Amazonka.S3.Encryption
-- import System.IO
--
-- example :: Key -> IO GetObjectResponse
-- example k = do
--     -- A standard AWS environment with credentials is created using 'newEnv':
--     e <- newEnv Frankfurt Discover
--
--     runResourceT $ do
--         -- To store an encrypted object, 'encrypt' is used inplace of where you would
--         -- typically use 'AWS.send':
--         _ <- encrypt (kmsKey "alias/master-key") e (putObject "bucket-name" "object-key" body)
--
--         -- To retrieve a previously encrypted object, 'decrypt' is used, again similarly to
--         -- how you'd use 'AWS.send':
--         rs <- decrypt (kmsKey "alias/master-key") e (getObject "bucket-name" "object-key")
--
--         -- The 'GetObjectResponse' here contains a 'body' that is decrypted during read:
--         return rs
-- @

-- $master-key
-- You master key should be stored and secured by you alone (or KMS). The specific
-- key that is used to encrypt an object is required to decrypt the same object.
-- If you lose this key, you will not be able to decrypt the related objects.

-- $errors
-- Errors are thrown by the library using 'MonadThrow' and will consist of one of
-- the branches from 'EncryptionError' for anything crypto related, or a disparate
-- 'AWS.Error' anything related to the underlying 'AWS' service calls.
--
-- You can catch errors and sub-errors via 'trying' etc. from "Control.Exception.Lens",
-- and the appropriate 'AsEncryptionError' 'Prism':
--
-- @
-- trying '_EncryptionError' (encrypt (putObject "bkt" "key")) :: Either 'EncryptionError' PutObjectResponse
-- @

-- $requests
-- Only a small number of S3 operations actually utilise encryption/decryption
-- behaviour, namely 'PutObject', 'GetObject', and the related multipart upload
-- operations. The following functions store the encryption envelope in object
-- metadata (headers).

-- $instructions
-- An alternative method of storing the encryption envelope in an adjacent S3
-- object is provided for the case when metadata headers are reserved for other
-- data. This method removes the metadata overhead at the expense of an additional
-- HTTP request to perform encryption/decryption.
-- The provided @*Instruction@ functions make the convenient assumption that
-- the 'defaultExtension' is desired. If you wish to override the suffix\/extension,
-- you can simply call the underlying plumbing to modify the
-- 'PutInstructions' or 'GetInstructions' suffix before sending.
--
-- An example of encryption with a non-default instruction extension:
--
-- @
-- (a, b) <- 'encrypted' (x :: 'PutObject')
-- _      <- 'AWS.send' (b & 'piExtension' .~ ".envelope") -- Store the custom instruction file.
-- 'AWS.send' a -- Store the actual encrypted object.
-- @