From 27c988ca384b142f8bb58385a36f1659b18dfd4b Mon Sep 17 00:00:00 2001 From: Shulhan Date: Wed, 20 Sep 2023 21:25:46 +0700 Subject: all: implement method to decrypt file using private key The Decrypt method decrypt the file using private key from file "{{.BaseDir}}/.awwan.key". The encrypted file must have extension ".vault", otherwise it will return an error. The decrypted file output will be written in the same directory without the ".vault" extension in filePlain. --- .gitignore | 1 + awwan.go | 55 +++++++++++++++++ awwan_test.go | 68 +++++++++++++++++++++ testdata/decrypt-with-passphrase/.awwan.env.vault | Bin 0 -> 384 bytes testdata/decrypt-with-passphrase/.awwan.key | 1 + testdata/decrypt-with-passphrase/.ssh/empty | 0 testdata/decrypt-wrong-privatekey/.awwan.env.vault | 1 + testdata/decrypt-wrong-privatekey/.awwan.key | 39 ++++++++++++ testdata/decrypt-wrong-privatekey/.ssh/empty | 0 9 files changed, 165 insertions(+) create mode 100644 testdata/decrypt-with-passphrase/.awwan.env.vault create mode 120000 testdata/decrypt-with-passphrase/.awwan.key create mode 100644 testdata/decrypt-with-passphrase/.ssh/empty create mode 120000 testdata/decrypt-wrong-privatekey/.awwan.env.vault create mode 100644 testdata/decrypt-wrong-privatekey/.awwan.key create mode 100644 testdata/decrypt-wrong-privatekey/.ssh/empty diff --git a/.gitignore b/.gitignore index 96957aa..9c0deff 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ /cover.out /testdata/encrypt-with-passphrase/.awwan.env.vault /testdata/encrypt-without-passphrase/.awwan.env.vault +/testdata/decrypt-with-passphrase/.awwan.env diff --git a/awwan.go b/awwan.go index 5479778..cd28c83 100644 --- a/awwan.go +++ b/awwan.go @@ -14,6 +14,7 @@ import ( "log" "os" "path/filepath" + "strings" "git.sr.ht/~shulhan/awwan/internal" libcrypto "github.com/shuLhan/share/lib/crypto" @@ -42,6 +43,9 @@ const ( defTmpDir = "/tmp" ) +// defEncryptExt default file extension for encrypted file. +const defEncryptExt = `.vault` + // defFilePrivateKey define the default private key file name. const defFilePrivateKey = `.awwan.key` @@ -104,6 +108,57 @@ func New(baseDir string) (aww *Awwan, err error) { return aww, nil } +// Decrypt the file using private key from file "{{.BaseDir}}/.awwan.key". +// The encrypted file must have extension ".vault", otherwise it will return +// an error. +// The decrypted file output will be written in the same directory without +// the ".vault" extension in filePlain. +func (aww *Awwan) Decrypt(fileVault string) (filePlain string, err error) { + var ( + logp = `Decrypt` + ext = filepath.Ext(fileVault) + ) + + if ext != defEncryptExt { + return ``, fmt.Errorf(`%s: invalid extension, expecting %s, got %s`, logp, defEncryptExt, ext) + } + + if aww.privateKey == nil { + err = aww.loadPrivateKey() + if err != nil { + return ``, fmt.Errorf(`%s: %w`, logp, err) + } + } + + var ciphertext []byte + + ciphertext, err = os.ReadFile(fileVault) + if err != nil { + return ``, fmt.Errorf(`%s: %w`, logp, err) + } + + var ( + hash = sha256.New() + label = []byte(`awwan`) + + plaintext []byte + ) + + plaintext, err = rsa.DecryptOAEP(hash, rand.Reader, aww.privateKey, ciphertext, label) + if err != nil { + return ``, fmt.Errorf(`%s: %w`, logp, err) + } + + filePlain = strings.TrimSuffix(fileVault, defEncryptExt) + + err = os.WriteFile(filePlain, plaintext, 0600) + if err != nil { + return ``, fmt.Errorf(`%s: %w`, logp, err) + } + + return filePlain, nil +} + // Encrypt the file using private key from file "{{.BaseDir}}/.awwan.key". // The encrypted file output will be on the same file path with ".vault" // extension. diff --git a/awwan_test.go b/awwan_test.go index a216fbf..e1f8b4d 100644 --- a/awwan_test.go +++ b/awwan_test.go @@ -9,6 +9,74 @@ import ( "github.com/shuLhan/share/lib/test/mock" ) +func TestAwwanDecrypt(t *testing.T) { + type testCase struct { + baseDir string + fileVault string + passphrase string + expError string + } + + var cases = []testCase{{ + baseDir: filepath.Join(`testdata`, `decrypt-with-passphrase`), + fileVault: `.awwan.env`, + expError: `Decrypt: invalid extension, expecting .vault, got .env`, + }, { + baseDir: filepath.Join(`testdata`, `decrypt-with-passphrase`), + fileVault: `.awwan.env.vault`, + passphrase: "invalidpassphrase\r", + expError: `Decrypt: LoadPrivateKeyInteractive: x509: decryption password incorrect`, + }, { + baseDir: filepath.Join(`testdata`, `decrypt-with-passphrase`), + fileVault: `.awwan.env.vault`, + passphrase: "s3cret\r", + }, { + baseDir: filepath.Join(`testdata`, `decrypt-wrong-privatekey`), + fileVault: `.awwan.env.vault`, + passphrase: "news3cret\r", + expError: `Decrypt: crypto/rsa: decryption error`, + }} + + var ( + mockrw = mock.ReadWriter{} + + c testCase + aww *Awwan + err error + filePlain string + fileVault string + ) + + for _, c = range cases { + fileVault = filepath.Join(c.baseDir, c.fileVault) + + aww, err = New(c.baseDir) + if err != nil { + t.Fatal(err) + } + + if len(c.passphrase) != 0 { + // Write the passphrase to standard input to be read + // interactively. + mockrw.BufRead.WriteString(c.passphrase) + aww.termrw = &mockrw + } else { + aww.termrw = nil + } + + filePlain, err = aww.Decrypt(fileVault) + if err != nil { + test.Assert(t, `Decrypt`, c.expError, err.Error()) + continue + } + + _, err = os.Stat(filePlain) + if err != nil { + t.Fatal(err) + } + } +} + func TestAwwanEncrypt(t *testing.T) { type testCase struct { baseDir string diff --git a/testdata/decrypt-with-passphrase/.awwan.env.vault b/testdata/decrypt-with-passphrase/.awwan.env.vault new file mode 100644 index 0000000..41960cd Binary files /dev/null and b/testdata/decrypt-with-passphrase/.awwan.env.vault differ diff --git a/testdata/decrypt-with-passphrase/.awwan.key b/testdata/decrypt-with-passphrase/.awwan.key new file mode 120000 index 0000000..aa99eff --- /dev/null +++ b/testdata/decrypt-with-passphrase/.awwan.key @@ -0,0 +1 @@ +../encrypt-with-passphrase/.awwan.key \ No newline at end of file diff --git a/testdata/decrypt-with-passphrase/.ssh/empty b/testdata/decrypt-with-passphrase/.ssh/empty new file mode 100644 index 0000000..e69de29 diff --git a/testdata/decrypt-wrong-privatekey/.awwan.env.vault b/testdata/decrypt-wrong-privatekey/.awwan.env.vault new file mode 120000 index 0000000..1d98464 --- /dev/null +++ b/testdata/decrypt-wrong-privatekey/.awwan.env.vault @@ -0,0 +1 @@ +../encrypt-with-passphrase/.awwan.env.vault \ No newline at end of file diff --git a/testdata/decrypt-wrong-privatekey/.awwan.key b/testdata/decrypt-wrong-privatekey/.awwan.key new file mode 100644 index 0000000..bf2f333 --- /dev/null +++ b/testdata/decrypt-wrong-privatekey/.awwan.key @@ -0,0 +1,39 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +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 +pVmomw== +-----END OPENSSH PRIVATE KEY----- diff --git a/testdata/decrypt-wrong-privatekey/.ssh/empty b/testdata/decrypt-wrong-privatekey/.ssh/empty new file mode 100644 index 0000000..e69de29 -- cgit v1.3