diff --git a/.github/integration/tests/tests.sh b/.github/integration/tests/tests.sh index 2180e10f..92b97138 100755 --- a/.github/integration/tests/tests.sh +++ b/.github/integration/tests/tests.sh @@ -168,7 +168,7 @@ else fi # Decrypt file -./sda-cli decrypt -key sda_key.sec.pem downloads/data_file.c4gh +C4GH_PASSWORD="" ./sda-cli decrypt -key sda_key.sec.pem downloads/data_file.c4gh if [ -f downloads/data_file ]; then echo "Decrypted data file" @@ -204,7 +204,7 @@ check_encypted_file "data_file_keys.c4gh" for key in sda_key sda_key2 do rm data_file_keys - ./sda-cli decrypt -key $key.sec.pem data_file_keys.c4gh + C4GH_PASSWORD="" ./sda-cli decrypt -key $key.sec.pem data_file_keys.c4gh if [ -f data_file_keys ]; then echo "Decrypted data file" else @@ -222,7 +222,7 @@ check_encypted_file "data_file_keys.c4gh" for key in sda_key1 sda_key2 do rm data_file_keys - ./sda-cli decrypt -key $key.sec.pem data_file_keys.c4gh + C4GH_PASSWORD="" ./sda-cli decrypt -key $key.sec.pem data_file_keys.c4gh if [ -f data_file_keys ]; then echo "Decrypted data file" else @@ -240,7 +240,7 @@ check_encypted_file "data_file_keys.c4gh" for key in sda_key sda_key1 sda_key2 do rm data_file_keys - ./sda-cli decrypt -key $key.sec.pem data_file_keys.c4gh + C4GH_PASSWORD="" ./sda-cli decrypt -key $key.sec.pem data_file_keys.c4gh if [ -f data_file_keys ]; then echo "Decrypted data file" else diff --git a/decrypt/decrypt.go b/decrypt/decrypt.go index 539ae50d..3f286967 100644 --- a/decrypt/decrypt.go +++ b/decrypt/decrypt.go @@ -12,7 +12,6 @@ import ( "github.com/NBISweden/sda-cli/helpers" "github.com/neicnordic/crypt4gh/keys" "github.com/neicnordic/crypt4gh/streaming" - log "github.com/sirupsen/logrus" ) // Help text and command line flags. @@ -20,7 +19,7 @@ import ( // Usage text that will be displayed as command line help text when using the // `help decrypt` command var Usage = ` -USAGE: %s decrypt -key [file(s)] +USAGE: %s decrypt -key (--force-overwrite) [file(s)] decrypt: Decrypts files from the Sensitive Data Archive (SDA) with the @@ -39,13 +38,12 @@ var ArgHelp = ` // main program help var Args = flag.NewFlagSet("decrypt", flag.ExitOnError) -var privateKeyFile = Args.String("key", "", - "Private key to use for decrypting files.") +var privateKeyFile = Args.String("key", "", "Private key to use for decrypting files.") +var forceOverwrite = Args.Bool("force-overwrite", false, "Force overwrite existing files.") // Decrypt takes a set of arguments, parses them, and attempts to decrypt the // given data files with the given private key file.. func Decrypt(args []string) error { - // Call ParseArgs to take care of all the flag parsing err := helpers.ParseArgs(args, Args) if err != nil { @@ -57,10 +55,8 @@ func Decrypt(args []string) error { // All filenames are read into a struct together with their output filenames files := []helpers.EncryptionFileSet{} for _, filename := range Args.Args() { - // Set directory for the output file unencryptedFilename := strings.TrimSuffix(filename, ".c4gh") - files = append(files, helpers.EncryptionFileSet{Encrypted: filename, Unencrypted: unencryptedFilename}) } @@ -69,37 +65,38 @@ func Decrypt(args []string) error { return errors.New("a private key is required to decrypt data") } - var privateKey *[32]byte - - // try reading private key without password - privateKey, err = readPrivateKeyFile(*privateKeyFile, "") - if err != nil { - - // if there was an error, try again with the password - password, err := getPassword("C4GH_PASSWORD") - if err != nil { - return err - } - - // Loading private key file - privateKey, err = readPrivateKeyFile(*privateKeyFile, password) + password, available := os.LookupEnv("C4GH_PASSWORD") + if !available { + password, err = helpers.PromptPassword("Enter password to unlock private key") if err != nil { return err } } - // Check that all the encrypted files exist, and all the unencrypted don't - err = checkFiles(files) + // Loading private key file + privateKey, err := readPrivateKeyFile(*privateKeyFile, password) if err != nil { return err } + // Check that all the encrypted files exist, and all the unencrypted don't + for _, file := range files { + // check that the input file exists and is readable + if !helpers.FileIsReadable(file.Encrypted) { + return fmt.Errorf("cannot read input file %s", file.Encrypted) + } + + // check that the output file doesn't exist + if helpers.FileExists(file.Unencrypted) && !*forceOverwrite { + return fmt.Errorf("outfile %s already exists", file.Unencrypted) + } + } + // decrypt the input files numFiles := len(files) for i, file := range files { - log.Infof("Decrypting file %v/%v: %s", i+1, numFiles, file.Encrypted) - - err = decrypt(file.Encrypted, file.Unencrypted, *privateKey) + fmt.Printf("Decrypting file %v/%v: %s\n", i+1, numFiles, file.Encrypted) + err = decryptFile(file.Encrypted, file.Unencrypted, *privateKey) if err != nil { return err } @@ -108,31 +105,13 @@ func Decrypt(args []string) error { return nil } -// getPassword will check if the `envVar` environment variable is set, and -// return its value if present. Otherwise, the password will be read from a user -// prompt. -func getPassword(envVar string) (string, error) { - // check if there is a password available in the `envVar` env variable - password, available := os.LookupEnv(envVar) - if available { - return password, nil - } - - // otherwise, read the password from a user prompt - password, err := helpers.PromptPassword("Enter password to unlock private key") - - return password, err -} - // Reads a private key file from a file using the crypt4gh keys module func readPrivateKeyFile(filename, password string) (key *[32]byte, err error) { - // Check that the file exists if !helpers.FileExists(filename) { return nil, fmt.Errorf("private key file %s doesn't exist", filename) } - log.Info("Reading Private key file") file, err := os.Open(filepath.Clean(filename)) if err != nil { return nil, err @@ -146,48 +125,15 @@ func readPrivateKeyFile(filename, password string) (key *[32]byte, err error) { return &privateKey, err } -// Checks that all the encrypted files exists, and are readable, and that the -// unencrypted files do not exist -func checkFiles(files []helpers.EncryptionFileSet) error { - log.Info("Checking files") - for _, file := range files { - // check that the input file exists and is readable - if !helpers.FileIsReadable(file.Encrypted) { - return fmt.Errorf("cannot read input file %s", file.Encrypted) - } - - // check that the output file doesn't exist - if helpers.FileExists(file.Unencrypted) { - return fmt.Errorf("outfile %s already exists", file.Unencrypted) - } - } - - return nil -} - // decrypts the data in `filename` with the given `privateKey`, writing the // resulting data to `outfile`. -func decrypt(filename, outfileName string, privateKey [32]byte) error { - - // check that the infile exists, and the the outfile doesn't exist - if !helpers.FileIsReadable(filename) { - return fmt.Errorf("infile %s does not exist or could not be read", filename) - } - - if helpers.FileExists(outfileName) { - return fmt.Errorf("outfile %s already exists", outfileName) - } - +func decryptFile(filename, outfileName string, privateKey [32]byte) error { // open input file for reading inFile, err := os.Open(filepath.Clean(filename)) if err != nil { return err } - defer func() { - if err := inFile.Close(); err != nil { - log.Errorf("error closing file: %s\n", err) - } - }() + defer inFile.Close() // Create crypt4gh reader crypt4GHReader, err := streaming.NewCrypt4GHReader(inFile, privateKey, nil) diff --git a/decrypt/decrypt_test.go b/decrypt/decrypt_test.go index 167a99f1..a8c2f283 100644 --- a/decrypt/decrypt_test.go +++ b/decrypt/decrypt_test.go @@ -5,12 +5,11 @@ import ( "io" "os" "path/filepath" + "runtime" "testing" createKey "github.com/NBISweden/sda-cli/create_key" "github.com/NBISweden/sda-cli/encrypt" - "github.com/NBISweden/sda-cli/helpers" - log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" ) @@ -27,29 +26,20 @@ func TestDecryptTestSuite(t *testing.T) { } func (suite *DecryptTests) SetupTest() { - var err error - // Create a temporary directory for our files suite.tempDir, err = os.MkdirTemp(os.TempDir(), "sda-cli-test-") - if err != nil { - log.Error("Couldn't create temporary test directory", err) - } + assert.NoError(suite.T(), err) // create a test file... suite.testFile, err = os.CreateTemp(suite.tempDir, "testfile-") - if err != nil { - log.Error("cannot create temporary public key file", err) - } + assert.NoError(suite.T(), err) // ... create some content ... suite.fileContent = []byte("This is some fine content right here.") - // ... and write the known content to it err = os.WriteFile(suite.testFile.Name(), suite.fileContent, 0600) - if err != nil { - log.Errorf("failed to write to testfile: %s", err) - } + assert.NoError(suite.T(), err) } func (suite *DecryptTests) TearDownTest() { @@ -57,15 +47,10 @@ func (suite *DecryptTests) TearDownTest() { } func (suite *DecryptTests) TestreadPrivateKeyFile() { - testKeyFile := filepath.Join(suite.tempDir, "testkey") - // generate key files - - err := createKey.GenerateKeyPair(testKeyFile, "") - if err != nil { - log.Errorf("couldn't generate testing key pair: %s", err) - } + err := createKey.GenerateKeyPair(testKeyFile, "test") + assert.NoError(suite.T(), err) // Test reading a non-existent key _, err = readPrivateKeyFile(testKeyFile, "") @@ -75,91 +60,99 @@ func (suite *DecryptTests) TestreadPrivateKeyFile() { _, err = readPrivateKeyFile(suite.testFile.Name(), "") assert.ErrorContains(suite.T(), err, fmt.Sprintf("file: %s", suite.testFile.Name())) - // Test reading a real key - _, err = readPrivateKeyFile(fmt.Sprintf("%s.sec.pem", testKeyFile), "") - assert.NoError(suite.T(), err) -} - -func (suite *DecryptTests) TestcheckFiles() { - // unencrypted is readable, and unencrypted isn't (this is fine!) - testOk := helpers.EncryptionFileSet{Encrypted: suite.testFile.Name(), Unencrypted: "does-not-exist"} - err := checkFiles([]helpers.EncryptionFileSet{testOk}) - assert.NoError(suite.T(), err) + // Test reading a public key + _, err = readPrivateKeyFile(fmt.Sprintf("%s.pub.pem", testKeyFile), "") + assert.ErrorContains(suite.T(), err, "private key format not supported") - // unencrypted is readable, but encrypted exists - testHasEncrypted := helpers.EncryptionFileSet{Encrypted: suite.testFile.Name(), Unencrypted: suite.testFile.Name()} - err = checkFiles([]helpers.EncryptionFileSet{testHasEncrypted}) - assert.EqualError(suite.T(), err, fmt.Sprintf("outfile %s already exists", - suite.testFile.Name())) + // Test reading a real key with wrong passphrase + _, err = readPrivateKeyFile(fmt.Sprintf("%s.sec.pem", testKeyFile), "wrong") + assert.ErrorContains(suite.T(), err, "chacha20poly1305: message authentication failed") - // unencrypted isn't readable - testNoUnencrypted := helpers.EncryptionFileSet{Encrypted: "does-not-exist", Unencrypted: suite.testFile.Name()} - err = checkFiles([]helpers.EncryptionFileSet{testNoUnencrypted}) - assert.EqualError(suite.T(), err, "cannot read input file does-not-exist") + // Test reading a real key + _, err = readPrivateKeyFile(fmt.Sprintf("%s.sec.pem", testKeyFile), "test") + assert.NoError(suite.T(), err) } func (suite *DecryptTests) Testdecrypt() { - testKeyFile := filepath.Join(suite.tempDir, "testkey") encryptedFile := fmt.Sprintf("%s.c4gh", suite.testFile.Name()) decryptedFile := filepath.Join(suite.tempDir, "decrypted_file") // generate key files err := createKey.GenerateKeyPair(testKeyFile, "") - if err != nil { - log.Errorf("couldn't generate testing key pair: %s", err) - } + assert.NoError(suite.T(), err) // and read the private key privateKey, err := readPrivateKeyFile(fmt.Sprintf("%s.sec.pem", testKeyFile), "") - if err != nil { - log.Errorf("couldn't read test key: %s", err) - } + assert.NoError(suite.T(), err) // Encrypt a file using the encrypt module. change to the test directory to // make sure that the checksum files end up there. cwd, err := os.Getwd() - if err != nil { - log.Error("could not get working directory") - } + assert.NoError(suite.T(), err) err = os.Chdir(suite.tempDir) - if err != nil { - log.Error("could not change into test directory") - } - encryptArgs := []string{"sda-cli", "-key", fmt.Sprintf("%s.pub.pem", testKeyFile), suite.testFile.Name()} + assert.NoError(suite.T(), err) + encryptArgs := []string{"encrypt", "-key", fmt.Sprintf("%s.pub.pem", testKeyFile), suite.testFile.Name()} err = encrypt.Encrypt(encryptArgs) - if err != nil { - log.Errorf("couldn't encrypt file for decryption test: %s", err) - } + assert.NoError(suite.T(), err, "encrypting file for testing failed") err = os.Chdir(cwd) - if err != nil { - log.Error("could not return from test directory") - } + assert.NoError(suite.T(), err) // Test decrypting a non-existent file - err = decrypt(filepath.Join(suite.tempDir, "non-existent"), "output_file", *privateKey) - assert.EqualError(suite.T(), err, fmt.Sprintf("infile %s does not exist or could not be read", filepath.Join(suite.tempDir, "non-existent"))) - - // Test decrypting where the output file exists - err = decrypt(encryptedFile, suite.testFile.Name(), *privateKey) - assert.EqualError(suite.T(), err, fmt.Sprintf("outfile %s already exists", suite.testFile.Name())) + msg := "no such file or directory" + if runtime.GOOS == "windows" { + msg = "The system cannot find the file specified." + } + err = decryptFile(filepath.Join(suite.tempDir, "non-existent"), "output_file", *privateKey) + assert.ErrorContains(suite.T(), err, msg) // Test decryption with malformed key fakeKey := [32]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} - err = decrypt(encryptedFile, decryptedFile, fakeKey) + err = decryptFile(encryptedFile, decryptedFile, fakeKey) assert.EqualError(suite.T(), err, "could not create cryp4gh reader: could not find matching public key header, decryption failed") // Test decrypting with the real key - err = decrypt(encryptedFile, decryptedFile, *privateKey) + err = decryptFile(encryptedFile, decryptedFile, *privateKey) assert.NoError(suite.T(), err) // Check content of the decrypted file inFile, err := os.Open(decryptedFile) - if err != nil { - log.Errorf("Couldn't open decrypted file %s for content checking", decryptedFile) - } + assert.NoError(suite.T(), err, "unable to open decrypted file") fileData, err := io.ReadAll(inFile) - if err != nil { - log.Error("Couldn't read decrypted filedata for content checking") - } + assert.NoError(suite.T(), err, "unable to read decrypted file") assert.Equal(suite.T(), fileData, suite.fileContent) } + +func (suite *DecryptTests) TestDecrypt() { + testKeyFile := filepath.Join(suite.tempDir, "testkey") + err := createKey.GenerateKeyPair(testKeyFile, "") + assert.NoError(suite.T(), err) + + // Encrypt a file using the encrypt module. change to the test directory to + // make sure that the checksum files end up there. + cwd, err := os.Getwd() + assert.NoError(suite.T(), err) + err = os.Chdir(suite.tempDir) + assert.NoError(suite.T(), err) + encryptArgs := []string{"encrypt", "-key", fmt.Sprintf("%s.pub.pem", testKeyFile), suite.testFile.Name()} + assert.NoError(suite.T(), encrypt.Encrypt(encryptArgs), "encrypting file for testing failed") + assert.NoError(suite.T(), os.Chdir(cwd)) + os.Setenv("C4GH_PASSWORD", "") + if runtime.GOOS != "windows" { + assert.NoError(suite.T(), os.Remove(suite.testFile.Name())) + os.Args = []string{"decrypt", "-key", fmt.Sprintf("%s.sec.pem", testKeyFile), fmt.Sprintf("%s.c4gh", suite.testFile.Name())} + err = Decrypt(os.Args) + assert.NoError(suite.T(), err, "decrypt failed unexpectedly") + + // Check content of the decrypted file + inFile, err := os.Open(suite.testFile.Name()) + assert.NoError(suite.T(), err, "unable to open decrypted file") + fileData, err := io.ReadAll(inFile) + assert.NoError(suite.T(), err, "unable to read decrypted file") + assert.Equal(suite.T(), string(suite.fileContent), string(fileData)) + } + + os.Args = []string{"decrypt", "-key", fmt.Sprintf("%s.sec.pem", testKeyFile), "--force-overwrite", fmt.Sprintf("%s.c4gh", suite.testFile.Name())} + err = Decrypt(os.Args) + assert.NoError(suite.T(), err, "decrypt failed unexpectedly") + os.Args = nil +}