diff --git a/README.md b/README.md index 00642e0..68f3e9e 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ This section was just added so that I could get an idea of where I am at. - [X] Creates a folder and a .gpg-id file - [X] Support ``--path`` option - [X] Support multiple GPG ids -- [ ] Re-encryption functionality +- [X] Re-encryption functionality - [ ] Should output: ``Password store initialized for [gpg-id].`` - [ ] ``--clone `` allows to init from an existing repo diff --git a/cmd/gopass/internal/cli/command/init/init.go b/cmd/gopass/internal/cli/command/init/init.go index 871e1c8..e3e96d9 100644 --- a/cmd/gopass/internal/cli/command/init/init.go +++ b/cmd/gopass/internal/cli/command/init/init.go @@ -22,6 +22,7 @@ import ( "fmt" "io/ioutil" "path/filepath" + "strings" "github.com/aviau/gopass" "github.com/aviau/gopass/cmd/gopass/internal/cli/command" @@ -71,10 +72,36 @@ func ExecInit(cfg command.Config, args []string) error { gpgIDs := fs.Args() store := gopass.NewPasswordStore(path) - if err := store.Init(gpgIDs); err != nil { - return err + + // There is no existing store, create one. + if len(store.GPGIDs) == 0 { + if err := store.Init(gpgIDs); err != nil { + return err + } + fmt.Fprintf(cfg.WriterOutput(), "Successfully created Password Store at \"%s\".\n", path) + } else { + // The store already exists, reencrypt it. + + // First, set the GPG ids... + store.SetGPGIDs(gpgIDs) + + // Now, reencrypt every password + passwords := store.GetPasswordsList() + for _, password := range passwords { + fmt.Printf("%s: reencrypting to %s\n", password, strings.Join(gpgIDs, ", ")) + if err := store.ReencryptPassword(password); err != nil { + return err + } + } + + // Commit + if err := store.AddAndCommit( + "Reencrypt password store using new GPG id "+strings.Join(gpgIDs, ", "), + "*", + ); err != nil { + return err + } } - fmt.Fprintf(cfg.WriterOutput(), "Successfully created Password Store at \"%s\".\n", path) return nil } diff --git a/man/gopass.1 b/man/gopass.1 index 7744fe2..ee7b51a 100644 --- a/man/gopass.1 +++ b/man/gopass.1 @@ -56,6 +56,9 @@ Initialize new password storage and use for encryption. Multiple gpg-ids may be specified, in order to encrypt each password with multiple ids. This command must be run first before a password store can be used. +If the specified +.I gpg-id +is different from the key used in any existing files, these files will be reencrypted to use the new id. Note that use of .BR gpg-agent (1) is recommended so that the batch decryption does not require as much user diff --git a/password_store.go b/password_store.go index 3475a90..da02861 100644 --- a/password_store.go +++ b/password_store.go @@ -21,6 +21,7 @@ import ( "bufio" "fmt" "io" + "io/ioutil" "os" "os/exec" "path" @@ -129,25 +130,37 @@ func (store *PasswordStore) Init(gpgIDs []string) error { return store.AddAndCommit("initial commit", ".gpg-id") } -// InsertPassword inserts a new password or overwrites an existing one -func (store *PasswordStore) InsertPassword(pwname, pwtext string) error { - containsPassword, passwordPath := store.ContainsPassword(pwname) +//SetGPGIDs will set the store's GPG ids +func (store *PasswordStore) SetGPGIDs(gpgIDs []string) error { + gpgIDFile, err := os.OpenFile( + path.Join(store.Path, ".gpg-id"), + os.O_WRONLY|os.O_CREATE|os.O_TRUNC, + 0644, + ) + if err != nil { + return err + } + defer gpgIDFile.Close() - // Check if password already exists - var gitAction string - if containsPassword { - gitAction = "edited" - } else { - gitAction = "added" + for _, gpgID := range gpgIDs { + gpgIDFile.WriteString(gpgID + "\n") } + store.GPGIDs = gpgIDs + return store.AddAndCommit( + fmt.Sprintf("Set GPG id to %s", strings.Join(gpgIDs, ", ")), + ".gpg-id", + ) +} + +func (store *PasswordStore) getGPGEncryptArgs(destinationPath string) []string { gpgArgs := []string{ "--encrypt", "--batch", "--use-agent", "--no-tty", "--yes", - "--output", passwordPath, + "--output", destinationPath, } for _, recipient := range store.GPGIDs { @@ -158,7 +171,76 @@ func (store *PasswordStore) InsertPassword(pwname, pwtext string) error { ) } - gpg := exec.Command(store.GPGBin, gpgArgs...) + return gpgArgs +} + +func (store *PasswordStore) getGPGDecryptArgs(passwordPath string) []string { + return []string{ + "--quiet", + "--batch", + "--use-agent", + "--decrypt", + passwordPath, + } +} + +// ReencryptPassword will reencrypt a password to the current GPG ids +func (store *PasswordStore) ReencryptPassword(pwname string) error { + containsPassword, passwordPath := store.ContainsPassword(pwname) + + // Error if the password does not exist + if containsPassword == false { + return fmt.Errorf("could not find password \"%s\" at path \"%s\"", pwname, passwordPath) + } + + // Create a directory to temporarily hold the reencrypted password + tempDir, err := ioutil.TempDir("", "gopass") + if err != nil { + return err + } + defer os.RemoveAll(tempDir) + + tempFile := filepath.Join(tempDir, "temp.gpg") + + // Pipe gpg decrypt to gpg encrypt + gpgDecrypt := exec.Command(store.GPGBin, store.getGPGDecryptArgs(passwordPath)...) + gpgEncrypt := exec.Command(store.GPGBin, store.getGPGEncryptArgs(tempFile)...) + + gpgDecrypt.Stderr = os.Stderr + + gpgEncrypt.Stdin, _ = gpgDecrypt.StdoutPipe() + gpgEncrypt.Stderr = os.Stderr + gpgEncrypt.Start() + + if err := gpgDecrypt.Run(); err != nil { + return err + } + + if err := gpgEncrypt.Wait(); err != nil { + return err + } + + // Move the newly encrypted password to the password store + if err := os.Rename(tempFile, passwordPath); err != nil { + return err + } + + return nil +} + +// InsertPassword inserts a new password or overwrites an existing one +func (store *PasswordStore) InsertPassword(pwname, pwtext string) error { + containsPassword, passwordPath := store.ContainsPassword(pwname) + + // Check if password already exists + var gitAction string + if containsPassword { + gitAction = "edited" + } else { + gitAction = "added" + } + + gpg := exec.Command(store.GPGBin, store.getGPGEncryptArgs(passwordPath)...) stdin, _ := gpg.StdinPipe() io.WriteString(stdin, pwtext) @@ -319,8 +401,7 @@ func (store *PasswordStore) GetPassword(pwname string) (string, error) { return "", fmt.Errorf("could not find password \"%s\" at path \"%s\"", pwname, passwordPath) } - // TODO: Use GPG lib instead - show := exec.Command(store.GPGBin, "--quiet", "--batch", "--use-agent", "-d", passwordPath) + show := exec.Command(store.GPGBin, store.getGPGDecryptArgs(passwordPath)...) output, err := show.CombinedOutput() if err != nil { @@ -360,8 +441,10 @@ func (store *PasswordStore) GetPasswordsList() []string { var scan = func(path string, fileInfo os.FileInfo, inpErr error) (err error) { if strings.HasSuffix(path, ".gpg") { - _, file := filepath.Split(path) - password := strings.TrimSuffix(file, ".gpg") + password := strings.TrimSuffix( + strings.TrimPrefix(path, store.Path+"/"), + ".gpg", + ) list = append(list, password) } return diff --git a/password_store_get_password_list_test.go b/password_store_get_password_list_test.go index 3a68bc8..59a1971 100644 --- a/password_store_get_password_list_test.go +++ b/password_store_get_password_list_test.go @@ -31,18 +31,24 @@ func TestGetPasswordsList(t *testing.T) { st := gopasstest.NewPasswordStoreTest(t) defer st.Close() - _, err := os.Create(filepath.Join(st.PasswordStore.Path, "test.com.gpg")) - if err != nil { + if _, err := os.Create(filepath.Join(st.PasswordStore.Path, "test.com.gpg")); err != nil { t.Fatal(err) } - _, err = os.Create(filepath.Join(st.PasswordStore.Path, "test2.com.gpg")) - if err != nil { + if _, err := os.Create(filepath.Join(st.PasswordStore.Path, "test2.com.gpg")); err != nil { t.Fatal(err) } - _, err = os.Create(filepath.Join(st.PasswordStore.Path, "test3")) - if err != nil { + if _, err := os.Create(filepath.Join(st.PasswordStore.Path, "test3")); err != nil { + t.Fatal(err) + } + + dirPath := filepath.Join(st.PasswordStore.Path, "dir") + if err := os.Mkdir(dirPath, os.ModePerm); err != nil { + t.Fatal(err) + } + + if _, err := os.Create(filepath.Join(dirPath, "test3.com.gpg")); err != nil { t.Fatal(err) } @@ -50,7 +56,7 @@ func TestGetPasswordsList(t *testing.T) { assert.Equal( t, passwords, - []string{"test.com", "test2.com"}, + []string{"dir/test3.com", "test.com", "test2.com"}, "Password list should contain test.com and test2.com", ) }