Added support for reconnect tokens

Repository.Token() generates a base64-encoded token that can
be stored in password manager that fully describes repository connection
information (blob.ConnectionInfo) and optionally a password.

Use `kopia repo status -t` to print the token.
Use `kopia repo status -t -s` to print the token that also includes
repository password.

Use `kopia repo connect from-config --token T` to reconnect using the
token.
This commit is contained in:
Jarek Kowalski
2019-07-10 06:47:51 -07:00
parent 89469e74f2
commit e414e7a4d1
6 changed files with 149 additions and 4 deletions

View File

@@ -11,16 +11,29 @@
"github.com/kopia/kopia/repo/blob"
)
var connectToStorageFromConfigPath string
var connectFromConfigFile string
var connectFromConfigToken string
func connectToStorageFromConfig(ctx context.Context, isNew bool) (blob.Storage, error) {
if isNew {
return nil, errors.New("not supported")
}
if connectFromConfigFile != "" {
return connectToStorageFromConfigFile(ctx)
}
if connectFromConfigToken != "" {
return connectToStorageFromConfigToken(ctx)
}
return nil, errors.New("either --file or --token must be provided")
}
func connectToStorageFromConfigFile(ctx context.Context) (blob.Storage, error) {
var cfg repo.LocalConfig
f, err := os.Open(connectToStorageFromConfigPath)
f, err := os.Open(connectFromConfigFile)
if err != nil {
return nil, errors.Wrap(err, "unable to open config")
}
@@ -33,12 +46,23 @@ func connectToStorageFromConfig(ctx context.Context, isNew bool) (blob.Storage,
return blob.NewStorage(ctx, cfg.Storage)
}
func connectToStorageFromConfigToken(ctx context.Context) (blob.Storage, error) {
ci, pass, err := repo.DecodeToken(connectFromConfigToken)
if err != nil {
return nil, errors.Wrap(err, "invalid token")
}
passwordFromToken = pass
return blob.NewStorage(ctx, ci)
}
func init() {
RegisterStorageConnectFlags(
"from-config",
"the provided configuration file",
func(cmd *kingpin.CmdClause) {
cmd.Arg("file", "Path to the configuration file").Required().StringVar(&connectToStorageFromConfigPath)
cmd.Flag("file", "Path to the configuration file").StringVar(&connectFromConfigFile)
cmd.Flag("token", "Configuration token").StringVar(&connectFromConfigToken)
},
connectToStorageFromConfig)
}

View File

@@ -14,7 +14,9 @@
)
var (
statusCommand = repositoryCommands.Command("status", "Display the status of connected repository.")
statusCommand = repositoryCommands.Command("status", "Display the status of connected repository.")
statusReconnectToken = statusCommand.Flag("reconnect-token", "Display reconnect command").Short('t').Bool()
statusReconnectTokenIncludePassword = statusCommand.Flag("reconnect-token-with-password", "Include password in reconnect token").Short('s').Bool()
)
func runStatusCommand(ctx context.Context, rep *repo.Repository) error {
@@ -26,6 +28,7 @@ func runStatusCommand(ctx context.Context, rep *repo.Repository) error {
if cjson, err := json.MarshalIndent(scrubber.ScrubSensitiveData(reflect.ValueOf(ci.Config)).Interface(), " ", " "); err == nil {
fmt.Printf("Storage config: %v\n", string(cjson))
}
fmt.Println()
fmt.Println()
@@ -37,6 +40,24 @@ func runStatusCommand(ctx context.Context, rep *repo.Repository) error {
fmt.Printf("Max pack length: %v\n", units.BytesStringBase2(int64(rep.Content.Format.MaxPackSize)))
fmt.Printf("Splitter: %v\n", rep.Objects.Format.Splitter)
if *statusReconnectToken {
pass := ""
if *statusReconnectTokenIncludePassword {
pass = mustGetPasswordFromFlags(false, true)
}
tok, err := rep.Token(pass)
if err != nil {
return err
}
fmt.Printf("\nTo reconnect to the repository use:\n\n$ kopia repository connect from-config --token %v\n\n", tok)
if pass != "" {
fmt.Printf("NOTICE: The token printed above can be trivially decoded to reveal the repository password. Do not store it in an unsecured place.\n")
}
}
return nil
}

View File

@@ -40,7 +40,14 @@ func mustAskForExistingRepositoryPassword() string {
return p1
}
var passwordFromToken string
func mustGetPasswordFromFlags(isNew, allowPersistent bool) string {
if passwordFromToken != "" {
// password provided via token
return passwordFromToken
}
if !isNew && allowPersistent {
pass, ok := getPersistedPassword(repositoryConfigFileName(), getUserName())
if ok {

53
repo/token.go Normal file
View File

@@ -0,0 +1,53 @@
package repo
import (
"encoding/base64"
"encoding/json"
"github.com/pkg/errors"
"github.com/kopia/kopia/repo/blob"
)
type tokenInfo struct {
Version string `json:"version"`
Storage blob.ConnectionInfo `json:"storage"`
Password string `json:"password,omitempty"`
}
// Token returns an opaque token that contains repository connection information
// and optionally the provided password.
func (r *Repository) Token(password string) (string, error) {
ti := &tokenInfo{
Version: "1",
Storage: r.Blobs.ConnectionInfo(),
Password: password,
}
v, err := json.Marshal(ti)
if err != nil {
return "", errors.Wrap(err, "marshal token")
}
return base64.RawURLEncoding.EncodeToString(v), nil
}
// DecodeToken decodes the provided token and returns connection info and password if persisted.
func DecodeToken(token string) (blob.ConnectionInfo, string, error) {
t := &tokenInfo{}
v, err := base64.RawURLEncoding.DecodeString(token)
if err != nil {
return blob.ConnectionInfo{}, "", errors.New("unable to decode token")
}
if err := json.Unmarshal(v, t); err != nil {
return blob.ConnectionInfo{}, "", errors.New("unable to decode token")
}
if t.Version != "1" {
return blob.ConnectionInfo{}, "", errors.New("unsupported token version")
}
return t.Storage, t.Password, nil
}

View File

@@ -36,6 +36,25 @@ To disconnect:
$ kopia repository disconnect
```
### Quick Reconnection To Repository
To quickly reconnect to the repository on another machine, you can use `kopia repository status -t`, which will print quick-reconnect command that encodes all repository connection parameters in an opaque token. You can also embed the repository password, by using `kopia repository status -t -s`.
Such command can be stored long-term in a secure location, such as password manager for easy recovery.
```shell
$ kopia repository status -t -s
...
To reconnect to the repository use:
$ kopia repository connect from-config --token 03Fy598cYIqbMlNNDz9VLU0K6Pk9alC...BNeazLBdRzP2MHo0MS83zRb
NOTICE: The token printed above can be trivially decoded to reveal the repository password. Do not store it in an unsecured place.
```
> NOTE: Make sure to safeguard the repository token, as it gives full access to the repository to anybody in its posession.
### Configuration File
For each repository connection, Kopia maintains a configuration file and local cache:

View File

@@ -121,6 +121,27 @@ func TestEndToEnd(t *testing.T) {
e.runAndExpectSuccess(t, "repo", "status")
})
t.Run("ReconnectUsingToken", func(t *testing.T) {
lines := e.runAndExpectSuccess(t, "repo", "status", "-t", "-s")
prefix := "$ kopia "
var reconnectArgs []string
// look for output line containing the prefix - this will be our reconnect command
for _, l := range lines {
if strings.HasPrefix(l, prefix) {
reconnectArgs = strings.Split(strings.TrimPrefix(l, prefix), " ")
}
}
if reconnectArgs == nil {
t.Fatalf("can't find reonnect command in kopia repo status output")
}
e.runAndExpectSuccess(t, "repo", "disconnect")
e.runAndExpectSuccess(t, reconnectArgs...)
e.runAndExpectSuccess(t, "repo", "status")
})
e.runAndExpectSuccess(t, "snapshot", "create", ".")
e.runAndExpectSuccess(t, "snapshot", "list", ".")