-- | -- Module : Amazonka.S3.Encryption -- Copyright : (c) 2013-2023 Brendan Hay -- License : Mozilla Public License, v. 2.0. -- Maintainer : Brendan Hay -- Stability : provisional -- Portability : non-portable (GHC extensions) -- -- Addons for 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. -- @