diff --git a/cmd/cli-uploader/cliapi/cliapi.go b/cmd/cli-uploader/cliapi/cliapi.go index 25fd920e..10961d7f 100644 --- a/cmd/cli-uploader/cliapi/cliapi.go +++ b/cmd/cli-uploader/cliapi/cliapi.go @@ -233,6 +233,24 @@ func DownloadFile(downloadParams cliflags.FlagConfig) error { fmt.Println("ERROR: Could not get file info or file does not exist") return err } + + // For E2E files, retrieve the per-file cipher and real filename + var e2eCipher []byte + if info.IsEndToEndEncrypted { + if len(e2eKey) == 0 { + return errors.New("file is end-to-end encrypted but no E2E key is configured - please re-run login") + } + var realName string + e2eCipher, realName, err = getE2eCipher(downloadParams.DownloadId) + if err != nil { + fmt.Println("ERROR: Could not retrieve E2E decryption key for this file") + return err + } + if downloadParams.FileName == "" { + info.Name = realName + } + } + if downloadParams.OutputPath == "" { downloadParams.OutputPath = "." } @@ -256,17 +274,6 @@ func DownloadFile(downloadParams cliflags.FlagConfig) error { return err } } - helper.CreateDir(downloadParams.OutputPath) - file, err := os.Create(downloadParams.OutputPath + "/" + downloadParams.FileName) - defer file.Close() - if err != nil { - fmt.Println("ERROR: Could not create new file") - return err - } - - if !downloadParams.JsonOutput { - progressBar = progressbar.DefaultBytes(info.SizeBytes, "Downloading") - } req, err := http.NewRequest("GET", gokapiUrl+"/files/download/"+downloadParams.DownloadId, nil) if err != nil { @@ -286,13 +293,36 @@ func DownloadFile(downloadParams cliflags.FlagConfig) error { os.Exit(4) } + helper.CreateDir(downloadParams.OutputPath) + file, err := os.Create(filename) + if err != nil { + fmt.Println("ERROR: Could not create new file") + return err + } + defer file.Close() + + if !downloadParams.JsonOutput { + progressBar = progressbar.DefaultBytes(info.SizeBytes, "Downloading") + } + + // For E2E files, wrap the response body in a decryption reader + var body io.Reader = resp.Body + if info.IsEndToEndEncrypted { + body, err = encryption.GetDecryptReader(e2eCipher, resp.Body) + if err != nil { + os.Remove(filename) + return err + } + } + if !downloadParams.JsonOutput { - _, err = io.Copy(file, io.TeeReader(resp.Body, progressBar)) + _, err = io.Copy(file, io.TeeReader(body, progressBar)) } else { - _, err = io.Copy(file, resp.Body) + _, err = io.Copy(file, body) } if err != nil { + os.Remove(filename) fmt.Println("ERROR: Could not download file") return err } @@ -310,6 +340,20 @@ func DownloadFile(downloadParams cliflags.FlagConfig) error { return nil } +// getE2eCipher retrieves the per-file cipher and real filename for an E2E encrypted file +func getE2eCipher(fileId string) ([]byte, string, error) { + e2eInfo, err := GetE2eInfo() + if err != nil { + return nil, "", err + } + for _, f := range e2eInfo.Files { + if f.Id == fileId { + return f.Cipher, f.Filename, nil + } + } + return nil, "", errors.New("file not found in E2E metadata") +} + func nameToBase64(f *os.File, uploadParams cliflags.FlagConfig) string { return "base64:" + base64.StdEncoding.EncodeToString([]byte(getFileName(f, uploadParams))) } diff --git a/cmd/cli-uploader/cliflags/cliflags.go b/cmd/cli-uploader/cliflags/cliflags.go index a0bcffcd..9743086a 100644 --- a/cmd/cli-uploader/cliflags/cliflags.go +++ b/cmd/cli-uploader/cliflags/cliflags.go @@ -167,9 +167,12 @@ func checkRequiredUploadParameter(config *FlagConfig, mode int) bool { if mode == ModeUpload && config.File != "" { return true } - if mode == ModeDownload && config.DownloadId == "" { - fmt.Println("ERROR: Missing parameter --id") - return false + if mode == ModeDownload { + if config.DownloadId == "" { + fmt.Println("ERROR: Missing parameter --id") + return false + } + return true } if !environment.IsDockerInstance() { diff --git a/internal/encryption/Encryption.go b/internal/encryption/Encryption.go index e18eabd4..42e8355d 100644 --- a/internal/encryption/Encryption.go +++ b/internal/encryption/Encryption.go @@ -39,6 +39,12 @@ const EndToEndEncryption = 5 var encryptedKey, ramCipher []byte +// IsDecryptionAvailable returns true if the master encryption key has been +// loaded into memory, meaning server-side decryption is possible. +func IsDecryptionAvailable() bool { + return len(ramCipher) > 0 +} + const blockSize = 32 const nonceSize = 12 diff --git a/internal/storage/filesystem/s3filesystem/aws/Aws.go b/internal/storage/filesystem/s3filesystem/aws/Aws.go index 7b451515..01a9863c 100644 --- a/internal/storage/filesystem/s3filesystem/aws/Aws.go +++ b/internal/storage/filesystem/s3filesystem/aws/Aws.go @@ -223,7 +223,14 @@ func serveDecryptedFile(w http.ResponseWriter, file models.File) error { defer obj.Body.Close() headers.Write(file, w, true, true) - return encryption.DecryptReader(file.Encryption, obj.Body, w) + if file.Encryption.IsEncrypted { + if !encryption.IsDecryptionAvailable() { + return errors.New("file is encrypted but server-side decryption key is not available") + } + return encryption.DecryptReader(file.Encryption, obj.Body, w) + } + _, err = io.Copy(w, obj.Body) + return err } func getTimeoutContext() (context.Context, context.CancelFunc) { diff --git a/internal/webserver/api/Api.go b/internal/webserver/api/Api.go index f643a696..65a5cf0d 100644 --- a/internal/webserver/api/Api.go +++ b/internal/webserver/api/Api.go @@ -598,7 +598,8 @@ func apiDownloadSingle(w http.ResponseWriter, r requestParser, user models.User) return } if !request.PresignUrl { - storage.ServeFile(file, w, request.WebRequest, true, request.IncreaseCounter, true) + forceDecryption := !file.Encryption.IsEndToEndEncrypted + storage.ServeFile(file, w, request.WebRequest, true, request.IncreaseCounter, forceDecryption) return } createAndOutputPresignedUrl([]string{file.Id}, w, "") @@ -635,9 +636,6 @@ func checkDownloadAllowed(fileId string, user models.User) (models.File, int, in if file.UserId != user.Id && !user.HasPermission(models.UserPermListOtherUploads) { return models.File{}, http.StatusUnauthorized, errorcodes.NoPermission, "no permission to download file" } - if file.Encryption.IsEndToEndEncrypted { - return models.File{}, http.StatusBadRequest, errorcodes.EndToEndNotSupported, "End-to-end encrypted files cannot be downloaded" - } return file, 0, 0, "" }