├── stack.yaml ├── credentials-cli ├── Setup.hs ├── src │ ├── Credentials │ │ └── CLI │ │ │ ├── Types │ │ │ └── Protocol.hs │ │ │ ├── IO.hs │ │ │ ├── Options.hs │ │ │ ├── Format.hs │ │ │ └── Types.hs │ └── Main.hs ├── credentials-cli.cabal └── LICENSE ├── credentials ├── Setup.hs ├── test │ └── Main.hs ├── CHANGELOG.md ├── credentials.cabal ├── src │ ├── Credentials.hs │ └── Credentials │ │ ├── KMS.hs │ │ ├── DynamoDB │ │ └── Item.hs │ │ ├── Types.hs │ │ └── DynamoDB.hs └── LICENSE ├── Makefile ├── script ├── dynamo-local.sh ├── travis-lifecycle-before_install ├── travis-timeout ├── travis-lifecycle-script ├── travis-lifecycle-install ├── dynamo-test.sh └── travis-documentation ├── stack-7.10.3.yaml ├── stack-8.0.1.yaml ├── share └── completion.bash ├── .gitignore ├── .travis.yml ├── LICENSE └── README.md /stack.yaml: -------------------------------------------------------------------------------- 1 | stack-8.0.1.yaml -------------------------------------------------------------------------------- /credentials-cli/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /credentials/Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /usr/bin/env bash 2 | 3 | build: 4 | stack build --copy-bins 5 | 6 | clean: 7 | stack clean 8 | -------------------------------------------------------------------------------- /script/dynamo-local.sh: -------------------------------------------------------------------------------- 1 | java -Djava.library.path=./dynamodb-local/DynamoDBLocal_lib -jar ./dynamodb-local/DynamoDBLocal.jar -sharedDb 2 | -------------------------------------------------------------------------------- /stack-7.10.3.yaml: -------------------------------------------------------------------------------- 1 | compiler: ghc-7.10.3 2 | resolver: lts-6.1 3 | flags: 4 | aeson-pretty: 5 | lib-only: true 6 | extra-deps: 7 | - aeson-pretty-0.8.1 8 | packages: 9 | - credentials 10 | - credentials-cli 11 | -------------------------------------------------------------------------------- /stack-8.0.1.yaml: -------------------------------------------------------------------------------- 1 | compiler: ghc-8.0.1 2 | resolver: nightly-2016-08-11 3 | flags: 4 | aeson-pretty: 5 | lib-only: true 6 | extra-deps: 7 | - aeson-pretty-0.8.1 8 | packages: 9 | - credentials 10 | - credentials-cli 11 | -------------------------------------------------------------------------------- /script/travis-lifecycle-before_install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | mkdir -p ~/.local/bin 6 | 7 | curl -L https://www.stackage.org/stack/linux-x86_64 | \ 8 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack' 9 | 10 | ghc --version 11 | stack --version 12 | -------------------------------------------------------------------------------- /script/travis-timeout: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | $* & 4 | pidA=$! 5 | minutes=0 6 | 7 | while true; do sleep 60; ((minutes++)); echo -e "\033[0;32m$minutes minute(s) elapsed running '$*'\033[0m."; done & 8 | pidB=$! 9 | wait $pidA 10 | resA=$? 11 | 12 | echo -e "\033[0;32m'$*' exited with status ${resA}.\033[0m" 13 | kill -9 $pidB 14 | 15 | exit $resA 16 | -------------------------------------------------------------------------------- /share/completion.bash: -------------------------------------------------------------------------------- 1 | _credentials() 2 | { 3 | local cmdline 4 | CMDLINE=(--bash-completion-index $COMP_CWORD) 5 | 6 | for arg in ${COMP_WORDS[@]}; do 7 | CMDLINE=(${CMDLINE[@]} --bash-completion-word $arg) 8 | done 9 | 10 | COMPREPLY=( $(credentials "${CMDLINE[@]}") ) 11 | } 12 | 13 | complete -o filenames -F _credentials credentials 14 | -------------------------------------------------------------------------------- /credentials/test/Main.hs: -------------------------------------------------------------------------------- 1 | -- | 2 | -- Module : Main 3 | -- Copyright : (c) 2015-2016 Brendan Hay 4 | -- License : Mozilla Public License, v. 2.0. 5 | -- Maintainer : Brendan Hay 6 | -- Stability : provisional 7 | -- Portability : non-portable (GHC extensions) 8 | -- 9 | module Main (main) where 10 | 11 | main :: IO () 12 | main = putStrLn "Test suite not yet implemented" 13 | -------------------------------------------------------------------------------- /credentials/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.0.1.1](https://github.com/brendanhay/credentials/tree/0.0.1.1) 4 | Released: **10 August, 2016**, Compare: [0.0.1](https://github.com/brendanhay/credentials/compare/0.0.1...0.0.1.1) 5 | 6 | ### Changed 7 | 8 | - TemplateHaskell is no longer used to derive `Prism`s, they are now hand written. 9 | 10 | 11 | ## [0.0.1](https://github.com/brendanhay/credentials/tree/0.0.1) 12 | Released: **9 August, 2016** 13 | 14 | ### Initial Release 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Haskell 2 | .conf 3 | *.o 4 | *.hi 5 | *.chi 6 | *.chs.h 7 | *.imports 8 | cabal.sandbox.config 9 | cabal.config 10 | 11 | # Dirs 12 | .cabal-sandbox 13 | dist 14 | vendor 15 | .vagrant.d 16 | .stack-work 17 | 18 | # CTAGS 19 | TAGS 20 | tags 21 | 22 | # Emacs/Vim 23 | *~ 24 | *# 25 | .#* 26 | \#*# 27 | .*.sw[a-z] 28 | *.un~ 29 | *.org 30 | .projectile 31 | 32 | # OSX 33 | .DS_Store 34 | 35 | # Certificates 36 | *.pem 37 | 38 | # Binaries 39 | /bin 40 | 41 | # DynamoDB Local 42 | /dynamodb-local 43 | *.db 44 | -------------------------------------------------------------------------------- /script/travis-lifecycle-script: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | GHCVER=${GHCVER?"GHCVER needs to be set."} 6 | LIBRARY=${LIBRARY?"LIBRARY needs to be set."} 7 | 8 | path=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) 9 | 10 | if [ "$LIBRARY" == "documentation" ]; then 11 | echo "Building documentation..." 12 | $path/travis-documentation 13 | else 14 | echo "Running tests for ${LIBRARY}..." 15 | stack --no-terminal --skip-ghc-check --stack-yaml stack-$GHCVER.yaml build --test --fast $LIBRARY 16 | fi 17 | -------------------------------------------------------------------------------- /script/travis-lifecycle-install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | GHCVER=${GHCVER?"GHCVER needs to be set."} 6 | LIBRARY=${LIBRARY?"LIBRARY needs to be set."} 7 | 8 | path=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) 9 | 10 | $path/travis-timeout \ 11 | stack --no-terminal --skip-ghc-check --stack-yaml stack-$GHCVER.yaml setup 12 | 13 | if [ "$LIBRARY" != "documentation" ]; then 14 | $path/travis-timeout \ 15 | stack --no-terminal --skip-ghc-check --stack-yaml stack-$GHCVER.yaml build --test --fast --only-snapshot $LIBRARY 16 | fi 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # This is the simple Travis configuration, which is intended for use 2 | # on applications which do not require cross-platform and 3 | # multiple-GHC-version support. For more information and other 4 | # options, see: 5 | # 6 | # https://docs.haskellstack.org/en/stable/travis_ci/ 7 | # 8 | # Copy these contents into the root directory of your Github project in a file 9 | # named .travis.yml 10 | 11 | # Use new container infrastructure to enable caching 12 | sudo: false 13 | 14 | # Do not choose a language; we provide our own build tools. 15 | language: generic 16 | 17 | # Caching so the next build will be fast too. 18 | cache: 19 | directories: 20 | - $HOME/.stack 21 | 22 | # Ensure necessary system libraries are present 23 | addons: 24 | apt: 25 | packages: 26 | - libgmp-dev 27 | 28 | before_install: 29 | # Download and unpack the stack executable 30 | - mkdir -p ~/.local/bin 31 | - export PATH=$HOME/.local/bin:$PATH 32 | - travis_retry curl -L https://www.stackage.org/stack/linux-x86_64 | tar xz --wildcards --strip-components=1 -C ~/.local/bin '*/stack' 33 | 34 | install: 35 | # Build dependencies 36 | - stack --no-terminal --install-ghc test --only-dependencies 37 | 38 | script: 39 | # Build the package, its tests, and its docs and run the tests 40 | - stack --no-terminal test --haddock --no-haddock-deps 41 | -------------------------------------------------------------------------------- /script/dynamo-test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | store="$1" 6 | 7 | if [ -z "$store" ]; then 8 | echo "Usage: run " 9 | exit 1 10 | fi 11 | 12 | region="eu-central-1" 13 | 14 | send() { 15 | local mode="$1" 16 | local args="$2" 17 | 18 | credentials $mode -r $region -u $store $args --output echo 19 | } 20 | 21 | selects() { 22 | local name="$1" 23 | local expect="$2" 24 | local context="$3" 25 | 26 | local actual="$(credentials select -r $region -u $store --name $name $context --output echo)" 27 | 28 | if [ "$expect" != "$actual" ]; then 29 | send "list" 30 | echo "" 31 | echo "Test failed for $name, expected: $expect, actual: $actual" 32 | send "select" "-l debug --name $name $context" 33 | echo "" 34 | exit 1 35 | fi 36 | } 37 | 38 | inserts() { 39 | local __revision=$1 40 | local name="$2" 41 | local secret="$3" 42 | local context="$4" 43 | 44 | local revision=$(credentials insert -r $region -u $store --name $name -s "$secret" $context --output echo) 45 | selects "$name" "$secret" "$context" 46 | 47 | eval $__revision="'$revision'" 48 | } 49 | 50 | send "teardown" "-f" 51 | echo "" 52 | send "setup" 53 | 54 | echo "" 55 | echo "Insert series ..." 56 | 57 | inserts foo1 "foo" "secret" 58 | inserts foo2 "foo" "not so secret" 59 | inserts foo3 "foo" "something" 60 | inserts foo4 "foo" "bother" 61 | inserts bar1 "bar" "rah rah rah" 62 | inserts bar2 "bar" "nothing" 63 | inserts baz1 "baz" "supercalifragilistic" 64 | inserts baz2 "baz" "anything" 65 | 66 | echo "Delete revision $foo3" 67 | 68 | send "delete" "--name foo --revision $foo3 -f" 69 | selects "foo" "bother" 70 | 71 | echo "Delete revision $foo4" 72 | 73 | send "delete" "--name foo --revision $foo4 -f" 74 | selects "foo" "not so secret" 75 | 76 | echo "Insert with context ..." 77 | 78 | inserts bar3 "bar" "longer secret '-c a=b -c this=notthis'" 79 | 80 | send "list" 81 | 82 | echo "Done." 83 | -------------------------------------------------------------------------------- /credentials-cli/src/Credentials/CLI/Types/Protocol.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleInstances #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | 5 | -- | 6 | -- Module : Credentials.CLI.Types.Protocol 7 | -- Copyright : (c) 2015-2016 Brendan Hay 8 | -- License : Mozilla Public License, v. 2.0. 9 | -- Maintainer : Brendan Hay 10 | -- Stability : provisional 11 | -- Portability : non-portable (GHC extensions) 12 | -- 13 | module Credentials.CLI.Types.Protocol where 14 | 15 | import Control.Lens (preview, _Just) 16 | 17 | import Credentials (DynamoTable (..)) 18 | 19 | import Data.Attoparsec.Text (Parser) 20 | import Data.ByteString (ByteString) 21 | import Data.Maybe 22 | import Data.Text (Text) 23 | 24 | import Network.AWS.Data (FromText, fromText, toBS, toText) 25 | 26 | import URI.ByteString 27 | 28 | import qualified Data.Attoparsec.Text as A 29 | import qualified Data.ByteString.Char8 as BS8 30 | import qualified Data.Text as Text 31 | 32 | uriParser :: FromURI a => Parser a 33 | uriParser = uri >>= either fail pure . fromURI 34 | where 35 | uri = A.takeText >>= either (fail . show) pure . f . toBS 36 | f = parseURI strictURIParserOptions 37 | 38 | class FromURI a where 39 | fromURI :: URI -> Either String a 40 | 41 | -- dynamo:/table-name 42 | instance FromURI DynamoTable where 43 | fromURI u = do 44 | scheme "dynamo" u 45 | ensure "Table name cannot be empty." (path u) 46 | 47 | ensure :: FromText a => String -> Text -> Either String a 48 | ensure m x 49 | | Text.null x = Left m 50 | | otherwise = fromText x 51 | 52 | scheme :: ByteString -> URI -> Either String () 53 | scheme e u 54 | | a == e = Right () 55 | | otherwise = Left $ "Protocol '" ++ BS8.unpack a ++ "' unrecognized." 56 | where 57 | a = schemeBS (uriScheme u) 58 | 59 | path :: URI -> Text 60 | path = toText . BS8.dropWhile (== '/') . uriPath 61 | 62 | host :: URI -> Maybe ByteString 63 | host = preview (authorityL . _Just . authorityHostL . hostBSL) 64 | 65 | secure :: URI -> Bool 66 | secure = (== 443) . port 67 | 68 | port :: URI -> Int 69 | port = fromMaybe 443 . preview 70 | (authorityL . _Just . authorityPortL . _Just . portNumberL) 71 | -------------------------------------------------------------------------------- /script/travis-documentation: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | if [ "$TRAVIS_BRANCH" != "develop" ]; then 6 | echo "Ignoring documentation build for branch '$TRAVIS_BRANCH'." 7 | exit 0 8 | fi 9 | 10 | if [ -z "$DOC_TOKEN" ]; then 11 | echo "Ignoring documentation build due to missing token." 12 | exit 0 13 | fi 14 | 15 | DOC_REF=github.com/brendanhay/credentials-doc.git 16 | GHCVER=${GHCVER?"GHCVER needs to be set."} 17 | BUILD_ID=${TRAVIS_BUILD_ID?"TRAVIS_BUILD_ID needs to be set."} 18 | BUILD_NUMBER=${TRAVIS_BUILD_NUMBER?"TRAVIS_BUILD_NUMBER needs to be set."} 19 | 20 | path=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) 21 | 22 | output=doc 23 | platform=x86_64-linux 24 | resolver=$(sed -n 's/^resolver: *\(.*\)$/\1/p' stack-$GHCVER.yaml) 25 | 26 | echo "Using resolver ${resolver}" 27 | 28 | databases=( $(stack --stack-yaml stack-$GHCVER.yaml path | sed -n 's/^.*-pkg-db: *\(.*\)$/\1/p') ) 29 | databases=( "${databases[@]/#/--package-db }" ) 30 | 31 | echo "Using resolver ${resolver}" 32 | echo "Using databases ${databases[@]}" 33 | 34 | $path/travis-timeout \ 35 | stack --no-terminal --skip-ghc-check --stack-yaml stack-$GHCVER.yaml install \ 36 | standalone-haddock \ 37 | hscolour 38 | 39 | $path/travis-timeout \ 40 | stack --no-terminal --skip-ghc-check --stack-yaml stack-$GHCVER.yaml build \ 41 | credentials 42 | 43 | $path/travis-timeout \ 44 | standalone-haddock --hyperlink-source -o $output \ 45 | ${databases[@]} \ 46 | credentials 47 | 48 | { 49 | cat <<-HTML 50 | 51 |

