Allow specifying user and host when adding keys

The username and hostname for new keys can be specified with the new
--user and --host flags, respectively. The flags are used only by the
`key add` command and are otherwise ignored.

This allows adding keys with for a desired user and host without having
to run restic as that particular user on that particular host, making
automated key management easier.

Co-authored-by: James TD Smith <ahktenzero@mohorovi.cc>
This commit is contained in:
Peter Schultz 2019-06-26 09:37:08 +02:00
parent 2b5a6d255a
commit 90fc639a67
4 changed files with 60 additions and 16 deletions

View file

@ -0,0 +1,9 @@
Enhancement: Allow specifying user and host when creating keys
When adding a new key to the repository, the username and hostname for the new
key can be specified on the command line. This allows overriding the defaults,
for example if you would prefer to use the FQDN to identify the host or if you
want to add keys for several different hosts without having to run the key add
command on those hosts.
https://github.com/restic/restic/issues/2175

View file

@ -32,13 +32,19 @@ Exit status is 0 if the command was successful, and non-zero if there was any er
}, },
} }
var newPasswordFile string var (
newPasswordFile string
keyUsername string
keyHostname string
)
func init() { func init() {
cmdRoot.AddCommand(cmdKey) cmdRoot.AddCommand(cmdKey)
flags := cmdKey.Flags() flags := cmdKey.Flags()
flags.StringVarP(&newPasswordFile, "new-password-file", "", "", "the file from which to load a new password") flags.StringVarP(&newPasswordFile, "new-password-file", "", "", "the file from which to load a new password")
flags.StringVarP(&keyUsername, "user", "", "", "the username for new keys")
flags.StringVarP(&keyHostname, "host", "", "", "the hostname for new keys")
} }
func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions) error { func listKeys(ctx context.Context, s *repository.Repository, gopts GlobalOptions) error {
@ -120,7 +126,7 @@ func addKey(gopts GlobalOptions, repo *repository.Repository) error {
return err return err
} }
id, err := repository.AddKey(gopts.ctx, repo, pw, repo.Key()) id, err := repository.AddKey(gopts.ctx, repo, pw, keyUsername, keyHostname, repo.Key())
if err != nil { if err != nil {
return errors.Fatalf("creating new key failed: %v\n", err) return errors.Fatalf("creating new key failed: %v\n", err)
} }
@ -151,7 +157,7 @@ func changePassword(gopts GlobalOptions, repo *repository.Repository) error {
return err return err
} }
id, err := repository.AddKey(gopts.ctx, repo, pw, repo.Key()) id, err := repository.AddKey(gopts.ctx, repo, pw, "", "", repo.Key())
if err != nil { if err != nil {
return errors.Fatalf("creating new key failed: %v\n", err) return errors.Fatalf("creating new key failed: %v\n", err)
} }

View file

@ -639,6 +639,28 @@ func testRunKeyAddNewKey(t testing.TB, newPassword string, gopts GlobalOptions)
rtest.OK(t, runKey(gopts, []string{"add"})) rtest.OK(t, runKey(gopts, []string{"add"}))
} }
func testRunKeyAddNewKeyUserHost(t testing.TB, gopts GlobalOptions) {
testKeyNewPassword = "john's geheimnis"
defer func() {
testKeyNewPassword = ""
keyUsername = ""
keyHostname = ""
}()
cmdKey.Flags().Parse([]string{"--user=john", "--host=example.com"})
t.Log("adding key for john@example.com")
rtest.OK(t, runKey(gopts, []string{"add"}))
repo, err := OpenRepository(gopts)
rtest.OK(t, err)
key, err := repository.SearchKey(gopts.ctx, repo, testKeyNewPassword, 1, "")
rtest.OK(t, err)
rtest.Equals(t, "john", key.Username)
rtest.Equals(t, "example.com", key.Hostname)
}
func testRunKeyPasswd(t testing.TB, newPassword string, gopts GlobalOptions) { func testRunKeyPasswd(t testing.TB, newPassword string, gopts GlobalOptions) {
testKeyNewPassword = newPassword testKeyNewPassword = newPassword
defer func() { defer func() {
@ -681,6 +703,8 @@ func TestKeyAddRemove(t *testing.T) {
t.Logf("testing access with last password %q\n", env.gopts.password) t.Logf("testing access with last password %q\n", env.gopts.password)
rtest.OK(t, runKey(env.gopts, []string{"list"})) rtest.OK(t, runKey(env.gopts, []string{"list"}))
testRunCheck(t, env.gopts) testRunCheck(t, env.gopts)
testRunKeyAddNewKeyUserHost(t, env.gopts)
} }
func testFileSize(filename string, size int64) error { func testFileSize(filename string, size int64) error {

View file

@ -58,7 +58,7 @@ var (
// createMasterKey creates a new master key in the given backend and encrypts // createMasterKey creates a new master key in the given backend and encrypts
// it with the password. // it with the password.
func createMasterKey(s *Repository, password string) (*Key, error) { func createMasterKey(s *Repository, password string) (*Key, error) {
return AddKey(context.TODO(), s, password, nil) return AddKey(context.TODO(), s, password, "", "", nil)
} }
// OpenKey tries do decrypt the key specified by name with the given password. // OpenKey tries do decrypt the key specified by name with the given password.
@ -199,7 +199,7 @@ func LoadKey(ctx context.Context, s *Repository, name string) (k *Key, err error
} }
// AddKey adds a new key to an already existing repository. // AddKey adds a new key to an already existing repository.
func AddKey(ctx context.Context, s *Repository, password string, template *crypto.Key) (*Key, error) { func AddKey(ctx context.Context, s *Repository, password, username, hostname string, template *crypto.Key) (*Key, error) {
// make sure we have valid KDF parameters // make sure we have valid KDF parameters
if Params == nil { if Params == nil {
p, err := crypto.Calibrate(KDFTimeout, KDFMemory) p, err := crypto.Calibrate(KDFTimeout, KDFMemory)
@ -213,24 +213,29 @@ func AddKey(ctx context.Context, s *Repository, password string, template *crypt
// fill meta data about key // fill meta data about key
newkey := &Key{ newkey := &Key{
Created: time.Now(), Created: time.Now(),
KDF: "scrypt", Username: username,
N: Params.N, Hostname: hostname,
R: Params.R,
P: Params.P, KDF: "scrypt",
N: Params.N,
R: Params.R,
P: Params.P,
} }
hn, err := os.Hostname() if newkey.Hostname == "" {
if err == nil { newkey.Hostname, _ = os.Hostname()
newkey.Hostname = hn
} }
usr, err := user.Current() if newkey.Username == "" {
if err == nil { usr, err := user.Current()
newkey.Username = usr.Username if err == nil {
newkey.Username = usr.Username
}
} }
// generate random salt // generate random salt
var err error
newkey.Salt, err = crypto.NewSalt() newkey.Salt, err = crypto.NewSalt()
if err != nil { if err != nil {
panic("unable to read enough random bytes for salt: " + err.Error()) panic("unable to read enough random bytes for salt: " + err.Error())