From c35c1ea2544bdc8b1441c2204977a50ee143f79a Mon Sep 17 00:00:00 2001 From: Derek McGowan Date: Mon, 20 Jul 2015 13:02:05 -0700 Subject: [PATCH] Move passphrase logic to its own package The logic to retrieve passphrase is generic and may be used by directly by clients. Signed-off-by: Derek McGowan (github: dmcgowan) --- client/client.go | 3 +- cmd/notary/tuf.go | 97 +------------------------ keystoremanager/import_export.go | 5 +- keystoremanager/keystoremanager.go | 3 +- pkg/passphrase/passphrase.go | 109 +++++++++++++++++++++++++++++ signer/api/rpc_api_test.go | 3 +- trustmanager/keyfilestore.go | 20 +++--- 7 files changed, 129 insertions(+), 111 deletions(-) create mode 100644 pkg/passphrase/passphrase.go diff --git a/client/client.go b/client/client.go index 1f61dd5611..9c27532db6 100644 --- a/client/client.go +++ b/client/client.go @@ -14,6 +14,7 @@ import ( "github.com/docker/notary/client/changelist" "github.com/docker/notary/cryptoservice" "github.com/docker/notary/keystoremanager" + "github.com/docker/notary/pkg/passphrase" "github.com/docker/notary/trustmanager" "github.com/endophage/gotuf" tufclient "github.com/endophage/gotuf/client" @@ -84,7 +85,7 @@ func NewTarget(targetName string, targetPath string) (*Target, error) { // It takes the base directory under where all the trust files will be stored // (usually ~/.docker/trust/). func NewNotaryRepository(baseDir, gun, baseURL string, rt http.RoundTripper, - passphraseRetriever trustmanager.PassphraseRetriever) (*NotaryRepository, error) { + passphraseRetriever passphrase.Retriever) (*NotaryRepository, error) { keyStoreManager, err := keystoremanager.NewKeyStoreManager(baseDir, passphraseRetriever) if err != nil { diff --git a/cmd/notary/tuf.go b/cmd/notary/tuf.go index 08904859c4..d8aa52a2aa 100644 --- a/cmd/notary/tuf.go +++ b/cmd/notary/tuf.go @@ -1,31 +1,27 @@ package main import ( - "bufio" "crypto/sha256" "crypto/tls" - "errors" "fmt" "io/ioutil" "net/http" "os" "github.com/Sirupsen/logrus" - "github.com/docker/docker/pkg/term" notaryclient "github.com/docker/notary/client" - "github.com/docker/notary/trustmanager" + "github.com/docker/notary/pkg/passphrase" "github.com/spf13/cobra" "github.com/spf13/viper" - "strings" ) // FIXME: This should not be hardcoded const hardcodedBaseURL = "https://notary-server:4443" -var retriever trustmanager.PassphraseRetriever +var retriever passphrase.Retriever func init() { - retriever = getNotaryPassphraseRetriever() + retriever = passphrase.PromptRetriever() } var remoteTrustServer string @@ -272,93 +268,6 @@ func verify(cmd *cobra.Command, args []string) { return } -func getNotaryPassphraseRetriever() trustmanager.PassphraseRetriever { - userEnteredTargetsSnapshotsPass := false - targetsSnapshotsPass := "" - userEnteredRootsPass := false - rootsPass := "" - - return func(keyID string, alias string, createNew bool, numAttempts int) (string, bool, error) { - - // First, check if we have a password cached for this alias. - if numAttempts == 0 { - if userEnteredTargetsSnapshotsPass && (alias == "snapshot" || alias == "targets") { - return targetsSnapshotsPass, false, nil - } - if userEnteredRootsPass && (alias == "root") { - return rootsPass, false, nil - } - } - - if numAttempts > 3 && !createNew { - return "", true, errors.New("Too many attempts") - } - - state, err := term.SaveState(0) - if err != nil { - return "", false, err - } - term.DisableEcho(0, state) - defer term.RestoreTerminal(0, state) - - stdin := bufio.NewReader(os.Stdin) - - if createNew { - fmt.Printf("Enter passphrase for new %s key with id %s: ", alias, keyID) - } else { - fmt.Printf("Enter key passphrase for %s key with id %s: ", alias, keyID) - } - - passphrase, err := stdin.ReadBytes('\n') - fmt.Println() - if err != nil { - return "", false, err - } - - retPass := strings.TrimSpace(string(passphrase)) - - if !createNew { - if alias == "snapshot" || alias == "targets" { - userEnteredTargetsSnapshotsPass = true - targetsSnapshotsPass = retPass - } - if alias == "root" { - userEnteredRootsPass = true - rootsPass = retPass - } - return retPass, false, nil - } - - if len(retPass) < 8 { - fmt.Println("Please use a password manager to generate and store a good random passphrase.") - return "", false, errors.New("Passphrase too short") - } - - fmt.Printf("Repeat passphrase for new %s key with id %s: ", alias, keyID) - confirmation, err := stdin.ReadBytes('\n') - fmt.Println() - if err != nil { - return "", false, err - } - confirmationStr := strings.TrimSpace(string(confirmation)) - - if retPass != confirmationStr { - return "", false, errors.New("The entered passphrases do not match") - } - - if alias == "snapshot" || alias == "targets" { - userEnteredTargetsSnapshotsPass = true - targetsSnapshotsPass = retPass - } - if alias == "root" { - userEnteredRootsPass = true - rootsPass = retPass - } - - return retPass, false, nil - } -} - func getInsecureTransport() *http.Transport { return &http.Transport{ TLSClientConfig: &tls.Config{ diff --git a/keystoremanager/import_export.go b/keystoremanager/import_export.go index 1a86157f0a..3c42ab8263 100644 --- a/keystoremanager/import_export.go +++ b/keystoremanager/import_export.go @@ -12,6 +12,7 @@ import ( "strings" "github.com/Sirupsen/logrus" + "github.com/docker/notary/pkg/passphrase" "github.com/docker/notary/trustmanager" ) @@ -135,7 +136,7 @@ func addKeysToArchive(zipWriter *zip.Writer, newKeyStore *trustmanager.KeyFileSt // ExportAllKeys exports all keys to an io.Writer in zip format. // newPassphraseRetriever will be used to obtain passphrases to use to encrypt the existing keys. -func (km *KeyStoreManager) ExportAllKeys(dest io.Writer, newPassphraseRetriever trustmanager.PassphraseRetriever) error { +func (km *KeyStoreManager) ExportAllKeys(dest io.Writer, newPassphraseRetriever passphrase.Retriever) error { tempBaseDir, err := ioutil.TempDir("", "notary-key-export-") defer os.RemoveAll(tempBaseDir) @@ -280,7 +281,7 @@ func moveKeysByGUN(oldKeyStore, newKeyStore *trustmanager.KeyFileStore, gun stri // ExportKeysByGUN exports all keys associated with a specified GUN to an // io.Writer in zip format. passphraseRetriever is used to select new passphrases to use to // encrypt the keys. -func (km *KeyStoreManager) ExportKeysByGUN(dest io.Writer, gun string, passphraseRetriever trustmanager.PassphraseRetriever) error { +func (km *KeyStoreManager) ExportKeysByGUN(dest io.Writer, gun string, passphraseRetriever passphrase.Retriever) error { tempBaseDir, err := ioutil.TempDir("", "notary-key-export-") defer os.RemoveAll(tempBaseDir) diff --git a/keystoremanager/keystoremanager.go b/keystoremanager/keystoremanager.go index 3c8da66c2f..6f4c1d6b20 100644 --- a/keystoremanager/keystoremanager.go +++ b/keystoremanager/keystoremanager.go @@ -11,6 +11,7 @@ import ( "github.com/Sirupsen/logrus" "github.com/docker/notary/cryptoservice" + "github.com/docker/notary/pkg/passphrase" "github.com/docker/notary/trustmanager" "github.com/endophage/gotuf/data" "github.com/endophage/gotuf/signed" @@ -60,7 +61,7 @@ func (err ErrRootRotationFail) Error() string { // NewKeyStoreManager returns an initialized KeyStoreManager, or an error // if it fails to create the KeyFileStores or load certificates -func NewKeyStoreManager(baseDir string, passphraseRetriever trustmanager.PassphraseRetriever) (*KeyStoreManager, error) { +func NewKeyStoreManager(baseDir string, passphraseRetriever passphrase.Retriever) (*KeyStoreManager, error) { nonRootKeysPath := filepath.Join(baseDir, privDir, nonRootKeysSubdir) nonRootKeyStore, err := trustmanager.NewKeyFileStore(nonRootKeysPath, passphraseRetriever) if err != nil { diff --git a/pkg/passphrase/passphrase.go b/pkg/passphrase/passphrase.go new file mode 100644 index 0000000000..c2f0ad2fcf --- /dev/null +++ b/pkg/passphrase/passphrase.go @@ -0,0 +1,109 @@ +// Package passphrase is a utility function for managing passphrase +// for TUF and Notary keys. +package passphrase + +import ( + "bufio" + "errors" + "fmt" + "os" + "strings" + + "github.com/docker/docker/pkg/term" +) + +// Retriever is a callback function that should retrieve a passphrase +// for a given named key. If it should be treated as new passphrase (e.g. with +// confirmation), createNew will be true. Attempts is passed in so that implementers +// decide how many chances to give to a human, for example. +type Retriever func(keyName, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error) + +// PromptRetriever returns a new Retriever which will provide a terminal prompt +// to retrieve a passphrase. The passphrase will be cached such that subsequent +// prompts will produce the same passphrase. +func PromptRetriever() Retriever { + userEnteredTargetsSnapshotsPass := false + targetsSnapshotsPass := "" + userEnteredRootsPass := false + rootsPass := "" + + return func(keyName string, alias string, createNew bool, numAttempts int) (string, bool, error) { + + // First, check if we have a password cached for this alias. + if numAttempts == 0 { + if userEnteredTargetsSnapshotsPass && (alias == "snapshot" || alias == "targets") { + return targetsSnapshotsPass, false, nil + } + if userEnteredRootsPass && (alias == "root") { + return rootsPass, false, nil + } + } + + if numAttempts > 3 && !createNew { + return "", true, errors.New("Too many attempts") + } + + state, err := term.SaveState(0) + if err != nil { + return "", false, err + } + term.DisableEcho(0, state) + defer term.RestoreTerminal(0, state) + + stdin := bufio.NewReader(os.Stdin) + + if createNew { + fmt.Printf("Enter passphrase for new %s key with id %s: ", alias, keyName) + } else { + fmt.Printf("Enter key passphrase for %s key with id %s: ", alias, keyName) + } + + passphrase, err := stdin.ReadBytes('\n') + fmt.Println() + if err != nil { + return "", false, err + } + + retPass := strings.TrimSpace(string(passphrase)) + + if !createNew { + if alias == "snapshot" || alias == "targets" { + userEnteredTargetsSnapshotsPass = true + targetsSnapshotsPass = retPass + } + if alias == "root" { + userEnteredRootsPass = true + rootsPass = retPass + } + return retPass, false, nil + } + + if len(retPass) < 8 { + fmt.Println("Please use a password manager to generate and store a good random passphrase.") + return "", false, errors.New("Passphrase too short") + } + + fmt.Printf("Repeat passphrase for new %s key with id %s: ", alias, keyName) + confirmation, err := stdin.ReadBytes('\n') + fmt.Println() + if err != nil { + return "", false, err + } + confirmationStr := strings.TrimSpace(string(confirmation)) + + if retPass != confirmationStr { + return "", false, errors.New("The entered passphrases do not match") + } + + if alias == "snapshot" || alias == "targets" { + userEnteredTargetsSnapshotsPass = true + targetsSnapshotsPass = retPass + } + if alias == "root" { + userEnteredRootsPass = true + rootsPass = retPass + } + + return retPass, false, nil + } +} diff --git a/signer/api/rpc_api_test.go b/signer/api/rpc_api_test.go index 042e415f15..4021c7dae9 100644 --- a/signer/api/rpc_api_test.go +++ b/signer/api/rpc_api_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/docker/notary/cryptoservice" + "github.com/docker/notary/pkg/passphrase" "github.com/docker/notary/signer" "github.com/docker/notary/signer/api" "github.com/docker/notary/trustmanager" @@ -24,7 +25,7 @@ var ( sClient pb.SignerClient grpcServer *grpc.Server void *pb.Void - pr trustmanager.PassphraseRetriever + pr passphrase.Retriever ) func init() { diff --git a/trustmanager/keyfilestore.go b/trustmanager/keyfilestore.go index b4a6080b57..ae941088bd 100644 --- a/trustmanager/keyfilestore.go +++ b/trustmanager/keyfilestore.go @@ -6,6 +6,8 @@ import ( "errors" "fmt" + + "github.com/docker/notary/pkg/passphrase" "github.com/endophage/gotuf/data" ) @@ -24,27 +26,21 @@ type KeyStore interface { RemoveKey(name string) error } -// PassphraseRetriever is a callback function that should retrieve a passphrase -// for a given named key. If it should be treated as new passphrase (e.g. with -// confirmation), createNew will be true. Attempts is passed in so that implementers -// decide how many chances to give to a human, for example. -type PassphraseRetriever func(keyId, alias string, createNew bool, attempts int) (passphrase string, giveup bool, err error) - // KeyFileStore persists and manages private keys on disk type KeyFileStore struct { SimpleFileStore - PassphraseRetriever + PassphraseRetriever passphrase.Retriever } // KeyMemoryStore manages private keys in memory type KeyMemoryStore struct { MemoryFileStore - PassphraseRetriever + PassphraseRetriever passphrase.Retriever } // NewKeyFileStore returns a new KeyFileStore creating a private directory to // hold the keys. -func NewKeyFileStore(baseDir string, passphraseRetriever PassphraseRetriever) (*KeyFileStore, error) { +func NewKeyFileStore(baseDir string, passphraseRetriever passphrase.Retriever) (*KeyFileStore, error) { fileStore, err := NewPrivateSimpleFileStore(baseDir, keyExtension) if err != nil { return nil, err @@ -81,7 +77,7 @@ func (s *KeyFileStore) RemoveKey(name string) error { } // NewKeyMemoryStore returns a new KeyMemoryStore which holds keys in memory -func NewKeyMemoryStore(passphraseRetriever PassphraseRetriever) *KeyMemoryStore { +func NewKeyMemoryStore(passphraseRetriever passphrase.Retriever) *KeyMemoryStore { memStore := NewMemoryFileStore() return &KeyMemoryStore{*memStore, passphraseRetriever} @@ -114,7 +110,7 @@ func (s *KeyMemoryStore) RemoveKey(name string) error { return removeKey(s, name) } -func addKey(s LimitedFileStore, passphraseRetriever PassphraseRetriever, name, alias string, privKey data.PrivateKey) error { +func addKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, name, alias string, privKey data.PrivateKey) error { pemPrivKey, err := KeyToPEM(privKey) if err != nil { return err @@ -166,7 +162,7 @@ func getKeyAlias(s LimitedFileStore, keyID string) (string, error) { } // GetKey returns the PrivateKey given a KeyID -func getKey(s LimitedFileStore, passphraseRetriever PassphraseRetriever, name string) (data.PrivateKey, error) { +func getKey(s LimitedFileStore, passphraseRetriever passphrase.Retriever, name string) (data.PrivateKey, error) { keyAlias, err := getKeyAlias(s, name) if err != nil { return nil, err