Credentials Library Documentation

52 |

Build #${BUILD_NUMBER}, GHC ${GHCVER}

53 |
    54 | HTML 55 | 56 | for dir in $(ls -d $output/*/); do 57 | file=$(basename $dir) 58 | cat <<-HTML 59 |
  • ${file}
  • 60 | HTML 61 | done 62 | 63 | cat <<-HTML 64 |
65 | HTML 66 | } > $output/index.html 67 | 68 | set -x 69 | 70 | cd $output 71 | 72 | git init 73 | 74 | git config user.name "Brendan Hay" 75 | git config user.email "brendan.g.hay@gmail.com" 76 | 77 | git add . 78 | git commit -m "Travis CI documentation deploy $BUILD_NUMBER" 79 | 80 | git push --force --quiet "https://${DOC_TOKEN}@${DOC_REF}" master:gh-pages > /dev/null 2>&1 81 | -------------------------------------------------------------------------------- /credentials-cli/credentials-cli.cabal: -------------------------------------------------------------------------------- 1 | name: credentials-cli 2 | version: 0.0.2 3 | synopsis: Secure Credentials Administration 4 | homepage: https://github.com/brendanhay/credentials 5 | license: OtherLicense 6 | license-file: LICENSE 7 | author: Brendan Hay 8 | maintainer: Brendan Hay 9 | copyright: Copyright (c) 2015-2016 Brendan Hay 10 | category: Network, AWS, Security, Console 11 | build-type: Simple 12 | cabal-version: >= 1.10 13 | 14 | description: 15 | @credentials@ is a console application used to administer secure credentials 16 | via the library of the same name. 17 | . 18 | You can read more about use-cases and prerequisites . 19 | 20 | source-repository head 21 | type: git 22 | location: git://github.com/brendanhay/credentials.git 23 | 24 | executable credentials 25 | default-language: Haskell2010 26 | hs-source-dirs: src 27 | main-is: Main.hs 28 | 29 | ghc-options: -Wall -threaded 30 | 31 | other-modules: 32 | Credentials.CLI.Format 33 | , Credentials.CLI.IO 34 | , Credentials.CLI.Options 35 | , Credentials.CLI.Types 36 | , Credentials.CLI.Types.Protocol 37 | 38 | build-depends: 39 | aeson >= 0.8 40 | , aeson-pretty >= 0.7.2 41 | , amazonka >= 1.3.7 42 | , amazonka-core >= 1.3.7 43 | , amazonka-dynamodb >= 1.3.6 44 | , attoparsec >= 0.11 45 | , base >= 4.7 && < 5 46 | , bytestring >= 0.10 47 | , conduit >= 1.2 48 | , conduit-extra >= 1.1 49 | , cryptonite >= 0.10 50 | , credentials >= 0.0.1 && < 0.1 51 | , exceptions >= 0.6 52 | , lens >= 4.4 53 | , mmorph >= 1 54 | , mtl >= 2.2.1 55 | , optparse-applicative >= 0.12 56 | , resourcet >= 1.1 57 | , text >= 0.11 58 | , transformers-base >= 0.4 59 | , unordered-containers >= 0.2.5 60 | , uri-bytestring >= 0.2.2 -------------------------------------------------------------------------------- /credentials-cli/src/Credentials/CLI/IO.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ExtendedDefaultRules #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | {-# OPTIONS_GHC -fno-warn-type-defaults #-} 5 | 6 | -- | 7 | -- Module : Credentials.CLI.IO 8 | -- Copyright : (c) 2015-2016 Brendan Hay 9 | -- License : Mozilla Public License, v. 2.0. 10 | -- Maintainer : Brendan Hay 11 | -- Stability : provisional 12 | -- Portability : non-portable (GHC extensions) 13 | -- 14 | module Credentials.CLI.IO where 15 | 16 | import Control.Arrow 17 | import Control.Monad.Reader 18 | 19 | import Credentials.CLI.Format 20 | import Credentials.CLI.Types 21 | 22 | import Data.Aeson (ToJSON (..)) 23 | import Data.Aeson.Encode 24 | import Data.Aeson.Encode.Pretty 25 | import Data.ByteString.Builder (Builder, hPutBuilder, stringUtf8) 26 | import Data.Char (isSpace, toLower) 27 | import Data.Functor.Identity (runIdentity) 28 | import Data.Monoid 29 | 30 | import Network.AWS.Data 31 | 32 | import Options.Applicative.Help.Pretty 33 | 34 | import System.Exit 35 | import System.IO 36 | 37 | import qualified Data.ByteString.Builder as Build 38 | 39 | default (Builder) 40 | 41 | data Agree 42 | = Yes 43 | | No 44 | | What String 45 | 46 | quit :: ToLog a => Int -> a -> IO () 47 | quit n m = err m >> exitWith (ExitFailure n) 48 | 49 | err :: (MonadIO m, ToLog a) => a -> m () 50 | err x = liftIO $ Build.hPutBuilder stderr ("Error!:\n " <> build x <> "\n") 51 | 52 | says :: ToLog a => a -> App () 53 | says x = say (build x <> "\n") 54 | 55 | say :: ToLog a => a -> App () 56 | say x = do 57 | f <- asks format 58 | when (f == Print) $ 59 | liftIO $ hPutBuilder stderr (build x) 60 | 61 | emit :: Result -> App () 62 | emit r = do 63 | (f, s) <- asks (format &&& store) 64 | let e = Emit (runIdentity s) r 65 | liftIO . hPutBuilder stdout $ 66 | case f of 67 | Pretty -> build (encodePretty e) <> "\n" 68 | JSON -> encodeToBuilder (toJSON e) 69 | Echo -> build r 70 | Print -> stringUtf8 71 | (displayS (renderPretty 0.4 80 (pretty e)) "") <> "\n" 72 | 73 | prompt :: Force -> App () -> App () 74 | prompt NoPrompt io = says "Running ..." >> io 75 | prompt Prompt io = do 76 | say " -> Proceed? [y/n]: " 77 | a <- agree 78 | case a of 79 | Yes -> says "Running ..." >> io 80 | No -> says "Cancelling ..." 81 | What w -> says $ build w <> ", what? Cancelling ..." 82 | 83 | agree :: App Agree 84 | agree = do 85 | r <- map toLower . filter (not . isSpace) <$> liftIO getLine 86 | return $! case r of 87 | "yes" -> Yes 88 | "y" -> Yes 89 | "no" -> No 90 | "n" -> No 91 | "" -> No 92 | x -> What x 93 | -------------------------------------------------------------------------------- /credentials/credentials.cabal: -------------------------------------------------------------------------------- 1 | name: credentials 2 | version: 0.0.2 3 | synopsis: Secure Credentials Storage and Distribution 4 | homepage: https://github.com/brendanhay/credentials 5 | license: OtherLicense 6 | license-file: LICENSE 7 | author: Brendan Hay 8 | maintainer: Brendan Hay 9 | copyright: Copyright (c) 2015-2016 Brendan Hay 10 | category: Network, AWS, Security 11 | build-type: Simple 12 | extra-source-files: CHANGELOG.md 13 | cabal-version: >= 1.10 14 | 15 | description: 16 | This library provides a unified interface for managing secure, shared credentials. 17 | It uses Amazon Key Management Service (KMS) for master key management, locally 18 | encrypts and decrypts secrets, which are then stored in any of the supported 19 | storage backends. (Currently DynamoDB.) 20 | . 21 | The use-case is to avoid storing sensitive information such as passwords and 22 | connection strings in plaintext in places such as source control or on 23 | developers' machines. Instead you can securely administer and distribute 24 | secrets, leveraging Amazon's IAM policies for access control and permissions to 25 | ensure limited read-only permissions from production/deployed hosts. 26 | You can embed this library into projects such as web applications to securely 27 | retrieve sensitive information such as database passwords or private keys on startup. 28 | . 29 | A complementary CLI for management of credentials can be installed via 30 | . 31 | . 32 | You can read more about other use-cases and prerequisites . 33 | 34 | source-repository head 35 | type: git 36 | location: git://github.com/brendanhay/credentials.git 37 | 38 | library 39 | default-language: Haskell2010 40 | hs-source-dirs: src 41 | 42 | ghc-options: -Wall 43 | 44 | exposed-modules: 45 | Credentials 46 | , Credentials.KMS 47 | , Credentials.DynamoDB 48 | , Credentials.DynamoDB.Item 49 | , Credentials.Types 50 | 51 | build-depends: 52 | aeson >= 0.8 53 | , amazonka >= 1.3.7 54 | , amazonka-core >= 1.3.7 55 | , amazonka-dynamodb >= 1.3.7 56 | , amazonka-kms >= 1.3.7 57 | , base >= 4.7 && < 5 58 | , bytestring >= 0.10 59 | , conduit >= 1.2 60 | , cryptonite >= 0.10 61 | , exceptions >= 0.6 62 | , lens >= 4.4 63 | , memory >= 0.11 64 | , retry >= 0.7.0.1 65 | , semigroups >= 0.6 66 | , text >= 0.11 67 | , time >= 1.4 68 | , transformers >= 0.3 69 | , unordered-containers >= 0.2.5 70 | 71 | test-suite tests 72 | type: exitcode-stdio-1.0 73 | default-language: Haskell2010 74 | hs-source-dirs: test 75 | main-is: Main.hs 76 | 77 | ghc-options: -Wall -threaded 78 | 79 | other-modules: 80 | 81 | build-depends: 82 | base 83 | , credentials 84 | -------------------------------------------------------------------------------- /credentials-cli/src/Credentials/CLI/Options.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | -- | 4 | -- Module : Credentials.CLI.Options 5 | -- Copyright : (c) 2015-2016 Brendan Hay 6 | -- License : Mozilla Public License, v. 2.0. 7 | -- Maintainer : Brendan Hay 8 | -- Stability : provisional 9 | -- Portability : non-portable (GHC extensions) 10 | -- 11 | module Credentials.CLI.Options where 12 | 13 | import Credentials.CLI.Types 14 | 15 | import Data.Bifunctor 16 | import Data.List (foldl') 17 | import Data.Maybe (isJust) 18 | 19 | import Network.AWS.Data 20 | import Network.AWS.Data.Text 21 | 22 | import Options.Applicative hiding (optional) 23 | import Options.Applicative.Help hiding (string) 24 | 25 | import qualified Data.Text as Text 26 | import qualified Options.Applicative as Opt 27 | 28 | data Fact 29 | = Required 30 | | Optional 31 | | Default 32 | 33 | -- | Setup an option with formatted help text. 34 | describe :: Text -- ^ The options' description. 35 | -> Maybe Doc -- ^ The help body (title/footer in tabular). 36 | -> Fact 37 | -> Mod OptionFields a 38 | describe title body r = helpDoc . Just $ wrap title <> doc <> line 39 | where 40 | doc | Just b <- body = pad (maybe b (b .$.) foot) 41 | | otherwise = maybe mempty pad foot 42 | 43 | foot = case r of 44 | Required -> Just ("This is" <+> bold "required.") 45 | Optional -> Just ("This is" <+> bold "optional.") 46 | Default -> Nothing 47 | 48 | pad = mappend line . indent 2 49 | 50 | -- | Setup a tabular list of possible values for an option, 51 | -- a default value, and an auto-completer. 52 | completes :: ToText a 53 | => Text -- ^ The options' description. 54 | -> Text -- ^ A title for the values. 55 | -> [(a, String)] -- ^ Possible values and their documentation. 56 | -> Maybe a -- ^ A default value. 57 | -> Maybe Text -- ^ Footer contents. 58 | -> Mod OptionFields a 59 | completes title note xs x foot = doc <> completeWith (map fst ys) 60 | where 61 | doc = defaults title note ys x foot 62 | ys = map (first string) xs 63 | 64 | -- | Construct a tabular representation displaying the default values, 65 | -- without using ToText for the tabular values. 66 | defaults :: ToText a 67 | => Text 68 | -> Text 69 | -> [(String, String)] 70 | -> Maybe a 71 | -> Maybe Text 72 | -> Mod OptionFields a 73 | defaults title note xs x foot = 74 | describe title (Just doc) Default <> maybe mempty value x 75 | where 76 | doc = maybe table (table .$.) (wrap <$> foot) 77 | table = wrap note 78 | .$. indent 2 rows 79 | $$$ ( case x of 80 | Nothing -> mempty 81 | Just y -> "Defaults to " <> bold (text (string y)) <> "." 82 | ) 83 | 84 | ($$$) | isJust x = (.$.) 85 | | otherwise = mappend 86 | 87 | len = maximum (map (length . fst) xs) 88 | 89 | rows | [r] <- xs = f r 90 | | r:rs <- xs = foldl' (.$.) (f r) (map f rs) 91 | | otherwise = mempty 92 | where 93 | f (k, v) = "-" <+> bold (text k) <+> indent (len - length k) ts 94 | where 95 | ts | null v = mempty 96 | | otherwise = tupled [text v] 97 | 98 | require :: (Fact -> a) -> a 99 | require f = f Required 100 | 101 | optional :: Alternative f => (Fact -> f a) -> f (Maybe a) 102 | optional f = Opt.optional (f Optional) 103 | 104 | textOption :: FromText a => Mod OptionFields a -> Parser a 105 | textOption = option (eitherReader (fromText . Text.pack)) 106 | 107 | wrap :: Text -> Doc 108 | wrap = extractChunk . paragraph . Text.unpack 109 | -------------------------------------------------------------------------------- /credentials/src/Credentials.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | {-# LANGUAGE NoImplicitPrelude #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE RankNTypes #-} 6 | 7 | -- | 8 | -- Module : Credentials 9 | -- Copyright : (c) 2015-2016 Brendan Hay 10 | -- License : Mozilla Public License, v. 2.0. 11 | -- Maintainer : Brendan Hay 12 | -- Stability : provisional 13 | -- Portability : non-portable (GHC extensions) 14 | -- 15 | -- This module provides a common interface for operating on your shared 16 | -- credentials. 17 | module Credentials 18 | ( 19 | -- * Usage 20 | -- $usage 21 | 22 | -- * Operations 23 | insert 24 | , select 25 | , delete 26 | , truncate 27 | , revisions 28 | , setup 29 | , teardown 30 | 31 | -- * KMS 32 | , KeyId (..) 33 | , defaultKeyId 34 | 35 | -- * DynamoDB 36 | , DynamoTable (..) 37 | , defaultTable 38 | 39 | -- * Errors 40 | , CredentialError (..) 41 | , AsCredentialError (..) 42 | 43 | -- * Types 44 | , Name (..) 45 | , Revision (..) 46 | , Context (..) 47 | , Setup (..) 48 | ) where 49 | 50 | import Credentials.DynamoDB 51 | import Credentials.Types 52 | 53 | -- $usage 54 | -- To use the library, make sure you have met the following prerequisites: 55 | -- 56 | -- * You have a master key in KMS. You can create this under Identity and Access 57 | -- Management > Encryption Keys, in the AWS developer console. 58 | -- 59 | -- * Your AWS access credentials are available where 60 | -- can find them. This 61 | -- will be automatic if you are running on an EC2 host, otherwise 62 | -- the file, as @AWS_ACCESS_KEY_ID@ and 63 | -- @AWS_SECRET_ACCESS_KEY@ environment variables need to be configured. 64 | -- 65 | -- Since all of the credentials operations are constrained by a 'MonadAWS' context, 66 | -- running them is identical to that of [amazonka](https://hackage.haskell.org/package/amazonka), 67 | -- which you will also need to add to your @build-depends@ section of your project's cabal file. 68 | -- 69 | -- > {-# LANGUAGE OverloadedStrings #-} 70 | -- > 71 | -- > import Credentials 72 | -- > import Control.Lens 73 | -- > import Data.ByteString (ByteString) 74 | -- > import Network.AWS 75 | -- > import System.IO 76 | -- > 77 | -- > example :: IO (ByteString, Revision) 78 | -- > example = do 79 | -- > -- A new 'Logger' to replace the default noop logger is created, 80 | -- > -- which will print AWS debug information and errors to stdout. 81 | -- > lgr <- newLogger Debug stdout 82 | -- > 83 | -- > -- A new amazonka 'Env' is created, which auto-discovers the 84 | -- > -- underlying host credentials. 85 | -- > env <- newEnv Frankfurt Discover 86 | -- > 87 | -- > let table = "dynamo-table-name" 88 | -- > key = "kms-key-alias" 89 | -- > name = "credential-name" 90 | -- > 91 | -- > -- We now run the 'AWS' computation with the overriden logger, 92 | -- > -- performing the sequence of credentials operations. 93 | -- > runResourceT . runAWS (env & envLogger .~ lgr) $ do 94 | -- > -- Firstly, we create the DynamoDB table. 95 | -- > -- This is an idempotent operation but since it makes remote API calls, 96 | -- > -- it's recommended you only run this once via the CLI. 97 | -- > Credentials.setup table 98 | -- > 99 | -- > -- Then we insert a credential\'s value, for a given name. 100 | -- > -- Encryption is handled transparently and the resulting revision 101 | -- > -- is returned. 102 | -- > _ <- Credentials.insert key mempty name "a-super-secret-value" table 103 | -- > 104 | -- > -- Selecting the credential by name, and specifying 'Nothing' for the 105 | -- > -- revision results in the latest credential revision being returned. 106 | -- > Credentials.select mempty name Nothing table 107 | -------------------------------------------------------------------------------- /credentials-cli/src/Credentials/CLI/Format.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ExtendedDefaultRules #-} 2 | {-# LANGUAGE FlexibleContexts #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE TupleSections #-} 6 | {-# LANGUAGE UndecidableInstances #-} 7 | 8 | {-# OPTIONS_GHC -fno-warn-type-defaults #-} 9 | 10 | -- | 11 | -- Module : Credentials.CLI.Format 12 | -- Copyright : (c) 2015-2016 Brendan Hay 13 | -- License : Mozilla Public License, v. 2.0. 14 | -- Maintainer : Brendan Hay 15 | -- Stability : provisional 16 | -- Portability : non-portable (GHC extensions) 17 | -- 18 | module Credentials.CLI.Format where 19 | 20 | import Credentials 21 | import Credentials.CLI.Types 22 | 23 | import Data.Aeson (ToJSON (..), object, (.=)) 24 | import Data.Bifunctor 25 | import Data.ByteString (ByteString) 26 | import Data.List (foldl', intersperse) 27 | import Data.List.NonEmpty (NonEmpty (..)) 28 | import Data.Monoid 29 | 30 | import Network.AWS.Data 31 | 32 | import Options.Applicative.Help hiding (list, string) 33 | 34 | import qualified Data.Text as Text 35 | 36 | data Status 37 | = Deleted 38 | | Truncated 39 | 40 | instance ToLog Status where 41 | build = build . toText 42 | 43 | instance ToText Status where 44 | toText = \case 45 | Deleted -> "deleted" 46 | Truncated -> "truncated" 47 | 48 | data Emit = Emit { store' :: Store, result :: Result } 49 | 50 | instance ToJSON Emit where 51 | toJSON (Emit s r) = object [toText s .= r] 52 | 53 | instance Pretty Emit where 54 | pretty (Emit s r) = doc s <> char ':' .$. indent 2 (pretty r) 55 | 56 | data Result 57 | = SetupR Setup 58 | | TeardownR 59 | | InsertR Name Revision 60 | | SelectR Name Revision ByteString 61 | | DeleteR Name Revision 62 | | TruncateR Name 63 | | ListR [(Name, NonEmpty Revision)] 64 | 65 | instance ToLog Result where 66 | build = \case 67 | SetupR s -> build s 68 | TeardownR -> build Deleted 69 | InsertR _ r -> build r 70 | SelectR _ _ v -> build v 71 | DeleteR {} -> build Deleted 72 | TruncateR {} -> build Truncated 73 | ListR rs -> foldMap f rs 74 | where 75 | f (n, v :| vs) = 76 | build n % "," % mconcat (intersperse "," $ map build (v:vs)) % "\n" 77 | 78 | instance ToJSON Result where 79 | toJSON = \case 80 | SetupR s -> object ["status" =~ s] 81 | TeardownR -> object ["status" =~ Deleted] 82 | InsertR n r -> object ["name" =~ n, "revision" =~ r] 83 | SelectR n r v -> object ["name" =~ n, "revision" =~ r, "secret" =~ toBS v] 84 | DeleteR n r -> object ["name" =~ n, "revision" =~ r, "status" =~ Deleted] 85 | TruncateR n -> object ["name" =~ n, "status" =~ Truncated] 86 | ListR rs -> object (map go rs) 87 | where 88 | k =~ v = k .= toText v 89 | 90 | go (n, v :| vs) = toText n .= map toText (v:vs) 91 | 92 | instance Pretty Result where 93 | pretty = \case 94 | SetupR s -> stat s 95 | TeardownR -> stat Deleted 96 | InsertR n r -> name n .$. rev r 97 | SelectR n r v -> name n .$. rev r .$. val v 98 | DeleteR n r -> name n .$. rev r .$. stat Deleted 99 | TruncateR n -> name n .$. stat Truncated 100 | ListR rs -> list rs 101 | where 102 | name n = "name:" <+> doc n 103 | rev r = "revision:" <+> doc r 104 | stat s = "status:" <+> doc s 105 | val v = "secret:" <+> doc (toBS v) 106 | 107 | list [] = mempty 108 | list (r:rs) = foldl' (.$.) (f r) (map f rs) 109 | where 110 | f (n, v :| vs) = doc n <> ":" .$. 111 | indent 2 (extractChunk (revs v vs)) 112 | 113 | revs v vs = table $ (v, "# latest") : map (,mempty) vs 114 | 115 | table [] = mempty 116 | table xs = pure $ vcat 117 | [indent 2 (fillBreak n (item k) <+> v) | (k, v) <- ys] 118 | where 119 | n = maximum (map (Text.length . fst) ys) + 2 120 | ys = map (first toText) xs 121 | 122 | item x = "-" <+> doc x 123 | 124 | doc :: ToText a => a -> Doc 125 | doc = text . string 126 | -------------------------------------------------------------------------------- /credentials/src/Credentials/KMS.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE BangPatterns #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | 7 | -- | 8 | -- Module : Credentials.KMS 9 | -- Copyright : (c) 2015-2016 Brendan Hay 10 | -- License : Mozilla Public License, v. 2.0. 11 | -- Maintainer : Brendan Hay 12 | -- Stability : provisional 13 | -- Portability : non-portable (GHC extensions) 14 | -- 15 | -- Encryption and decryption of local data, by using a wrapped key mechanism 16 | -- and master keys stored in KMS. 17 | -- 18 | -- See the "Credentials" module for usage information. 19 | module Credentials.KMS 20 | ( encrypt 21 | , decrypt 22 | ) where 23 | 24 | import Control.Exception.Lens (catching_, handler) 25 | import Control.Lens hiding (Context) 26 | import Control.Monad 27 | import Control.Monad.Catch (Exception, MonadThrow (..), catches) 28 | 29 | import Credentials.Types 30 | 31 | import Crypto.Cipher.AES (AES256) 32 | import Crypto.Cipher.Types (nullIV) 33 | import Crypto.Error 34 | import Crypto.MAC.HMAC (HMAC (..), hmac) 35 | 36 | import Data.ByteArray.Encoding (Base (Base16), convertToBase) 37 | import Data.ByteString (ByteString) 38 | import Data.Text (Text) 39 | import Data.Typeable (Typeable) 40 | 41 | import Network.AWS 42 | import Network.AWS.Data 43 | import Network.AWS.Error (hasCode, hasStatus) 44 | import Network.AWS.KMS hiding (decrypt, encrypt) 45 | 46 | import Numeric.Natural (Natural) 47 | 48 | import qualified Crypto.Cipher.Types as Cipher 49 | import qualified Data.ByteString as BS 50 | import qualified Data.HashMap.Strict as Map 51 | import qualified Data.Text as Text 52 | import qualified Network.AWS.KMS as KMS 53 | 54 | -- | Encrypt a plaintext 'ByteString' with the given master key and 55 | -- encryption context. The 'Name' is used to annotate error messages. 56 | -- 57 | -- The wrapped data encryption key, ciphertext, and HMAC SHA256 are returned 58 | -- if no error occurs. 59 | encrypt :: (MonadAWS m, Typeable m) 60 | => KeyId 61 | -> Context 62 | -> Name 63 | -> ByteString 64 | -> m Encrypted 65 | encrypt key ctx name plaintext = do 66 | let rq = generateDataKey (toText key) 67 | & gdkNumberOfBytes ?~ keyLength 68 | & gdkEncryptionContext .~ fromContext ctx 69 | 70 | rs <- catches (send rq) 71 | [ handler (_ServiceError . hasStatus 400 . hasCode "NotFound") $ 72 | throwM . MasterKeyMissing key . fmap toText . _serviceMessage 73 | , handler _NotFoundException $ 74 | throwM . MasterKeyMissing key . fmap toText . _serviceMessage 75 | ] 76 | 77 | let (dataKey, hmacKey) = splitKey (rs ^. gdkrsPlaintext) 78 | failure = EncryptFailure ctx name 79 | 80 | aes :: AES256 <- cryptoError failure (Cipher.cipherInit dataKey) 81 | 82 | let !wrappedKey = rs ^. gdkrsCiphertextBlob 83 | !ciphertext = Cipher.ctrCombine aes nullIV plaintext 84 | !digest = hmac hmacKey ciphertext 85 | 86 | pure $! Encrypted{..} 87 | 88 | -- | Decrypt ciphertext using the given encryption context, and wrapped 89 | -- data encryption key. The HMAC SHA256 is recalculated and compared for 90 | -- message integrity. The 'Name' is used to annotate error messages. 91 | -- 92 | -- The resulting unencrypted plaintext 'ByteString' is returned if no error occurs. 93 | decrypt :: MonadAWS m 94 | => Context 95 | -> Name 96 | -> Encrypted 97 | -> m ByteString 98 | decrypt ctx name Encrypted{..} = do 99 | let rq = KMS.decrypt wrappedKey 100 | & decEncryptionContext .~ fromContext ctx 101 | 102 | rs <- catching_ _InvalidCiphertextException (send rq) $ 103 | throwM . DecryptFailure ctx name $ 104 | if Map.null (fromContext ctx) 105 | then "Could not decrypt stored key using KMS. \ 106 | \The credential may require an ecryption context." 107 | else "Could not decrypt stored key using KMS. \ 108 | \The provided encryption context may not match the one \ 109 | \used when the credential was stored." 110 | 111 | -- Decrypted plaintext data. This value may not be returned if the customer 112 | -- master key is not available or if you didn't have permission to use it. 113 | plaintextKey <- 114 | case rs ^. drsPlaintext of 115 | Nothing -> throwM $ 116 | DecryptFailure ctx name 117 | "Decrypted plaintext data not available from KMS." 118 | Just t -> pure t 119 | 120 | let (dataKey, hmacKey) = splitKey plaintextKey 121 | expect = hmac hmacKey (toBS ciphertext) 122 | failure = DecryptFailure ctx name 123 | 124 | unless (expect == digest) $ 125 | throwM (IntegrityFailure name (encodeHex expect) (encodeHex digest)) 126 | 127 | aes :: AES256 <- cryptoError failure (Cipher.cipherInit dataKey) 128 | 129 | pure $! Cipher.ctrCombine aes nullIV (toBS ciphertext) 130 | 131 | splitKey :: ByteString -> (ByteString, ByteString) 132 | splitKey = BS.splitAt 32 133 | 134 | keyLength :: Natural 135 | keyLength = 64 136 | 137 | cryptoError :: (MonadThrow m, Exception e) 138 | => (Text -> e) 139 | -> CryptoFailable a 140 | -> m a 141 | cryptoError f = onCryptoFailure (throwM . f . Text.pack . show) pure 142 | 143 | encodeHex :: HMAC a -> ByteString 144 | encodeHex = convertToBase Base16 145 | -------------------------------------------------------------------------------- /credentials/src/Credentials/DynamoDB/Item.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleInstances #-} 2 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE RecordWildCards #-} 5 | {-# LANGUAGE ScopedTypeVariables #-} 6 | 7 | -- | 8 | -- Module : Credentials.DynamoDB.Item 9 | -- Copyright : (c) 2013-2015-2016 Brendan Hay 10 | -- License : Mozilla Public License, v. 2.0. 11 | -- Maintainer : Brendan Hay 12 | -- Stability : provisional 13 | -- Portability : non-portable (GHC extensions) 14 | -- 15 | -- This module contains the schema that is used by "Credentials.DynamoDB" to 16 | -- serialise encryption parameters to DynamoDB items. 17 | module Credentials.DynamoDB.Item where 18 | 19 | import Control.Lens (set, view, (&), (.~)) 20 | import Control.Monad ((>=>)) 21 | import Control.Monad.Catch (MonadThrow (..)) 22 | 23 | import Credentials.Types 24 | 25 | import Crypto.Hash (SHA256, digestFromByteString) 26 | import Crypto.MAC.HMAC (HMAC (..)) 27 | 28 | import Data.ByteArray.Encoding (Base (Base16), convertFromBase, convertToBase) 29 | import Data.ByteString (ByteString) 30 | import Data.HashMap.Strict (HashMap) 31 | import Data.Monoid ((<>)) 32 | import Data.Text (Text) 33 | 34 | import Network.AWS.Data 35 | import Network.AWS.DynamoDB 36 | 37 | import qualified Data.HashMap.Strict as Map 38 | import qualified Data.Text as Text 39 | import qualified Data.Text.Encoding as Text 40 | 41 | padding :: Text 42 | padding = Text.replicate 19 "0" 43 | 44 | -- | The DynamoDB field used for optimistic locking. 45 | -- 46 | -- Serialisation of 'Version' handles left-padding to support 47 | -- consistent lexicographic ordering when used as a range in DynamoDB. 48 | newtype Version = Version Integer 49 | deriving (Eq, Ord, Num, FromText, ToText) 50 | 51 | equals :: Item a => a -> HashMap Text Condition 52 | equals = Map.map (\x -> condition EQ' & cAttributeValueList .~ [x]) . toItem 53 | 54 | nameField, revisionField, versionField, wrappedKeyField, 55 | ciphertextField, digestField :: Text 56 | nameField = "name" 57 | revisionField = "revision" 58 | versionField = "version" 59 | wrappedKeyField = "key" 60 | ciphertextField = "contents" 61 | digestField = "hmac" 62 | 63 | class Item a where 64 | -- | Encode an item as a set of attributes including their schema. 65 | toItem :: a -> HashMap Text AttributeValue 66 | 67 | -- | Decode an item from a set of attributes. 68 | parseItem :: HashMap Text AttributeValue -> Either CredentialError a 69 | 70 | -- | Decode an item by throwing a 'CredentialError' exception when an 71 | -- error is encountered. 72 | fromItem :: (MonadThrow m, Item a) => HashMap Text AttributeValue -> m a 73 | fromItem = either throwM pure . parseItem 74 | 75 | instance (Item a, Item b) => Item (a, b) where 76 | toItem (x, y) = toItem x <> toItem y 77 | parseItem m = (,) <$> parseItem m <*> parseItem m 78 | 79 | instance Item Name where 80 | toItem = Map.singleton nameField . toAttr 81 | parseItem = parse nameField 82 | 83 | instance Item Revision where 84 | toItem = Map.singleton revisionField . toAttr 85 | parseItem = parse revisionField 86 | 87 | instance Item Version where 88 | toItem = Map.singleton versionField . toAttr 89 | parseItem = parse versionField 90 | 91 | instance Item Encrypted where 92 | toItem Encrypted{..} = 93 | Map.fromList 94 | [ (wrappedKeyField, toAttr wrappedKey) 95 | , (ciphertextField, toAttr ciphertext) 96 | , (digestField, toAttr digest) 97 | ] 98 | 99 | parseItem m = 100 | Encrypted 101 | <$> parse wrappedKeyField m 102 | <*> parse ciphertextField m 103 | <*> parse digestField m 104 | 105 | parse :: Attribute a 106 | => Text 107 | -> HashMap Text AttributeValue 108 | -> Either CredentialError a 109 | parse k m = 110 | case Map.lookup k m of 111 | Nothing -> Left $ FieldMissing k (Map.keys m) 112 | Just v -> 113 | case parseAttr v of 114 | Nothing -> Left $ FieldInvalid k (show v) 115 | Just x -> Right x 116 | 117 | class Attribute a where 118 | -- | Encode an attribute value. 119 | toAttr :: a -> AttributeValue 120 | 121 | -- | Decode an attribute value. 122 | parseAttr :: AttributeValue -> Maybe a 123 | 124 | instance Attribute Text where 125 | toAttr t = set avS (Just t) attributeValue 126 | parseAttr = view avS 127 | 128 | instance Attribute ByteString where 129 | toAttr bs = set avB (Just bs) attributeValue 130 | parseAttr = view avB 131 | 132 | instance Attribute Name where 133 | toAttr = toAttr . toText 134 | parseAttr = fmap Name . parseAttr 135 | 136 | instance Attribute Revision where 137 | toAttr = toAttr . toBS 138 | parseAttr = fmap Revision . parseAttr 139 | 140 | instance Attribute Integer where 141 | toAttr = toAttr . toText 142 | parseAttr = parseAttr >=> either (const Nothing) Just . fromText 143 | 144 | instance Attribute Version where 145 | toAttr (Version n) = 146 | let x = toText n 147 | y = Text.drop (Text.length x) padding <> x 148 | in toAttr y 149 | parseAttr = fmap Version . parseAttr 150 | 151 | instance Attribute (HMAC SHA256) where 152 | toAttr = toAttr . Text.decodeUtf8 . convertToBase Base16 . hmacGetDigest 153 | parseAttr v = do 154 | t :: Text <- parseAttr v 155 | case convertFromBase Base16 (Text.encodeUtf8 t) of 156 | Left _ -> Nothing 157 | Right bs -> HMAC <$> digestFromByteString (bs :: ByteString) 158 | -------------------------------------------------------------------------------- /credentials-cli/src/Credentials/CLI/Types.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE FlexibleInstances #-} 3 | {-# LANGUAGE GADTs #-} 4 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 5 | {-# LANGUAGE LambdaCase #-} 6 | {-# LANGUAGE OverloadedStrings #-} 7 | {-# LANGUAGE RankNTypes #-} 8 | {-# LANGUAGE ScopedTypeVariables #-} 9 | {-# LANGUAGE StandaloneDeriving #-} 10 | {-# LANGUAGE TupleSections #-} 11 | {-# LANGUAGE TypeFamilies #-} 12 | 13 | -- | 14 | -- Module : Credentials.CLI.Types 15 | -- Copyright : (c) 2015-2016 Brendan Hay 16 | -- License : Mozilla Public License, v. 2.0. 17 | -- Maintainer : Brendan Hay 18 | -- Stability : provisional 19 | -- Portability : non-portable (GHC extensions) 20 | -- 21 | module Credentials.CLI.Types where 22 | 23 | import Control.Monad.Base 24 | import Control.Monad.Catch 25 | import Control.Monad.Morph (hoist) 26 | import Control.Monad.Reader 27 | import Control.Monad.Trans.Resource 28 | 29 | import Credentials 30 | import Credentials.CLI.Types.Protocol 31 | 32 | import Crypto.Random (MonadRandom (..)) 33 | 34 | import Data.ByteString (ByteString) 35 | import Data.ByteString.Builder (Builder) 36 | import Data.Conduit 37 | import Data.Conduit.Lazy 38 | import Data.Data 39 | import Data.Functor.Identity (Identity (..)) 40 | import Data.List (sort) 41 | import Data.Maybe (fromMaybe) 42 | import Data.Text (Text) 43 | 44 | import Network.AWS 45 | import Network.AWS.Data 46 | import Network.AWS.DynamoDB (dynamoDB) 47 | import Network.AWS.Endpoint 48 | 49 | import Options.Applicative 50 | import Options.Applicative.Help.Pretty (Pretty (..), text) 51 | 52 | import URI.ByteString (Authority (..), Host (..), Port (..), Scheme (..), URI, 53 | URIRef (..)) 54 | 55 | import qualified Data.Attoparsec.Text as A 56 | import qualified Data.HashMap.Strict as Map 57 | import qualified Data.Text as Text 58 | import qualified URI.ByteString as URI 59 | 60 | data Force 61 | = NoPrompt 62 | | Prompt 63 | 64 | data Input 65 | = Value !ByteString 66 | | Path !FilePath 67 | 68 | data Mode 69 | = Setup 70 | | Teardown !Force 71 | | List 72 | | Insert !KeyId !Context !Name !Input 73 | | Select !Context !Name !(Maybe Revision) 74 | | Delete !Name !Revision !Force 75 | | Truncate !Name !Force 76 | 77 | data Format 78 | = Pretty 79 | | JSON 80 | | Echo 81 | | Print 82 | deriving (Eq, Show) 83 | 84 | instance ToText Format where 85 | toText = \case 86 | Pretty -> "pretty" 87 | JSON -> "json" 88 | Echo -> "echo" 89 | Print -> "print" 90 | 91 | instance FromText Format where 92 | parser = takeLowerText >>= \case 93 | "pretty" -> pure Pretty 94 | "json" -> pure JSON 95 | "echo" -> pure Echo 96 | e -> fromTextError $ "Failure parsing format from: " <> e 97 | 98 | data Options f = Options 99 | { region :: !(f Region) 100 | , store :: !(f Store) 101 | , format :: !Format 102 | , level :: !LogLevel 103 | } 104 | 105 | deriving instance Show (Options Maybe) 106 | deriving instance Show (Options Identity) 107 | 108 | newtype App a = App { unApp :: ReaderT (Options Identity) AWS a } 109 | deriving 110 | ( Functor 111 | , Applicative 112 | , Monad 113 | , MonadIO 114 | , MonadThrow 115 | , MonadCatch 116 | , MonadMask 117 | , MonadReader (Options Identity) 118 | , MonadBase IO 119 | ) 120 | 121 | instance MonadAWS App where 122 | liftAWS = App . lift 123 | 124 | instance MonadResource App where 125 | liftResourceT = App . liftResourceT 126 | 127 | instance MonadRandom App where 128 | getRandomBytes = liftIO . getRandomBytes 129 | 130 | runApp :: Env -> Options Identity -> App a -> IO a 131 | runApp e c = runResourceT . runAWS e . (`runReaderT` c) . unApp 132 | 133 | runLazy :: Source App a -> App [a] 134 | runLazy = App . lazyConsume . hoist unApp 135 | 136 | data Store = Table URI DynamoTable 137 | 138 | instance FromText Store where 139 | parser = uriParser 140 | 141 | instance FromURI Store where 142 | fromURI u = Table u <$> fromURI u 143 | 144 | instance ToText Store where 145 | toText = toText . URI.serializeURIRef' . \case 146 | Table u _ -> u 147 | 148 | instance Show Store where show = Text.unpack . toText 149 | instance Pretty Store where pretty = text . show 150 | instance ToLog Store where build = build . toText 151 | 152 | defaultOptions :: Options Maybe -> Options Identity 153 | defaultOptions x = 154 | let r = fromMaybe defaultRegion (region x) 155 | s = fromMaybe (defaultStore r) (store x) 156 | in x { region = Identity r 157 | , store = Identity s 158 | } 159 | 160 | defaultRegion :: Region 161 | defaultRegion = Frankfurt 162 | 163 | defaultStore :: Region -> Store 164 | defaultStore r = Table u defaultTable 165 | where 166 | u = URI s (Just a) ("/" <> toBS defaultTable) mempty Nothing 167 | s = Scheme "dynamo" 168 | a = Authority Nothing h (Just p) 169 | h = Host (_endpointHost e) 170 | p = Port (_endpointPort e) 171 | e = defaultEndpoint dynamoDB r 172 | 173 | setStore :: HasEnv a => Options Identity -> a -> a 174 | setStore c = configure f 175 | where 176 | f = case store c of 177 | Identity (Table u _) -> g u dynamoDB 178 | 179 | g u | Just h <- host u = setEndpoint (secure u) h (port u) 180 | | otherwise = id 181 | 182 | (%) :: ToLog a => Builder -> a -> Builder 183 | b % x = b <> build x 184 | 185 | unsafeEnum :: forall a. (Ord a, Data a, ToText a) => [a] 186 | unsafeEnum = sort . map fromConstr . dataTypeConstrs $ dataTypeOf val 187 | where 188 | val :: a 189 | val = undefined 190 | 191 | string :: ToText a => a -> String 192 | string = Text.unpack . toText 193 | 194 | data Pair = Pair Text Text 195 | 196 | instance FromText Pair where 197 | parser = Pair <$> key <*> val 198 | where 199 | key = A.skipSpace *> A.takeWhile1 (/= '=') 200 | val = A.char '=' *> A.takeText 201 | 202 | fromPairs :: Alternative f => f Pair -> f Context 203 | fromPairs f = Context . Map.fromList . map (\(Pair k v) -> (k, v)) <$> many f 204 | -------------------------------------------------------------------------------- /credentials/src/Credentials/Types.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE DefaultSignatures #-} 2 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 3 | {-# LANGUAGE LambdaCase #-} 4 | {-# LANGUAGE OverloadedStrings #-} 5 | {-# LANGUAGE TypeFamilies #-} 6 | 7 | -- | 8 | -- Module : Credentials.Types 9 | -- Copyright : (c) 2015-2016 Brendan Hay 10 | -- License : Mozilla Public License, v. 2.0. 11 | -- Maintainer : Brendan Hay 12 | -- Stability : provisional 13 | -- Portability : non-portable (GHC extensions) 14 | -- 15 | module Credentials.Types where 16 | 17 | import Control.Exception.Lens (exception) 18 | import Control.Lens (Prism', prism) 19 | import Control.Monad.Catch (Exception, SomeException) 20 | 21 | import Crypto.Hash (SHA256) 22 | import Crypto.MAC.HMAC (HMAC) 23 | 24 | import Data.ByteString (ByteString) 25 | import Data.HashMap.Strict (HashMap) 26 | import Data.Text (Text) 27 | import Data.Typeable (Typeable) 28 | 29 | import Network.AWS.Data 30 | 31 | -- | The KMS master key identifier. 32 | newtype KeyId = KeyId Text 33 | deriving (Eq, Ord, Show, FromText, ToText, ToByteString, ToLog) 34 | 35 | -- | The default KMS master key alias. 36 | -- 37 | -- /Value:/ @alias\/credentials@ 38 | defaultKeyId :: KeyId 39 | defaultKeyId = KeyId "alias/credentials" 40 | 41 | -- | A shared/readable name for a secret. 42 | newtype Name = Name Text 43 | deriving (Eq, Ord, Show, FromText, ToText, ToByteString, ToLog) 44 | 45 | -- | An opaque, non-monotonic revision number. 46 | newtype Revision = Revision ByteString 47 | deriving (Eq, Ord, Show, FromText, ToText, ToByteString, ToLog) 48 | 49 | -- | A KMS encryption context. 50 | -- 51 | -- /See:/ KMS 52 | -- documentation for more information. 53 | newtype Context = Context { fromContext :: HashMap Text Text } 54 | deriving (Eq, Show, Monoid) 55 | 56 | -- | The encryption parameters required to perform decryption. 57 | data Encrypted = Encrypted 58 | { wrappedKey :: !ByteString -- ^ The wrapped (encrypted) data encryption key. 59 | , ciphertext :: !ByteString -- ^ The encrypted ciphertext. 60 | , digest :: !(HMAC SHA256) -- ^ HMAC SHA256 digest of the ciphertext. 61 | } 62 | 63 | -- | Denotes idempotency of an action. That is, whether an action resulted 64 | -- in any setup being performed. 65 | data Setup 66 | = Created 67 | | Exists 68 | deriving (Eq, Show) 69 | 70 | instance ToText Setup where 71 | toText = \case 72 | Created -> "created" 73 | Exists -> "exists" 74 | 75 | instance ToLog Setup where 76 | build = build . toText 77 | 78 | data CredentialError 79 | = MasterKeyMissing KeyId (Maybe Text) 80 | -- ^ The specified master key id doesn't exist. 81 | 82 | | IntegrityFailure Name ByteString ByteString 83 | -- ^ The computed HMAC doesn't matched the stored HMAC. 84 | 85 | | EncryptFailure Context Name Text 86 | -- ^ Failure occured during local encryption. 87 | 88 | | DecryptFailure Context Name Text 89 | -- ^ Failure occured during local decryption. 90 | 91 | | StorageMissing Text 92 | -- ^ Storage doesn't exist, or has gone on holiday. 93 | 94 | | StorageFailure Text 95 | -- ^ Some storage pre-condition wasn't met. 96 | -- For example: DynamoDB column size exceeded. 97 | 98 | | FieldMissing Text [Text] 99 | -- ^ Missing field from the storage engine. 100 | 101 | | FieldInvalid Text String 102 | -- ^ Unable to parse field from the storage engine. 103 | 104 | | SecretMissing Name (Maybe Revision) Text 105 | -- ^ Secret with the specified name cannot found. 106 | 107 | | OptimisticLockFailure Name Revision Text 108 | -- ^ Attempting to insert a revision that already exists. 109 | 110 | deriving (Eq, Show, Typeable) 111 | 112 | instance Exception CredentialError 113 | 114 | class AsCredentialError a where 115 | _CredentialError :: Prism' a CredentialError 116 | _MasterKeyMissing :: Prism' a (KeyId, Maybe Text) 117 | _IntegrityFailure :: Prism' a (Name, ByteString, ByteString) 118 | _EncryptFailure :: Prism' a (Context, Name, Text) 119 | _DecryptFailure :: Prism' a (Context, Name, Text) 120 | _StorageMissing :: Prism' a Text 121 | _StorageFailure :: Prism' a Text 122 | _FieldMissing :: Prism' a (Text, [Text]) 123 | _FieldInvalid :: Prism' a (Text, String) 124 | _SecretMissing :: Prism' a (Name, Maybe Revision, Text) 125 | _OptimisticLockFailure :: Prism' a (Name, Revision, Text) 126 | 127 | _MasterKeyMissing = (.) _CredentialError _MasterKeyMissing 128 | _IntegrityFailure = (.) _CredentialError _IntegrityFailure 129 | _EncryptFailure = (.) _CredentialError _EncryptFailure 130 | _DecryptFailure = (.) _CredentialError _DecryptFailure 131 | _StorageMissing = (.) _CredentialError _StorageMissing 132 | _StorageFailure = (.) _CredentialError _StorageFailure 133 | _FieldMissing = (.) _CredentialError _FieldMissing 134 | _FieldInvalid = (.) _CredentialError _FieldInvalid 135 | _SecretMissing = (.) _CredentialError _SecretMissing 136 | _OptimisticLockFailure = (.) _CredentialError _OptimisticLockFailure 137 | 138 | instance AsCredentialError CredentialError where 139 | _CredentialError = id 140 | 141 | _MasterKeyMissing = prism 142 | (\(key, msg) -> MasterKeyMissing key msg) 143 | (\case 144 | MasterKeyMissing key msg -> Right (key, msg) 145 | x -> Left x) 146 | 147 | _IntegrityFailure = prism 148 | (\(name, a, b) -> IntegrityFailure name a b) 149 | (\case 150 | IntegrityFailure name a b -> Right (name, a, b) 151 | x -> Left x) 152 | 153 | _EncryptFailure = prism 154 | (\(ctx, name, msg) -> EncryptFailure ctx name msg) 155 | (\case 156 | EncryptFailure ctx name msg -> Right (ctx, name, msg) 157 | x -> Left x) 158 | 159 | _DecryptFailure = prism 160 | (\(ctx, name, msg) -> DecryptFailure ctx name msg) 161 | (\case 162 | DecryptFailure ctx name msg -> Right (ctx, name, msg) 163 | x -> Left x) 164 | 165 | _StorageMissing = prism 166 | StorageMissing 167 | (\case 168 | StorageMissing msg -> Right msg 169 | x -> Left x) 170 | 171 | _StorageFailure = prism 172 | StorageFailure 173 | (\case 174 | StorageFailure msg -> Right msg 175 | x -> Left x) 176 | 177 | _FieldMissing = prism 178 | (\(field, found) -> FieldMissing field found) 179 | (\case 180 | FieldMissing field found -> Right (field, found) 181 | x -> Left x) 182 | 183 | _FieldInvalid = prism 184 | (\(field, msg) -> FieldInvalid field msg) 185 | (\case 186 | FieldInvalid field msg -> Right (field, msg) 187 | x -> Left x) 188 | 189 | _SecretMissing = prism 190 | (\(name, rev, msg) -> SecretMissing name rev msg) 191 | (\case 192 | SecretMissing name rev msg -> Right (name, rev, msg) 193 | x -> Left x) 194 | 195 | _OptimisticLockFailure = prism 196 | (\(name, rev, msg) -> OptimisticLockFailure name rev msg) 197 | (\case 198 | OptimisticLockFailure name rev msg -> Right (name, rev, msg) 199 | x -> Left x) 200 | 201 | instance AsCredentialError SomeException where 202 | _CredentialError = exception 203 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /credentials/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /credentials-cli/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Credentials 2 | 3 | [![Build Status](https://travis-ci.org/brendanhay/credentials.svg?branch=develop)](https://travis-ci.org/brendanhay/credentials) 4 | [![Hackage Version](https://img.shields.io/hackage/v/credentials.svg)](http://hackage.haskell.org/package/credentials) 5 | 6 | * [Description](#description) 7 | * [Setup](#setup) 8 | * [Usage](#usage) 9 | - [Library Usage](#library-usage) 10 | - [CLI Usage](#cli-usage) 11 | - [Credential Contexts](#credential-contexts) 12 | - [Credential Revisions](#credential-revisions) 13 | - [IAM Policies](#iam-policies) 14 | * [Security Considerations](#security-considerations) 15 | * [Service Pricing](#service-pricing) 16 | * [Local Development](#local-development) 17 | * [Contribute](#contribute) 18 | * [Licence](#licence) 19 | 20 | 21 | ## Description 22 | 23 | **Warning:** This is an experimental prototype/preview release which is still 24 | in development and not intended for public consumption, caveat emptor! 25 | 26 | The `credentials` library and CLI provides a unified interface for managing secure, shared credentials. 27 | 28 | It uses [Key Management Service (KMS)](http://aws.amazon.com/kms/) for master key management, locally 29 | encrypts and decrypts secrets, which are then stored in any of the supported 30 | storage backends. (Currently [DynamoDB](http://aws.amazon.com/dynamodb/).) 31 | 32 | The use-case is to avoid storing sensitive information such as passwords and 33 | connection strings in plaintext in places such as source control or on 34 | developer machines. Instead you can securely administer and distribute 35 | secrets, leveraging Amazon's IAM policies for access control and permissions to 36 | ensure limited read-only permissions from production/deployed hosts where applicable. 37 | 38 | Please see the [introductory blog post](http://brendanhay.nz/credentials) for more information, 39 | or the Haddock documentation built by CI from the `develop` branch, 40 | which can be found [here](http://brendanhay.nz/credentials-doc/credentials/). 41 | 42 | If Haskell is not your thing, check out [credstash](https://github.com/fugue/credstash), 43 | the Python project that inspired `credentials`. 44 | 45 | 46 | ## Setup 47 | 48 | 1. `stack install credentials-cli` 49 | 2. Create a new key in KMS called "credentials". You can do this under Identity 50 | and Access Management > Encryption Keys in the AWS developer console. 51 | 3. Your AWS access credentials are available where 52 | [amazonka](https://hackage.haskell.org/package/amazonka) can find them. This 53 | will be automatic if you are running on an EC2 host, otherwise the 54 | [~/.aws/credentials](https://blogs.aws.amazon.com/security/post/Tx3D6U6WSFGOK2H/A-New-and-Standardized-Way-to-Manage-Credentials-in-the-AWS-SDKs) 55 | file or `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment 56 | variables need to be configured. 57 | 4. `credentials setup` 58 | 59 | 60 | ## Usage 61 | 62 | The following is an example of using the `credentials` library as a dependency 63 | of your Haskell project. It retrieves a database connection string containing a 64 | sensitive password, when a webserver starts. It's worth pointing out the setup 65 | all pertains to the underlying 66 | [amazonka](https://github.com/brendanhay/credentials) library, since all of 67 | the `credentials` operations run in a `MonadAWS` context. 68 | 69 | ```haskell 70 | {-# LANGUAGE OverloadedStrings #-} 71 | 72 | import Control.Lens 73 | 74 | import Credentials 75 | 76 | import Data.ByteString (ByteString) 77 | 78 | import Network.AWS 79 | import Network.Wai (Application) 80 | import Network.Wai.Handler.Warp (run) 81 | 82 | import System.IO (stdout) 83 | 84 | main :: IO 85 | main = do 86 | -- A new 'Logger' to replace the default noop logger is created, 87 | -- which will print AWS debug information and errors to stdout. 88 | lgr <- newLogger Debug stdout 89 | 90 | -- A new amazonka 'Env' is created, which auto-discovers the 91 | -- underlying host credentials. 92 | env <- newEnv Frankfurt Discover 93 | 94 | let table = Credentials.defaultTable 95 | key = Credentials.defaultKeyId 96 | name = "secret-database-uri" 97 | 98 | -- We now run the 'AWS' computation with the overriden logger, 99 | -- performing sequence credentials operation(s). 100 | -- For 'select', the plaintext and corresponding revision is returned. 101 | (uri, _) <- runResourceT . runAWS (env & envLogger .~ lgr) $ do 102 | -- Selecting the credential by name, and specifying 'Nothing' for the 103 | -- revision results in the latest revision of the credential. 104 | Credentials.select mempty name Nothing table 105 | 106 | -- We can now connect to the database using our sensitive connection URI. 107 | run 3000 (app uri) 108 | 109 | app :: ByteString -> Application 110 | app uri rq f = ... 111 | ``` 112 | 113 | A basic example of using the CLI is as follows: 114 | 115 | ``` 116 | $ credentials setup 117 | Setting up dynamo:///credentials in eu-central-1. 118 | Running ... 119 | dynamo://dynamodb.eu-central-1.amazonaws.com:443/credentials: 120 | status: created 121 | ``` 122 | 123 | ``` 124 | $ credentials insert --name foo --secret "A magical secret." 125 | Writing new revision of foo to dynamo:///credentials in eu-central-1... 126 | dynamo://dynamodb.eu-central-1.amazonaws.com:443/credentials: 127 | name: foo 128 | revision: 82687c4 129 | ``` 130 | 131 | ``` 132 | $ credentials select --name foo 133 | Retrieving foo from dynamo:///credentials in eu-central-1... 134 | dynamo://dynamodb.eu-central-1.amazonaws.com:443/credentials: 135 | name: foo 136 | revision: 82687c4 137 | secret: A magical secret. 138 | ``` 139 | 140 | ``` 141 | $ credentials list 142 | Listing contents of dynamo:///credentials in eu-central-1... 143 | dynamo://dynamodb.eu-central-1.amazonaws.com:443/credentials: 144 | foo: 145 | - 82687c4 # latest 146 | ``` 147 | 148 | Additional means of formatting the output is available, for example the `--format` option 149 | supports `json` (JSON formatting) or `echo` (single shell value) output. 150 | 151 | ### Credential Contexts 152 | 153 | KMS [Encryption Context](http://docs.aws.amazon.com/kms/latest/developerguide/encrypt-context.html) is optionally supported, 154 | which is used to check ciphertext integrity but not stored as part of the encryption parameters. 155 | 156 | This means you need to pass the exact context that was used for encryption, 157 | when decrypting, and allows the use of KMS Key Policy and KMS Grant 158 | conditions. One such use would be to ensure your web and database servers can 159 | read a database password, but your database servers cannot read your web 160 | server's private key. 161 | 162 | For example, supplying encryption context would look as follows: 163 | 164 | ```haskell 165 | $ credentials insert --name hello --secret world \ 166 | --context application=db \ 167 | --context environment=production 168 | 169 | $ credentials select --name hello \ 170 | --context application=db \ 171 | --context environment=production 172 | ``` 173 | 174 | Failure to specify the exact encryption context resullts in an informative error 175 | during decryption. 176 | 177 | ### Credential Revisions 178 | 179 | Credential storage is immutable and the insertion of a credential into storage is append-only. 180 | When a value for an existing credential name is inserted a unique revision is generated which 181 | you can use in combination with the name to uniquely address any credential in the system. 182 | 183 | Rotation of credentials can be performed by simply inserting a new value for 184 | a given credential name, and any system which selects that credential without 185 | a specific revision will always receive the latest revision's value. 186 | 187 | You can also specify the exact revision when selecting a credential to perform pinning. 188 | 189 | ### IAM Policies 190 | 191 | The complete list of AWS KMS and DynamoDB permissions the various operations will 192 | require are: 193 | 194 | * Setup and teardown: 195 | - `CreateTable` 196 | - `DestroyTable` 197 | - `ListTables` 198 | * List credentials and revisions: 199 | - `Scan` 200 | * Insert a new encrypted credential revision: 201 | - `GenerateDataKey` 202 | - `Query` 203 | - `PutItem` 204 | * Select and decrypt an existing credential revision: 205 | - `Decrypt` 206 | - `Query` 207 | * Deletion of a credential revision, or revisions: 208 | - `Query` 209 | - `DeleteItem` 210 | 211 | It's recommended you allow only the minimal set of permissions as your usecase 212 | requires. For example, the following two IAM policies illustrate the minimum 213 | allowable API operations for read/write, and read only access respectively: 214 | 215 | **Read and Write** 216 | 217 | ```json 218 | { 219 | "Version": "2012-10-17", 220 | "Statement": [ 221 | { 222 | "Action": [ 223 | "kms:GenerateDataKey" 224 | ], 225 | "Effect": "Allow", 226 | "Resource": "arn:aws:kms:us-east-1:AWS_ACCOUNT_ID:alias/credentials" 227 | }, 228 | { 229 | "Action": [ 230 | "dynamodb:PutItem", 231 | "dynamodb:Query" 232 | ], 233 | "Effect": "Allow", 234 | "Resource": "arn:aws:dynamodb:us-east-1:AWS_ACCOUNT_ID:table/credentials" 235 | } 236 | ] 237 | } 238 | ``` 239 | 240 | **Read Only** 241 | 242 | ```json 243 | { 244 | "Version": "2012-10-17", 245 | "Statement": [ 246 | { 247 | "Action": [ 248 | "kms:Decrypt" 249 | ], 250 | "Effect": "Allow", 251 | "Resource": "arn:aws:kms:us-east-1:AWS_ACCOUNT_ID:alias/credentials" 252 | }, 253 | { 254 | "Action": [ 255 | "dynamodb:Query" 256 | ], 257 | "Effect": "Allow", 258 | "Resource": "arn:aws:dynamodb:us-east-1:AWS_ACCOUNT_ID:table/credentials" 259 | } 260 | ] 261 | } 262 | ``` 263 | 264 | Remember to replace the use of `AWS_ACCOUNT_ID` above with your own account identifier. 265 | 266 | 267 | ## Security Considerations 268 | 269 | Any IAM user who can read items from the DynamoDB credentials table and can 270 | call KMS `Decrypt`, can obtain the plaintext of stored credentials. 271 | 272 | Similarly when using `credentials` as part of an EC2 deployment, the instance 273 | boundary will be the security boundary as the IAM role credentials are available 274 | via the local EC2 metadata service at `http://169.254.169.254`. If your instance 275 | is compromised and has an IAM role assigned that similarly allows reading items 276 | from the DynamoDB table and calling KMS `Decrypt`, the attacker will likewise be 277 | able to recover credentials. 278 | 279 | If the instance boundary is too coarse, consider using `iptables` or similar to 280 | restrict metadata access to privileged users. 281 | 282 | 283 | ## Service Pricing 284 | 285 | A single master key in KMS costs $1 USD per month. The DynamoDB table throughput 286 | is configured to use 1 provisioned read and 1 provisioned write, so if you are using 287 | less than the free tier limit of 25 reads and 25 writes per second, only the KMS 288 | charges will apply. 289 | 290 | If you are likely to utilise much more than 25 reads/writes per second, you 291 | can estimate your monthly charges by using the [AWS pricing calculator](http://calculator.s3.amazonaws.com/index.html#s=DYNAMODB). 292 | 293 | > TL;DR, $1 USD per month for the predicted usecase. 294 | 295 | 296 | ## Local Development 297 | 298 | You can use both the library and CLI in a limited offline fashion by running 299 | a [DynamoDBLocal](https://aws.amazon.com/blogs/aws/dynamodb-local-for-desktop-development/) 300 | instance using the `script/dynamo-local.sh` script. The CLI commands can then 301 | take a `--uri dynamo://localhost:4000/credentials` parameter to force 302 | communication with the local DynamoDB database. 303 | 304 | Unfortunately there exists no similar mechanism to run KMS locally, currently. 305 | 306 | 307 | ## Contribute 308 | 309 | For any problems, comments, or feedback please create an issue [here on GitHub](https://github.com/brendanhay/credentials/issues). 310 | 311 | 312 | ## Licence 313 | 314 | `credentials` is released under the [Apache License Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). 315 | -------------------------------------------------------------------------------- /credentials/src/Credentials/DynamoDB.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE FlexibleInstances #-} 3 | {-# LANGUAGE GeneralizedNewtypeDeriving #-} 4 | {-# LANGUAGE OverloadedLists #-} 5 | {-# LANGUAGE OverloadedStrings #-} 6 | {-# LANGUAGE RankNTypes #-} 7 | {-# LANGUAGE RecordWildCards #-} 8 | {-# LANGUAGE ScopedTypeVariables #-} 9 | {-# LANGUAGE TupleSections #-} 10 | {-# LANGUAGE TypeFamilies #-} 11 | 12 | -- | 13 | -- Module : Credentials.DynamoDB 14 | -- Copyright : (c) 2015-2016 Brendan Hay 15 | -- License : Mozilla Public License, v. 2.0. 16 | -- Maintainer : Brendan Hay 17 | -- Stability : provisional 18 | -- Portability : non-portable (GHC extensions) 19 | -- 20 | -- Provides the implementation for storage and retrieval of encrypted credentials 21 | -- in DynamoDB. The encryption and decryption is handled by "Credentials.KMS". 22 | -- 23 | -- See the "Credentials" module for usage information. 24 | module Credentials.DynamoDB 25 | ( 26 | -- * Table 27 | DynamoTable (..) 28 | , defaultTable 29 | 30 | -- * Operations 31 | , insert 32 | , select 33 | , delete 34 | , truncate 35 | , revisions 36 | , setup 37 | , teardown 38 | ) where 39 | 40 | import Prelude hiding (truncate) 41 | 42 | import Control.Exception.Lens 43 | import Control.Lens hiding (Context) 44 | import Control.Monad 45 | import Control.Monad.Catch 46 | import Control.Monad.IO.Class 47 | import Control.Retry 48 | 49 | import Credentials.DynamoDB.Item 50 | import Credentials.KMS as KMS 51 | import Credentials.Types 52 | 53 | import Crypto.Hash (Digest, SHA1) 54 | 55 | import Data.ByteArray.Encoding 56 | import Data.ByteString (ByteString) 57 | import Data.Conduit hiding (await) 58 | import Data.List.NonEmpty (NonEmpty (..)) 59 | import Data.Maybe 60 | import Data.Monoid ((<>)) 61 | import Data.Ord 62 | import Data.Text (Text) 63 | import Data.Time.Clock.POSIX 64 | import Data.Typeable 65 | 66 | import Network.AWS 67 | import Network.AWS.Data 68 | import Network.AWS.DynamoDB 69 | 70 | import qualified Crypto.Hash as Crypto 71 | import qualified Data.ByteString as BS 72 | import qualified Data.Conduit as C 73 | import qualified Data.Conduit.List as CL 74 | import qualified Data.HashMap.Strict as Map 75 | import qualified Data.List.NonEmpty as NE 76 | 77 | -- | A DynamoDB table reference. 78 | newtype DynamoTable = DynamoTable { tableName :: Text } 79 | deriving (Eq, Ord, Show, FromText, ToText, ToByteString, ToLog) 80 | 81 | -- | The default DynamoDB table used to store credentials. 82 | -- 83 | -- /Value:/ @credentials@ 84 | defaultTable :: DynamoTable 85 | defaultTable = DynamoTable "credentials" 86 | 87 | -- | Encrypt and insert a new credential revision with the specified name. 88 | -- 89 | -- The newly inserted revision is returned. 90 | insert :: (MonadMask m, MonadAWS m, Typeable m) 91 | => KeyId -- ^ The KMS master key ARN or alias. 92 | -> Context -- ^ The KMS encryption context. 93 | -> Name -- ^ The credential name. 94 | -> ByteString -- ^ The unencrypted plaintext. 95 | -> DynamoTable -- ^ The DynamoDB table. 96 | -> m Revision 97 | insert key ctx name plaintext table = do 98 | ciphertext <- encrypt key ctx name plaintext 99 | catchResourceNotFound table (insertEncrypted name ciphertext table) 100 | 101 | -- | Select an existing credential, optionally specifying the revision. 102 | -- 103 | -- The decrypted plaintext and selected revision are returned. 104 | select :: MonadAWS m 105 | => Context -- ^ The KMS encryption context that was used during insertion. 106 | -> Name -- ^ The credential name. 107 | -> Maybe Revision -- ^ A revision. If 'Nothing', the latest will be selected. 108 | -> DynamoTable -- ^ The DynamoDB table. 109 | -> m (ByteString, Revision) 110 | select ctx name rev table = do 111 | (_, (ciphertext, rev')) <- 112 | catchResourceNotFound table (selectEncrypted name rev table) 113 | (,rev') <$> decrypt ctx name ciphertext 114 | 115 | -- | Delete the specific credential revision. 116 | delete :: MonadAWS m 117 | => Name -- ^ The credential name. 118 | -> Revision -- ^ The revision to delete. 119 | -> DynamoTable -- ^ The DynamoDB table. 120 | -> m () 121 | delete name rev table@DynamoTable{..} = 122 | catchResourceNotFound table $ do 123 | (ver, _) <- selectEncrypted name (Just rev) table 124 | void . send $ 125 | deleteItem tableName 126 | & diKey .~ toItem name <> toItem ver 127 | 128 | -- | Truncate all of a credential's revisions, so that only 129 | -- the latest revision remains. 130 | truncate :: MonadAWS m 131 | => Name -- ^ The credential name. 132 | -> DynamoTable -- ^ The DynamoDB table. 133 | -> m () 134 | truncate name table@DynamoTable{..} = catchResourceNotFound table $ 135 | queryAll $$ CL.mapM_ (deleteMany . view qrsItems) 136 | where 137 | queryAll = 138 | paginate $ 139 | queryByName name table 140 | & qAttributesToGet ?~ nameField :| [versionField] 141 | & qScanIndexForward ?~ True 142 | & qLimit ?~ batchSize 143 | 144 | deleteMany [] = pure () 145 | deleteMany (x:xs) = void . send $ 146 | batchWriteItem 147 | & bwiRequestItems .~ 148 | [ (tableName, deleteKey x :| map deleteKey (batchInit xs)) 149 | ] 150 | 151 | deleteKey k = 152 | writeRequest 153 | & wrDeleteRequest ?~ (deleteRequest & drKey .~ k) 154 | 155 | batchInit xs 156 | | i < n = take (i - 1) xs 157 | | otherwise = xs 158 | where 159 | n = fromIntegral (batchSize - 1) 160 | i = length xs 161 | 162 | batchSize = 50 163 | 164 | -- | Scan the entire credential database, grouping pages of results into 165 | -- unique credential names and their corresponding revisions. 166 | revisions :: MonadAWS m 167 | => DynamoTable -- ^ The DynamoDB table. 168 | -> Source m (Name, NonEmpty Revision) 169 | revisions table = catchResourceNotFound table $ 170 | paginate (scanTable table) 171 | =$= CL.concatMapM (traverse fromItem . view srsItems) 172 | =$= CL.groupOn1 fst 173 | =$= CL.map group 174 | where 175 | group ((name, rev), revs) = (name, desc (rev :| map snd revs)) 176 | 177 | desc :: NonEmpty (Version, Revision) -> NonEmpty Revision 178 | desc = NE.map snd . NE.sortWith (Down . fst) 179 | 180 | -- | Create the credentials database table. 181 | -- 182 | -- The returned idempotency flag can be used to notify configuration 183 | -- management tools such as ansible whether about system state. 184 | setup :: MonadAWS m 185 | => DynamoTable -- ^ The DynamoDB table. 186 | -> m Setup 187 | setup table@DynamoTable{..} = do 188 | p <- exists table 189 | unless p $ do 190 | let iops = provisionedThroughput 1 1 191 | keys = keySchemaElement nameField Hash 192 | :| [keySchemaElement versionField Range] 193 | attr = ctAttributeDefinitions .~ 194 | [ attributeDefinition nameField S 195 | , attributeDefinition versionField S 196 | , attributeDefinition revisionField B 197 | ] 198 | -- FIXME: Only non-key attributes need to be specified 199 | -- in the non-key attributes .. duh. 200 | secn = ctLocalSecondaryIndexes .~ 201 | [ localSecondaryIndex revisionField 202 | (keySchemaElement nameField Hash 203 | :| [keySchemaElement revisionField Range]) 204 | (projection & pProjectionType ?~ All) 205 | ] 206 | void $ send (createTable tableName keys iops & attr & secn) 207 | void $ await tableExists (describeTable tableName) 208 | pure $ 209 | if p 210 | then Exists 211 | else Created 212 | 213 | -- | Delete the credentials database table and all data. 214 | -- 215 | -- /Note:/ Unless you have DynamoDB backups running, this is a completely 216 | -- irrevocable action. 217 | teardown :: MonadAWS m => DynamoTable -> m () 218 | teardown table@DynamoTable{..} = do 219 | p <- exists table 220 | when p $ do 221 | void $ send (deleteTable tableName) 222 | void $ await tableNotExists (describeTable tableName) 223 | 224 | insertEncrypted :: (MonadMask m, MonadAWS m, Typeable m) 225 | => Name 226 | -> Encrypted 227 | -> DynamoTable 228 | -> m Revision 229 | insertEncrypted name encrypted table@DynamoTable{..} = 230 | recovering policy [const cond] write 231 | where 232 | write = const $ do 233 | ver <- maybe 1 (+1) <$> latest name table 234 | rev <- genRevision ver 235 | void . send $ putItem tableName 236 | & piExpected .~ Map.map (const expect) (toItem ver <> toItem rev) 237 | & piItem .~ 238 | toItem name 239 | <> toItem ver 240 | <> toItem rev 241 | <> toItem encrypted 242 | pure rev 243 | 244 | cond = handler_ _ConditionalCheckFailedException (pure True) 245 | 246 | expect = expectedAttributeValue & eavExists ?~ False 247 | 248 | policy = constantDelay 1000 <> limitRetries 5 249 | 250 | selectEncrypted :: (MonadThrow m, MonadAWS m) 251 | => Name 252 | -> Maybe Revision 253 | -> DynamoTable 254 | -> m (Version, (Encrypted, Revision)) 255 | selectEncrypted name rev table@DynamoTable{..} = 256 | send (queryByName name table & revision rev) >>= result 257 | where 258 | result = maybe missing fromItem . listToMaybe . view qrsItems 259 | 260 | missing = throwM $ SecretMissing name rev tableName 261 | 262 | -- If revision is specified, the revision index is used and 263 | -- a consistent read is done. 264 | revision Nothing = id 265 | revision (Just r) = 266 | (qIndexName ?~ revisionField) 267 | . (qKeyConditions <>~ equals r) 268 | . (qConsistentRead ?~ True) 269 | 270 | latest :: (MonadThrow m, MonadAWS m) 271 | => Name 272 | -> DynamoTable 273 | -> m (Maybe Version) 274 | latest name table = do 275 | rs <- send (queryByName name table & qConsistentRead ?~ True) 276 | case listToMaybe (rs ^. qrsItems) of 277 | Nothing -> pure Nothing 278 | Just m -> Just <$> fromItem m 279 | 280 | exists :: MonadAWS m => DynamoTable -> m Bool 281 | exists DynamoTable{..} = paginate listTables 282 | =$= CL.concatMap (view ltrsTableNames) 283 | $$ (isJust <$> findC (== tableName)) 284 | 285 | scanTable :: DynamoTable -> Scan 286 | scanTable DynamoTable{..} = 287 | scan tableName 288 | & sAttributesToGet ?~ nameField :| [versionField, revisionField] 289 | 290 | queryByName :: Name -> DynamoTable -> Query 291 | queryByName name DynamoTable{..} = 292 | query tableName 293 | & qLimit ?~ 1 294 | & qScanIndexForward ?~ False 295 | & qConsistentRead ?~ False 296 | & qKeyConditions .~ equals name 297 | 298 | genRevision :: MonadIO m => Version -> m Revision 299 | genRevision (Version ver) = do 300 | ts <- liftIO getPOSIXTime 301 | let d = Crypto.hash (toBS (show ts) <> toBS ver) :: Digest SHA1 302 | r = BS.take 7 (convertToBase Base16 d) 303 | pure $! Revision r 304 | 305 | findC :: Monad m => (a -> Bool) -> Consumer a m (Maybe a) 306 | findC f = loop 307 | where 308 | loop = C.await >>= maybe (pure Nothing) go 309 | 310 | go x | f x = pure (Just x) 311 | | otherwise = loop 312 | 313 | -- FIXME: Over specified due to the coarseness of _ResourceNotFound. 314 | catchResourceNotFound :: MonadCatch m => DynamoTable -> m b -> m b 315 | catchResourceNotFound DynamoTable{..} = 316 | handling_ _ResourceNotFoundException $ 317 | throwM $ StorageMissing ("Table " <> tableName <> " doesn't exist.") 318 | -------------------------------------------------------------------------------- /credentials-cli/src/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE ExtendedDefaultRules #-} 2 | {-# LANGUAGE LambdaCase #-} 3 | {-# LANGUAGE OverloadedStrings #-} 4 | {-# LANGUAGE RankNTypes #-} 5 | {-# LANGUAGE RecordWildCards #-} 6 | {-# LANGUAGE TupleSections #-} 7 | {-# LANGUAGE TypeFamilies #-} 8 | {-# LANGUAGE ViewPatterns #-} 9 | 10 | {-# OPTIONS_GHC -fno-warn-type-defaults #-} 11 | 12 | -- | 13 | -- Module : Main 14 | -- Copyright : (c) 2015-2016 Brendan Hay 15 | -- License : Mozilla Public License, v. 2.0. 16 | -- Maintainer : Brendan Hay 17 | -- Stability : provisional 18 | -- Portability : non-portable (GHC extensions) 19 | -- 20 | module Main (main) where 21 | 22 | import Prelude hiding (truncate) 23 | 24 | import Control.Exception.Lens 25 | import Control.Lens ((.~), (<&>)) 26 | import Control.Monad.Catch 27 | import Control.Monad.Reader 28 | import Control.Monad.Trans.Resource 29 | 30 | import Credentials 31 | import Credentials.CLI.Format 32 | import Credentials.CLI.IO 33 | import Credentials.CLI.Options 34 | import Credentials.CLI.Types 35 | 36 | import Data.ByteString.Builder (Builder) 37 | import Data.Conduit 38 | import Data.Functor.Identity (Identity (..)) 39 | import Data.Text (Text) 40 | 41 | import Network.AWS 42 | 43 | import Options.Applicative hiding (optional) 44 | import Options.Applicative.Help.Pretty 45 | 46 | import System.IO 47 | 48 | import qualified Control.Applicative as App 49 | import qualified Data.ByteString.Lazy as LBS 50 | import qualified Data.Conduit.Binary as CB 51 | import qualified Data.Conduit.List as CL 52 | 53 | default (Builder, Text) 54 | 55 | main :: IO () 56 | main = do 57 | (defaultOptions -> opt, m) <- 58 | customExecParser (prefs (showHelpOnError <> columns 90)) options 59 | 60 | lgr <- newLogger (level opt) stderr 61 | env <- newEnv (runIdentity (region opt)) Discover 62 | <&> (envLogger .~ lgr) . setStore opt 63 | 64 | catches (runApp env opt (program opt m)) 65 | [ handler _CredentialError (quit 1 . show) 66 | ] 67 | 68 | program :: Options Identity -> Mode -> App () 69 | program Options{ store = Identity store@(Table _ table) 70 | , region = Identity region 71 | , ..} = \case 72 | List -> do 73 | says ("Listing contents of " % store % " in " % region % "...") 74 | runLazy (revisions table) >>= 75 | emit . ListR 76 | 77 | Insert key ctx name input -> do 78 | says ("Writing new revision of " % 79 | name % " to " % store % " in " % region % "...") 80 | rev <- case input of 81 | Value val -> insert key ctx name val table 82 | Path path -> do 83 | sz <- getFileSize path 84 | if sz > 190 * 1024 85 | then throwM $ StorageFailure 86 | "Secret file is larger than allowable storage size." 87 | else do 88 | cs <- liftIO . runResourceT $ 89 | CB.sourceFile path $$ CL.consume 90 | let val = LBS.toStrict (LBS.fromChunks cs) 91 | insert key ctx name val table 92 | emit (InsertR name rev) 93 | 94 | Select ctx name rev -> do 95 | say "Retrieving" 96 | case rev of 97 | Nothing -> pure () 98 | Just r -> say (" revision " % r % " of") 99 | says (" " % name % " from " % store % " in " % region % "...") 100 | (val, rev') <- select ctx name rev table 101 | emit (SelectR name rev' val) 102 | 103 | Delete name rev force -> do 104 | says ("This will delete revision " % 105 | rev % " of " % name % " from " % store % " in " % region % "!") 106 | prompt force $ do 107 | delete name rev table 108 | emit (DeleteR name rev) 109 | 110 | Truncate name force -> do 111 | says ("This will delete all but the latest revision of " % 112 | name % " from " % store % " in " % region % "!") 113 | prompt force $ do 114 | truncate name table 115 | emit (TruncateR name) 116 | 117 | Setup -> do 118 | says ("Setting up " % store % " in " % region % ".") 119 | says "Running ..." 120 | setup table >>= emit . SetupR 121 | 122 | Teardown force -> do 123 | says ("This will delete " % store % " from " % region % "!") 124 | prompt force $ do 125 | teardown table 126 | emit TeardownR 127 | 128 | options :: ParserInfo (Options Maybe, Mode) 129 | options = info (helper <*> modes) (fullDesc <> headerDoc (Just desc)) 130 | where 131 | desc = bold "credentials" 132 | <+> "- Administration tool for managing secure, shared credentials." 133 | 134 | modes = hsubparser $ mconcat 135 | [ mode "list" 136 | (pure List) 137 | "List credential names and their respective revisions." 138 | "This does not perform decryption of any credentials, and can be used \ 139 | \to obtain an overview of the credential names and revisions that \ 140 | \are stored." 141 | 142 | , mode "select" 143 | (Select <$> contextOption <*> nameOption <*> optional revisionOption) 144 | "Fetch and decrypt a specific credential revision." 145 | "Defaults to the latest available revision, if --revision is not specified." 146 | 147 | , mode "insert" 148 | (Insert <$> keyOption <*> contextOption <*> nameOption <*> inputOption) 149 | "Write and encrypt a new credential revision." 150 | "You can supply the secret value as a string with --secret, or as \ 151 | \a file path which contents' will be read by using --path." 152 | 153 | , mode "delete" 154 | (Delete <$> nameOption <*> require revisionOption <*> forceFlag) 155 | "Remove a specific credential revision." 156 | "Please note that if an application is pinned to the revision specified \ 157 | \by --revision, it will no longer be able to decrypt the credential." 158 | 159 | , mode "truncate" 160 | (Truncate <$> nameOption <*> forceFlag) 161 | "Truncate a specific credential's revisions." 162 | "This will remove all but the most recent credential revision. \ 163 | \That is, after running this command you will have exactly _one_ \ 164 | \revision for the given credential name." 165 | 166 | , mode "setup" 167 | (pure Setup) 168 | "Setup a new credential store." 169 | "This will run the necessary actions to create a new credential store. \ 170 | \This action is idempotent and if the store already exists, \ 171 | \the operation will succeed with exit status 0." 172 | 173 | , mode "teardown" 174 | (Teardown <$> forceFlag) 175 | "Remove an entire credential store." 176 | "Warning: This will completely remove the credential store. For some \ 177 | \storage engines this action is irrevocable unless you specifically \ 178 | \perform backups for your data." 179 | ] 180 | 181 | mode :: String 182 | -> Parser a 183 | -> Text 184 | -> Text 185 | -> Mod CommandFields (Options Maybe, a) 186 | mode name p desc foot = 187 | command name $ info ((,) <$> commonOptions <*> p) 188 | ( fullDesc <> progDescDoc (Just $ wrap desc) 189 | <> footerDoc (Just $ indent 2 (wrap foot) <> line) 190 | ) 191 | 192 | commonOptions :: Parser (Options Maybe) 193 | commonOptions = Options 194 | <$> App.optional (textOption 195 | ( short 'r' 196 | <> long "region" 197 | <> metavar "REGION" 198 | <> completes "The AWS region in which to operate." 199 | "The following regions are supported:" 200 | (map (,mempty) unsafeEnum) 201 | Nothing 202 | (Just "Note: this corresponds to both the KMS key region and the \ 203 | \DynamoDB table region.") 204 | )) 205 | 206 | <*> App.optional (textOption 207 | ( short 'u' 208 | <> long "uri" 209 | <> metavar "URI" 210 | <> defaults "URI specifying the storage system to use." 211 | "The URI format must follow the following protocol:" 212 | [ ("dynamo:/[/host[:port]]/table-name", "") 213 | , (show (defaultStore defaultRegion), "") 214 | 215 | ] 216 | Nothing 217 | (Just "If no host is specified (ie. dynamo:/table-name), \ 218 | \the default AWS endpoint for the given --region will be used. \ 219 | \If an AWS endpoint is specified the endpoint region MUST \ 220 | \correspond with --region, or a signature error will occur.") 221 | )) 222 | 223 | <*> textOption 224 | ( short 'o' 225 | <> long "output" 226 | <> metavar "FORMAT" 227 | <> completes "Output format for displaying retrieved credentials." 228 | "The following formats are supported:" 229 | [ (Pretty, "Pretty printed JSON.") 230 | , (JSON, "Single-line JSON output.") 231 | , (Echo, "Untitled textual output with no trailing newline.") 232 | , (Print, "Print multi-line user output.") 233 | ] 234 | (Just Print) Nothing 235 | ) 236 | 237 | <*> textOption 238 | ( short 'l' 239 | <> long "level" 240 | <> metavar "LEVEL" 241 | <> completes "Log level of AWS messages to emit." 242 | "The following log levels are supported:" 243 | [ (Error, "Service errors and exceptions.") 244 | , (Debug, "Requests and responses.") 245 | , (Trace, "Sensitive signing metadata.") 246 | , (Info, "No logging of library routines.") 247 | ] 248 | (Just Info) Nothing 249 | ) 250 | 251 | keyOption :: Parser KeyId 252 | keyOption = textOption 253 | ( short 'k' 254 | <> long "key" 255 | <> metavar "ARN" 256 | <> defaults "The KMS Master Key Id to use." 257 | "Examples of KMS aliases or ARNs are:" 258 | [ ("arn:aws:kms:us-east-1:1234:key/12345678-1234", "") 259 | , ("arn:aws:kms:us-east-1:1234:alias/MyAliasName", "") 260 | , ("12345678-1234-1234-12345", "") 261 | , ("alias/MyAliasName", "") 262 | ] 263 | (Just defaultKeyId) 264 | (Just "It's recommended to setup a new key using the default alias.") 265 | ) 266 | 267 | contextOption :: Parser Context 268 | contextOption = fromPairs $ textOption 269 | ( short 'c' 270 | <> long "context" 271 | <> metavar "KEY=VALUE" 272 | <> describe "A key/value pair to add to the encryption context. \ 273 | \The same context must be provided during encryption and decryption." 274 | (Just $ "You can enter multiple key/value pairs. For example:" 275 | indent 2 "-c foo=bar -c something=\"containing spaces\" ..." 276 | ) Optional 277 | ) 278 | 279 | nameOption :: Parser Name 280 | nameOption = textOption 281 | ( short 'n' 282 | <> long "name" 283 | <> metavar "STRING" 284 | <> describe "The unique name of the credential." Nothing Required 285 | ) 286 | 287 | revisionOption :: Fact -> Parser Revision 288 | revisionOption r = textOption 289 | ( short 'v' 290 | <> long "revision" 291 | <> metavar "STRING" 292 | <> describe "The revision of the credential." Nothing r 293 | ) 294 | 295 | forceFlag :: Parser Force 296 | forceFlag = flag Prompt NoPrompt 297 | ( short 'f' 298 | <> long "force" 299 | <> help "Always overwrite or remove, without an interactive prompt." 300 | ) 301 | 302 | inputOption :: Parser Input 303 | inputOption = textual <|> filepath 304 | where 305 | textual = Value 306 | <$> textOption 307 | ( short 's' 308 | <> long "secret" 309 | <> metavar "STRING" 310 | <> help "The unencrypted secret value of the credential." 311 | ) 312 | 313 | filepath = Path 314 | <$> option str 315 | ( short 'p' 316 | <> long "path" 317 | <> metavar "PATH" 318 | <> help "A file to read as the contents of the unencrypted credential." 319 | <> action "file" 320 | ) 321 | --------------------------------------------------------------------------------