packages feed

s3-signer 0.3.0.0 → 0.4.0.0

raw patch · 7 files changed

+299/−100 lines, 7 filesdep +blaze-builderdep +byteabledep +bytestringPVP ok

version bump matches the API change (PVP)

Dependencies added: blaze-builder, byteable, bytestring, case-insensitive, s3-signer

API changes (from Hackage documentation)

- Network.S3: S3Keys :: ByteString -> ByteString -> S3Keys
- Network.S3: S3URL :: ByteString -> S3URL
- Network.S3: bucketName :: S3Request -> ByteString
- Network.S3: data S3Keys
- Network.S3: mimeType :: S3Request -> ByteString
- Network.S3: newtype S3URL
- Network.S3: objectName :: S3Request -> ByteString
- Network.S3: publicKey :: S3Keys -> ByteString
- Network.S3: s3method :: S3Request -> S3Method
- Network.S3: secondsToExpire :: S3Request -> Integer
- Network.S3: secretKey :: S3Keys -> ByteString
- Network.S3: signedRequest :: S3URL -> ByteString
+ Network.S3: S3DELETE :: S3Method
+ Network.S3: S3HEAD :: S3Method
+ Network.S3: S3SignedRequest :: [S3Header] -> ByteString -> ByteString -> ByteString -> ByteString -> ByteString -> S3SignedRequest
+ Network.S3: [bucketName] :: S3Request -> ByteString
+ Network.S3: [mimeType] :: S3Request -> Maybe ByteString
+ Network.S3: [objectName] :: S3Request -> ByteString
+ Network.S3: [payloadHash] :: S3Request -> Maybe ByteString
+ Network.S3: [queryString] :: S3Request -> Query
+ Network.S3: [regionName] :: S3Request -> ByteString
+ Network.S3: [requestTime] :: S3Request -> UTCTime
+ Network.S3: [s3headers] :: S3Request -> [S3Header]
+ Network.S3: [s3method] :: S3Request -> S3Method
+ Network.S3: [sigAlgorithm] :: S3SignedRequest -> ByteString
+ Network.S3: [sigCredential] :: S3SignedRequest -> ByteString
+ Network.S3: [sigDate] :: S3SignedRequest -> ByteString
+ Network.S3: [sigHeaders] :: S3SignedRequest -> [S3Header]
+ Network.S3: [sigPolicy] :: S3SignedRequest -> ByteString
+ Network.S3: [sigSignature] :: S3SignedRequest -> ByteString
+ Network.S3: data S3Header
+ Network.S3: data S3SignedRequest
+ Network.S3: getS3Header :: S3Header -> (ByteString, ByteString)
+ Network.S3: s3Header :: ByteString -> ByteString -> S3Header
+ Network.S3: s3HeaderBuilder :: S3Header -> Builder
- Network.S3: S3Request :: S3Method -> ByteString -> ByteString -> ByteString -> Integer -> S3Request
+ Network.S3: S3Request :: S3Method -> Maybe ByteString -> ByteString -> ByteString -> ByteString -> Query -> UTCTime -> Maybe ByteString -> [S3Header] -> S3Request
- Network.S3: generateS3URL :: S3Keys -> S3Request -> IO S3URL
+ Network.S3: generateS3URL :: ByteString -> S3Request -> IO S3SignedRequest

Files

