From 10e285cd2858fdf09b911a58db2951d6af21bdbb Mon Sep 17 00:00:00 2001 From: Emmanuel Odeke Date: Sun, 22 May 2016 21:34:32 -0700 Subject: [PATCH] E2E: encrypt on upload(at rest remotely), decrypt on download Fixes #543. When E2E encryption is enabled, data should only be unencrypted on the user's local disk but should always be encrypted remotely on Google Drive, if the password was provided. - When a password is provided, encrypt the data before it is received by Google Drive. Encrypt for upload: ```shell $ drive push --encryption-password foolsGold issueFilingDrive.mp4 $ cat issueFiling.txt | drive push --encryption-password "$1N4Lax" ok.enc ``` - When a password is provided, decrypt the data before writing it to disk: ```shell $ drive pull --piped --decryption-password foolsGold issueFilingDrive.mp4 > txt.v $ drive pull --decryption-password "t51L3ntNA6T" few.mp4 ``` --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++ cmd/drive/main.go | 42 ++++++++++++++++++++++++++++++++++------ src/commands.go | 4 ++++ src/help.go | 4 ++++ src/pull.go | 6 ++++++ src/push.go | 6 ++++++ src/remote.go | 20 +++++++++++++++++++ 7 files changed, 125 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f81a6a20..fdc80099 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ - [Usage](#usage) - [Initializing](#initializing) - [De Initializing](#de-initializing) + - [End to End Encryption](#end-to-end-encryption) - [Traversal Depth](#traversal-depth) - [Pulling](#pulling) - [Exporting Docs](#exporting-docs) @@ -221,6 +222,14 @@ Run it without any arguments to pull all of the files from the current path: $ drive pull ``` +To pull and decrypt your data that is stored encrypted at rest on Google Drive, use flag `--decryption-password`: + +See [Issue #543](https://github.com/odeke-em/issues/543) + +```shell +$ drive pull --decryption-password "$JiME5Umf" influx.txt +``` + Pulling by matches is also supported ```shell @@ -359,6 +368,13 @@ Note: To ignore checksum verification during a push: $ drive push -ignore-checksum ``` +To keep your data encrypted at rest remotely on Google Drive: + +```shell +$ drive push --encryption-password "$JiME5Umf" influx.txt +``` +For E2E discussions, see [issue #543](https://github.com/odeke-em/issues/543): + drive also supports pushing content piped from stdin which can be accomplished by: ```shell @@ -470,6 +486,39 @@ default count of 20: $ drive push --retry-count 4 a/bc/def terms ``` +### End to End Encryption + +See [Issue #543](https://github.com/odeke-em/issues/543) + +This can be toggled when you supply a non-empty password ie + +- `--encryption-password` for a push. +- `--decryption-password` for a pull. + +When you supply argument `--encryption-password` during a push, drive will encrypt your data +and store it remotely encrypted(stored encrypted at rest), it can only be decrypted by you when you +perform a pull with the respective arg `--decryption-password`. + +```shell +$ drive push --encryption-password "$400lsGO1Di3" few-ones.mp4 newest.mkv +``` + +```shell +$ drive pull --decryption-password "$400lsGO1Di3" few-ones.mp4 newest.mkv +``` + +If you supply the wrong password, you'll be warned if it cannot be decrypted + +```shell +$ drive pull --decryption-password "4nG5troM" few-ones.mp4 newest.mkv +message corrupt or incorrect password +``` + +To pull normally push or pull your content, without attempting any *cryption attempts, skip +passing in a password and no attempts will be made. + + + ### Publishing The `pub` command publishes a file or directory globally so that anyone can view it on the web using the link returned. diff --git a/cmd/drive/main.go b/cmd/drive/main.go index 0165fd62..38438e83 100644 --- a/cmd/drive/main.go +++ b/cmd/drive/main.go @@ -19,6 +19,7 @@ import ( "encoding/json" "flag" "fmt" + "io" "os" "os/signal" "path/filepath" @@ -30,6 +31,7 @@ import ( "github.com/odeke-em/drive/config" "github.com/odeke-em/drive/gen" "github.com/odeke-em/drive/src" + "github.com/odeke-em/drive/src/dcrypto" ) var context *config.Context @@ -627,12 +629,13 @@ type pullCmd struct { ExplicitlyExport *bool `json:"explicitly-export"` FixClashes *bool `json:"fix-clashes"` - Verbose *bool `json:"verbose"` - Depth *int `json:"depth"` - Starred *bool `json:"starred"` - AllStarred *bool `json:"all-starred"` - InTrash *bool `json:"trashed"` - ExponentialBackoffRetryCount *int `json:"retry-count"` + Verbose *bool `json:"verbose"` + Depth *int `json:"depth"` + Starred *bool `json:"starred"` + AllStarred *bool `json:"all-starred"` + InTrash *bool `json:"trashed"` + ExponentialBackoffRetryCount *int `json:"retry-count"` + DecryptionPassword *string `json:"decryption-password"` } func (cmd *pullCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { @@ -661,6 +664,7 @@ func (cmd *pullCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { cmd.Starred = fs.Bool(drive.CLIOptionStarred, false, drive.DescStarred) cmd.InTrash = fs.Bool(drive.TrashedKey, false, "pull content in the trash") cmd.ExponentialBackoffRetryCount = fs.Int(drive.CLIOptionRetryCount, drive.MaxFailedRetryCount, drive.DescExponentialBackoffRetryCount) + cmd.DecryptionPassword = fs.String(drive.CLIDecryptionPassword, "", drive.DescDecryptionPassword) return fs } @@ -696,6 +700,17 @@ func (pCmd *pullCmd) Run(args []string, definedFlags map[string]*flag.Flag) { retryCount = *(cmd.ExponentialBackoffRetryCount) } + var decryptFn func(io.Reader) (io.ReadCloser, error) + if cmd.DecryptionPassword != nil { + passStr := *(cmd.DecryptionPassword) + if passStr != "" { + passwordAsBytes := []byte(passStr) + decryptFn = func(r io.Reader) (io.ReadCloser, error) { + return dcrypto.NewDecrypter(r, passwordAsBytes) + } + } + } + options := &drive.Options{ Path: path, Sources: sources, @@ -721,6 +736,7 @@ func (pCmd *pullCmd) Run(args []string, definedFlags map[string]*flag.Flag) { Match: *cmd.Matches, InTrash: *cmd.InTrash, ExponentialBackoffRetryCount: retryCount, + Decrypter: decryptFn, } if *cmd.Matches || *cmd.Starred { @@ -764,6 +780,7 @@ type pushCmd struct { FixClashes *bool `json:"fix-clashes"` Destination *string `json:"dest"` ExponentialBackoffRetryCount *int `json:"retry-count"` + EncryptionPassword *string `json:"encryption-password"` } func (cmd *pushCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { @@ -788,6 +805,7 @@ func (cmd *pushCmd) Flags(fs *flag.FlagSet) *flag.FlagSet { cmd.FixClashes = fs.Bool(drive.CLIOptionFixClashesKey, false, drive.DescFixClashes) cmd.Destination = fs.String(drive.CLIOptionPushDestination, "", drive.DescPushDestination) cmd.ExponentialBackoffRetryCount = fs.Int(drive.CLIOptionRetryCount, drive.MaxFailedRetryCount, drive.DescExponentialBackoffRetryCount) + cmd.EncryptionPassword = fs.String(drive.CLIEncryptionPassword, "", drive.DescEncryptionPassword) return fs } @@ -939,6 +957,17 @@ func (pCmd *pushCmd) createPushOptions(absEntryPath string, definedFlags map[str retryCount = *(cmd.ExponentialBackoffRetryCount) } + var encryptFn func(io.Reader) (io.Reader, error) + if cmd.EncryptionPassword != nil { + passStr := *(cmd.EncryptionPassword) + if passStr != "" { + passwordAsBytes := []byte(passStr) + encryptFn = func(r io.Reader) (io.Reader, error) { + return dcrypto.NewEncrypter(r, passwordAsBytes) + } + } + } + opts := &drive.Options{ Force: *cmd.Force, Hidden: *cmd.Hidden, @@ -957,6 +986,7 @@ func (pCmd *pushCmd) createPushOptions(absEntryPath string, definedFlags map[str Depth: *cmd.Depth, FixClashes: *cmd.FixClashes, Destination: *cmd.Destination, + Encrypter: encryptFn, ExponentialBackoffRetryCount: retryCount, } diff --git a/src/commands.go b/src/commands.go index 8e2f874c..fae4232d 100644 --- a/src/commands.go +++ b/src/commands.go @@ -16,6 +16,7 @@ package drive import ( "errors" + "io" "os" "path" "path/filepath" @@ -92,6 +93,9 @@ type Options struct { Destination string RenameMode RenameMode ExponentialBackoffRetryCount int + + Encrypter func(io.Reader) (io.Reader, error) + Decrypter func(io.Reader) (io.ReadCloser, error) } type Commands struct { diff --git a/src/help.go b/src/help.go index 46d54aaf..b40104a4 100644 --- a/src/help.go +++ b/src/help.go @@ -191,6 +191,8 @@ const ( DescSkipContentCheck = "skip diffing actual body content, show only name, time, type changes" DescPushDestination = "specify the final destination of the contents of an operation" DescExponentialBackoffRetryCount = "max number of retries for exponential backoff" + DescEncryptionPassword = "encryption password" + DescDecryptionPassword = "decryption password" ) const ( @@ -234,6 +236,8 @@ const ( CLIOptionRenameLocal = "local" CLIOptionRenameRemote = "remote" CLIOptionRetryCount = "retry-count" + CLIEncryptionPassword = "encryption-password" + CLIDecryptionPassword = "decryption-password" ) const ( diff --git a/src/pull.go b/src/pull.go index 3a3afe62..6c519f06 100644 --- a/src/pull.go +++ b/src/pull.go @@ -90,6 +90,9 @@ func (g *Commands) PullMatchLike() error { } func pull(g *Commands, pt pullType) error { + g.rem.encrypter = g.opts.Encrypter + g.rem.decrypter = g.opts.Decrypter + cl, clashes, err := pullLikeResolve(g, pt) if len(clashes) >= 1 { @@ -273,6 +276,9 @@ func (g *Commands) pullLikeMatchesResolver(pt pullType) (cl, clashes []*Change, } func (g *Commands) PullPiped(byId bool) (err error) { + g.rem.encrypter = g.opts.Encrypter + g.rem.decrypter = g.opts.Decrypter + resolver := g.rem.FindByPathM if byId { resolver = g.rem.FindByIdM diff --git a/src/push.go b/src/push.go index 7dcd129c..07b8fdf6 100644 --- a/src/push.go +++ b/src/push.go @@ -35,6 +35,9 @@ var mkdirAllMu = sync.Mutex{} // directory, it recursively pushes to the remote if there are local changes. // It doesn't check if there are local changes if isForce is set. func (g *Commands) Push() (err error) { + g.rem.encrypter = g.opts.Encrypter + g.rem.decrypter = g.opts.Decrypter + defer g.clearMountPoints() var cl []*Change @@ -181,6 +184,9 @@ func (g *Commands) resolveConflicts(cl []*Change, push bool) (*[]*Change, *[]*Ch } func (g *Commands) PushPiped() (err error) { + g.rem.encrypter = g.opts.Encrypter + g.rem.decrypter = g.opts.Decrypter + // Cannot push asynchronously because the push order must be maintained for _, relToRootPath := range g.opts.Sources { rem, resErr := g.rem.FindByPath(relToRootPath) diff --git a/src/remote.go b/src/remote.go index 96575183..3c49dc6e 100644 --- a/src/remote.go +++ b/src/remote.go @@ -85,6 +85,8 @@ func errCannotMkdirAll(p string) error { type Remote struct { client *http.Client service *drive.Service + encrypter func(io.Reader) (io.Reader, error) + decrypter func(io.Reader) (io.ReadCloser, error) progressChan chan int } @@ -569,6 +571,15 @@ func (r *Remote) Download(id string, exportURL string) (io.ReadCloser, error) { } } + if r.decrypter != nil && body != nil { + decR, err := r.decrypter(body) + _ = body.Close() + if err != nil { + return nil, err + } + body = decR + } + return body, err } @@ -666,6 +677,15 @@ func (r *Remote) upsertByComparison(body io.Reader, args *upsertOpt) (f *File, m uploaded.MimeType = DriveFolderMimeType } + if r.encrypter != nil && body != nil { + encR, encErr := r.encrypter(body) + if encErr != nil { + err = encErr + return + } + body = encR + } + if args.src.MimeType != "" { uploaded.MimeType = args.src.MimeType }