README.md view
@@ -1,10 +1,16 @@-s3-signer [![Hackage](https://img.shields.io/hackage/v/s3-signer.svg?style=flat)](https://hackage.haskell.org/package/s3-signer)+s3-signer ======+![Hackage](https://img.shields.io/hackage/v/s3-signer.svg)+![Hackage Dependencies](https://img.shields.io/hackage-deps/v/s3-signer.svg)+![Haskell Programming Language](https://img.shields.io/badge/language-Haskell-blue.svg)+![BSD3 License](http://img.shields.io/badge/license-BSD3-brightgreen.svg)+![Build Status](https://api.travis-ci.org/dmjio/s3-signer.svg?branch=master)+ s3-signer is intended to be an aid in building secure cloud-based services with AWS. This library generates cryptographically secure URLs that expire at a user-defined interval. These URLs can be used to offload the process of uploading and downloading large files, freeing your-webserver to focus on other things. +webserver to focus on other things.  ### Features  - Minimal depedencies
s3-signer.cabal view
@@ -1,13 +1,13 @@ name:                s3-signer-version:             0.3.0.0+version:             0.4.0.0 homepage:            https://github.com/dmjio/s3-signer bug-reports:         https://github.com/dmjio/s3-signer/issues synopsis:            Pre-signed Amazon S3 URLs-description:         +description:         .-        s3-signer creates cryptographically secure Amazon S3 URLs that expire within a user-defined         -        period. It allows uploading and downloading of content from Amazon S3. -        Ideal for AJAX direct-to-s3 uploads via CORS and secure downloads. +        s3-signer creates cryptographically secure Amazon S3 URLs that expire within a user-defined+        period. It allows uploading and downloading of content from Amazon S3.+        Ideal for AJAX direct-to-s3 uploads via CORS and secure downloads.         Web framework agnostic with minimal dependencies.         .         > module Main where@@ -17,25 +17,49 @@         >   where         >     credentials = S3Keys "<public-key-goes-here>" "<secret-key-goes-here>"         >     request     = S3Request S3GET "application/extension" "bucket-name" "file-name.extension" 3 -- three seconds until expiration-        . +        .         Result         .         > S3URL "https://bucket-name.s3.amazonaws.com/file-name.extension?AWSAccessKeyId=<public-key-goes-here>&Expires=1402346638&Signature=1XraY%2Bhp117I5CTKNKPc6%2BiihRA%3D"  license:             BSD3 license-file:        LICENSE-author:              David Johnson <djohnson.m@gmail.com>+author:              David Johnson <djohnson.m@gmail.com>, William Casarin <jb55@jb55.com> maintainer:          David Johnson <djohnson.m@gmail.com>-copyright:           2014 David Johnson+copyright:           David Johnson (c) 2014-2018 category:            AWS, Network build-type:          Simple-cabal-version:       >=1.18-extra-source-files:  README.md        +cabal-version:       2.0+extra-source-files:  README.md+tested-with: GHC == 8.0.2, GHC == 8.2.2, GHC == 8.4.1 +library internal+  default-language:  Haskell2010+  build-depends: base == 4.*+               , base64-bytestring+               , bytestring+               , byteable+               , utf8-string+               , case-insensitive >= 1.2+               , blaze-builder >= 0.4+               , http-types+               , cryptohash+               , time++  hs-source-dirs: src+  exposed-modules: Network.S3+                 , Network.S3.Sign+                 , Network.S3.URL+                 , Network.S3.Types+ library   build-depends:       base == 4.*                      , base64-bytestring+                     , bytestring+                     , byteable                      , utf8-string+                     , case-insensitive >= 1.2+                     , blaze-builder >= 0.4                      , http-types                      , cryptohash                      , time@@ -47,6 +71,17 @@                      , Network.S3.Time                      , Network.S3.URL                      , Network.S3.Types+++test-suite test-simple+  default-language:  Haskell2010+  type: exitcode-stdio-1.0+  main-is: test/Test.hs+  build-depends: internal+               , base == 4.*+               , bytestring+               , blaze-builder >= 0.4+               , time   source-repository head
src/Network/S3.hs view
@@ -8,23 +8,15 @@     , module Network.S3.Types     ) where +import           Data.ByteString (ByteString)+import           Data.Time.Clock (getCurrentTime) import           Network.S3.Sign-import           Network.S3.Time import           Network.S3.Types-import           Network.S3.URL --- | Generates a cryptographically secure URL that expires within a--- user defined period------ See README for use cases and examples: <https://github.com/dmjio/s3-signer/blob/master/README.md>-generateS3URL :: S3Keys -- ^ Amazon S3 Keys-              -> S3Request  -- ^ Amazon S3 Request information-              -> IO S3URL -- ^ Generated URL-generateS3URL S3Keys{..} S3Request{..} = do-  time <- getExpirationTimeStamp secondsToExpire-  let url = case s3method of-              S3GET -> getURL bucketName objectName time-              S3PUT -> putURL bucketName objectName time mimeType -      req = s3URL bucketName objectName publicKey time (sign secretKey url)-  return (S3URL req) +generateS3URL :: ByteString -- ^ Amazon S3 SecretAccessKey+              -> S3Request -- ^ Amazon S3 Request information+              -> IO S3SignedRequest -- ^ Generated Request+generateS3URL secretKey req = do+  time <- getCurrentTime+  return (sign secretKey req time)
src/Network/S3/Sign.hs view
@@ -1,11 +1,71 @@+{-# LANGUAGE TupleSections #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+ module Network.S3.Sign  ( sign ) where -import           Crypto.Hash.SHA1       (hash)-import           Crypto.MAC.HMAC        (hmac)-import qualified Data.ByteString.Base64 as B64-import           Data.ByteString.UTF8   (ByteString)-import           Network.HTTP.Types.URI (urlEncode)+import qualified    Data.ByteString.Char8    as BS+import qualified    Data.ByteString.Base64   as Base64+import              Data.ByteString.UTF8     (ByteString)+import              Blaze.ByteString.Builder (toByteString, fromByteString)+import              Data.Monoid              ((<>))+import              Data.Time.Clock          (UTCTime)+import              Data.Time.Format         (formatTime, defaultTimeLocale)+import              Network.S3.Types         (S3Request(..), S3SignedRequest(..))+import              Network.S3.URL           (canonicalRequest)+import              Data.Byteable            (toBytes)+import              Crypto.Hash ++reqString :: S3Request -> Digest SHA256+reqString = hash . toByteString . canonicalRequest++hmacSHA256 :: ByteString -> ByteString -> HMAC SHA256+hmacSHA256 key = hmac key++hmac_ :: ByteString -> ByteString -> ByteString+hmac_ key = toBytes . hmacGetDigest . hmacSHA256 key++ -- | SHA1 Encrypted Signature-sign :: ByteString -> ByteString -> ByteString-sign secretKey url = urlEncode True . B64.encode $ hmac hash 64 secretKey url+sign :: ByteString -> S3Request -> UTCTime -> S3SignedRequest+sign key req utc =+  let+      seconds   = BS.pack (formatTime defaultTimeLocale "T%M%H%SZ" utc)+      date      = BS.pack (formatTime defaultTimeLocale "%Y%m%d" utc)++      timestamp = fromByteString (date <> seconds)+      reqHash   = fromByteString (digestToHexByteString (reqString req))+      region    = regionName req+      service   = "s3"++      dateKey              = hmac_ ("AWS4" <> key) date+      dateRegionKey        = hmac_ dateKey region+      dateRegionServiceKey = hmac_ dateRegionKey service+      signingKey           = hmac_ dateRegionServiceKey "aws4_request"++      scope =+        fromByteString date   <> fromByteString "/" <>+        fromByteString region <> fromByteString "/" <>+        fromByteString service <> fromByteString "/aws4_request"++      algorithm = "AWS4-HMAC-SHA256"++      signingStringBuilder =+        algorithm <> "\n" <>+        timestamp <> "\n" <>+        scope     <> "\n" <>+        reqHash++      stringToSign = toByteString signingStringBuilder+      signature    = hmacSHA256 signingKey stringToSign+      hexSignature = digestToHexByteString (hmacGetDigest signature)+  in+    S3SignedRequest {+        sigHeaders    = s3headers req+      , sigDate       = toByteString timestamp+      , sigCredential = toByteString scope+      , sigPolicy     = Base64.encode stringToSign+      , sigSignature  = hexSignature+      , sigAlgorithm  = toByteString algorithm+    }
src/Network/S3/Types.hs view
@@ -1,32 +1,72 @@ {-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE OverloadedStrings #-}  module Network.S3.Types-    ( S3URL (..)-    , S3Keys (..)-    , S3Method (..)+    ( S3Method (..)     , S3Request (..)+    , S3SignedRequest (..)+    , S3Header+    , getS3Header+    , s3Header+    , s3HeaderBuilder     ) where -import           Data.ByteString.UTF8 (ByteString)-import           GHC.Generics         (Generic)+import           Data.ByteString.UTF8     (ByteString)+import           GHC.Generics             (Generic)+import           Data.Char                (isSpace)+import           Network.HTTP.Types       (Query)+import           Data.Time.Clock          (UTCTime) -newtype S3URL = S3URL {-      signedRequest :: ByteString -- ^ Generated URL-    } deriving (Generic, Show)+import           Blaze.ByteString.Builder (Builder, fromByteString)+import           Data.Monoid              (mconcat)+import qualified Data.ByteString.Char8 as BS+import qualified Data.CaseInsensitive as CI -data S3Keys = S3Keys {-      publicKey :: ByteString -- ^ AWS Public Key-    , secretKey :: ByteString  -- ^ AWS Private Key-    } deriving (Generic, Show) -data S3Method = S3GET -- ^ GET Request-              | S3PUT -- ^ PUT Request+newtype S3Header = S3Header { getS3Header :: (ByteString, ByteString) }+  deriving (Generic, Show)+++data S3Method = S3GET    -- ^ GET Request+              | S3PUT    -- ^ PUT Request+              | S3HEAD   -- ^ HEAD Request+              | S3DELETE -- ^ DELETE Request     deriving (Generic, Show) + data S3Request = S3Request {-      s3method        :: S3Method -- ^ Type of HTTP Method-    , mimeType        :: ByteString -- ^ MIME Type-    , bucketName      :: ByteString -- ^ Name of Amazon S3 Bucket-    , objectName      :: ByteString -- ^ Name of Amazon S3 File-    , secondsToExpire :: Integer -- ^ Number of seconds until expiration+      s3method    :: S3Method         -- ^ Type of HTTP Method+    , mimeType    :: Maybe ByteString -- ^ MIME Type+    , bucketName  :: ByteString       -- ^ Name of Amazon S3 Bucket+    , objectName  :: ByteString       -- ^ Name of Amazon S3 File+    , regionName  :: ByteString       -- ^ Name of Amazon S3 Region+    , queryString :: Query            -- ^ Optional query string items+    , requestTime :: UTCTime          -- ^ Requests are valid within a 15 minute window of this timestamp+    , payloadHash :: Maybe ByteString -- ^ SHA256 hash string of the payload+    , s3headers   :: [S3Header]       -- ^ Headers     } deriving (Generic, Show)+++data S3SignedRequest = S3SignedRequest {+      sigHeaders    :: [S3Header] -- ^ The headers included in the signed request+    , sigDate       :: ByteString -- ^ The date you used in creating the signing key.+    , sigCredential :: ByteString -- ^ <your-access-key-id>/<date>/<aws-region>/<aws-service>/aws4_request+    , sigPolicy     :: ByteString -- ^ The Base64-encoded security policy that describes what is permitted in the request+    , sigSignature  :: ByteString -- ^ (AWS Signature Version 4) The HMAC-SHA256 hash of the security policy.+    , sigAlgorithm  :: ByteString -- ^ The signing algorithm used. For AWS Signature Version 4, the value is AWS4-HMAC-SHA256.+    } deriving (Generic, Show)+++trim :: ByteString -> ByteString+trim = BS.dropWhile isSpace . fst . BS.spanEnd isSpace++s3Header :: ByteString -> ByteString -> S3Header+s3Header header value = S3Header (lower, trimmed)+  where+    lower   = CI.foldCase header+    trimmed = trim value++s3HeaderBuilder :: S3Header -> Builder+s3HeaderBuilder (S3Header (header,value)) =+  mconcat [fromByteString header, ":", fromByteString value, "\n"]+
src/Network/S3/URL.hs view
@@ -1,55 +1,65 @@ {-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}  module Network.S3.URL-    ( putURL-    , getURL-    , s3URL+    ( canonicalRequest     ) where -import           Data.Monoid-import           Data.String+import           Data.Time.Format         (formatTime, defaultTimeLocale)+import           Data.ByteString.Char8    (pack)+import           Blaze.ByteString.Builder (Builder, fromByteString) --- | S3 Upload URL Template-putURL :: (Monoid m, IsString m) => m -> m -> m -> m -> m-putURL bucket object expires mimetype =-    mconcat [ "PUT\n\n"-            , mimetype -            ,"\n"-            , expires-            , "\nx-amz-acl:public-read\n/"-            , bucket-            , "/"-            , object-            ]+import qualified Network.HTTP.Types.URI as HTTP+import           Data.Function (on)+import           Data.List (sortBy, intersperse)+import           Data.Monoid ((<>))+import           Data.Maybe (fromMaybe) --- | S3 Download URL Template-getURL :: (Monoid m, IsString m) => m -> m -> m -> m-getURL bucket object expires =-    mconcat [ "GET\n\n\n"-            , expires-            , "\n/"-            , bucket-            , "/"-            , object-            ]+import           Network.S3.Types --- | Amazon S3 URL Template-baseUrl :: (Monoid m, IsString m) => m -> m -> m-baseUrl bucket object =-    mconcat [ "https://"-            , bucket-            , ".s3.amazonaws.com/"-            , object-            ]+sortS3Headers :: [S3Header] -> [S3Header]+sortS3Headers = sortBy (compare `on` (fst . getS3Header)) -s3URL :: (Monoid m, IsString m) =>-           m -> m -> m -> m -> m -> m-s3URL bucket object publicKey expires sig-    = mconcat [ baseUrl bucket object-              , "?AWSAccessKeyId="-              , publicKey-              , "&Expires="-              , expires-              , "&Signature="-              , sig-              ]++-- | Build a canonical request to be signed+--   AWS docs: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html#canonical-request+canonicalRequest :: S3Request -> Builder+canonicalRequest S3Request{..} =+  let+    -- We MUST sort the parameters in the query string alphabetically by key name+    qs         = sortBy (compare `on` fst) queryString+    emptyHash  = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"+    bodyHash   = fromMaybe emptyHash payloadHash+    hashHeader = s3Header "x-amz-content-sha256" bodyHash+    hostHeader = s3Header "host" (bucketName <> ".s3.amazonaws.com")+    seconds    = pack (formatTime defaultTimeLocale "T%M%H%SZ" requestTime)+    date       = pack (formatTime defaultTimeLocale "%Y%m%d" requestTime)+    timeHeader = s3Header "x-amz-date" (date <> seconds)+    headers    = sortS3Headers (timeHeader : hostHeader : hashHeader : s3headers)+    headerKeys = map (fst . getS3Header) headers++    httpMethod       = renderS3Method s3method+    canonicalURI     = HTTP.urlEncodeBuilder False objectName+    canonicalQS      = HTTP.renderQueryText False (HTTP.queryToQueryText qs)+    canonicalHeaders = foldMap s3HeaderBuilder headers+    signedHeaders    = foldMap fromByteString (intersperse ";" headerKeys)+    hashedPayload    = fromByteString bodyHash++    uriBuilder =+      httpMethod       <> "\n/" <>+      canonicalURI     <> "\n" <>+      canonicalQS      <> "\n" <>+      canonicalHeaders <> "\n" <>+      signedHeaders    <> "\n" <>+      hashedPayload+  in+    uriBuilder+++renderS3Method :: S3Method -> Builder+renderS3Method method =+  case method of+    S3GET    -> "GET"+    S3PUT    -> "PUT"+    S3HEAD   -> "HEAD"+    S3DELETE -> "DELETE"
+ test/Test.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+++module Main where++import Data.ByteString (ByteString)+import Data.Time.Calendar (fromGregorian)+import Data.Time.Clock (UTCTime(..))+import Network.S3+import Control.Monad (unless)+import Network.S3.Sign (sign)+import System.Exit (exitFailure)++expectedSig :: ByteString+expectedSig = "f0e8bdb87c964420e857bd35b5d6ed310bd44f0170aba48dd91039c6036bdb41"++testFail :: String -> IO ()+testFail msg = do+  print msg+  exitFailure++assertEqual :: (Show a, Eq a) => a -> a -> IO ()+assertEqual got expected = do+  let msg = "'" ++ show got ++ " does not match expected ''" ++ "'"+  unless (got == expected) (testFail msg)++main :: IO ()+main = do+  let time = UTCTime (fromGregorian 2013 5 24) 0+      sr@S3SignedRequest{..} = sign privKey (exampleRequest time) time+  assertEqual sigSignature expectedSig+  print sr++privKey :: ByteString+privKey = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"++pubKey :: ByteString+pubKey = "AKIAIOSFODNN7EXAMPLE"++-- https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html#example-signature-GET-object+exampleRequest :: UTCTime -> S3Request+exampleRequest time =+  S3Request {+      s3method    = S3GET+    , mimeType    = Nothing+    , bucketName  = "examplebucket"+    , regionName  = "us-east-1"+    , objectName  = "test.txt"+    , queryString = []+    , payloadHash = Nothing+    , requestTime = time+    , s3headers   = [ s3Header "range" "bytes=0-9" ]+  }++