diff options
| author | Shulhan <ms@kilabit.info> | 2023-09-24 02:13:15 +0700 |
|---|---|---|
| committer | Shulhan <ms@kilabit.info> | 2023-09-26 00:24:08 +0700 |
| commit | 9ae9a42e37b35e17120045da8bb72b07f6de2a44 (patch) | |
| tree | 32613a49f0d0f1e2d68ab6475745f791dba9cb37 | |
| parent | 8cc52027d243946c03c6b0d1016ca7cc3d7de09a (diff) | |
| download | awwan-9ae9a42e37b35e17120045da8bb72b07f6de2a44.tar.xz | |
all: move fields and methods related to encryption to struct cryptoContext
The cryptoContext contains the default hash, loaded privateKey, dummy
terminal, base directory, and default label; all of those fields are
required for encryption and decryption.
The cryptoContext have three methods: encrypt, decrypt, and
loadPrivateKey.
By moving to separate struct the cryptoContext instance can be shared
with Session.
| -rw-r--r-- | awwan.go | 98 | ||||
| -rw-r--r-- | awwan_test.go | 48 | ||||
| -rw-r--r-- | crypto_context.go | 128 | ||||
| -rw-r--r-- | session.go | 13 |
4 files changed, 164 insertions, 123 deletions
@@ -5,21 +5,13 @@ package awwan import ( "bytes" - "crypto" - "crypto/rand" - "crypto/rsa" - "crypto/sha256" - "errors" "fmt" - "io" - "io/fs" "log" "os" "path/filepath" "strings" "git.sr.ht/~shulhan/awwan/internal" - libcrypto "github.com/shuLhan/share/lib/crypto" "github.com/shuLhan/share/lib/http" "github.com/shuLhan/share/lib/memfs" "github.com/shuLhan/share/lib/ssh/config" @@ -64,29 +56,19 @@ var ( newLine = []byte("\n") ) -// errPrivateKeyMissing returned when private key file is missing or not -// loaded when command require loading encrypted file. -var errPrivateKeyMissing = errors.New(`private key is missing or not loaded`) - // Awwan is the service that run script in local or remote. // Awwan contains cache of sessions and cache of environment files. type Awwan struct { BaseDir string + cryptoc *cryptoContext + // All the Host values from SSH config files. sshConfig *config.Config httpd *http.Server // The HTTP server. memfsBase *memfs.MemFS // The files caches. - // privateKey define the key for encrypt and decrypt command. - privateKey *rsa.PrivateKey - - // termrw define the ReadWriter to prompt and read passphrase for - // privateKey. - // This field should be nil, only used during testing. - termrw io.ReadWriter - bufout bytes.Buffer buferr bytes.Buffer } @@ -124,10 +106,7 @@ func (aww *Awwan) init(baseDir string) (err error) { fmt.Printf("--- BaseDir: %s\n", aww.BaseDir) - err = aww.loadPrivateKey() - if err != nil { - return err - } + aww.cryptoc = newCryptoContext(aww.BaseDir) return nil } @@ -156,7 +135,7 @@ func (aww *Awwan) Decrypt(fileVault string) (filePlain string, err error) { var plaintext []byte - plaintext, err = decrypt(aww.privateKey, ciphertext) + plaintext, err = aww.cryptoc.decrypt(ciphertext) if err != nil { return ``, fmt.Errorf(`%s: %w`, logp, err) } @@ -177,10 +156,6 @@ func (aww *Awwan) Decrypt(fileVault string) (filePlain string, err error) { func (aww *Awwan) Encrypt(file string) (fileVault string, err error) { var logp = `Encrypt` - if aww.privateKey == nil { - return ``, fmt.Errorf(`%s: %w`, logp, errPrivateKeyMissing) - } - var src []byte src, err = os.ReadFile(file) @@ -188,14 +163,9 @@ func (aww *Awwan) Encrypt(file string) (fileVault string, err error) { return ``, fmt.Errorf(`%s: %w`, logp, err) } - var ( - hash = sha256.New() - label = []byte(`awwan`) - - ciphertext []byte - ) + var ciphertext []byte - ciphertext, err = libcrypto.EncryptOaep(hash, rand.Reader, &aww.privateKey.PublicKey, src, label) + ciphertext, err = aww.cryptoc.encrypt(src) if err != nil { return ``, fmt.Errorf(`%s: %w`, logp, err) } @@ -448,62 +418,6 @@ func (aww *Awwan) loadSshConfig() (err error) { return nil } -// loadPrivateKey from file "{{.BaseDir}}/.awwan.key" if its exist. -func (aww *Awwan) loadPrivateKey() (err error) { - var ( - fileKey = filepath.Join(aww.BaseDir, defFilePrivateKey) - - pkey crypto.PrivateKey - ok bool - ) - - _, err = os.Stat(fileKey) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return nil - } - return err - } - - fmt.Printf("--- Loading private key file %q (enter to skip passphrase) ...\n", fileKey) - - pkey, err = libcrypto.LoadPrivateKeyInteractive(aww.termrw, fileKey) - if err != nil { - if errors.Is(err, libcrypto.ErrEmptyPassphrase) { - // Ignore empty passphrase error, in case the - // command does not need to decrypt files when - // running. - return nil - } - return err - } - - aww.privateKey, ok = pkey.(*rsa.PrivateKey) - if !ok { - return fmt.Errorf(`the private key type must be RSA, got %T`, pkey) - } - - return nil -} - -func decrypt(pkey *rsa.PrivateKey, cipher []byte) (plain []byte, err error) { - if pkey == nil { - return nil, errPrivateKeyMissing - } - - var ( - hash = sha256.New() - label = []byte(`awwan`) - ) - - plain, err = libcrypto.DecryptOaep(hash, rand.Reader, pkey, cipher, label) - if err != nil { - return nil, err - } - - return plain, nil -} - // lookupBaseDir find the directory that contains ".ssh" directory from // current working directory until "/", as the base working directory of // awwan. diff --git a/awwan_test.go b/awwan_test.go index 1d9a23c..6438ea2 100644 --- a/awwan_test.go +++ b/awwan_test.go @@ -59,17 +59,17 @@ func TestAwwanDecrypt(t *testing.T) { var aww = Awwan{} fileVault = filepath.Join(c.baseDir, c.fileVault) - // Write the passphrase to standard input to be read - // interactively. - mockrw.BufRead.WriteString(c.passphrase) - aww.termrw = &mockrw - err = aww.init(c.baseDir) if err != nil { test.Assert(t, `Decrypt`, c.expError, err.Error()) continue } + // Write the passphrase to standard input to be read + // interactively. + mockrw.BufRead.WriteString(c.passphrase) + aww.cryptoc.termrw = &mockrw + filePlain, err = aww.Decrypt(fileVault) if err != nil { test.Assert(t, `Decrypt`, c.expError, err.Error()) @@ -103,12 +103,12 @@ func TestAwwanEncrypt(t *testing.T) { baseDir: filepath.Join(`testdata`, `encrypt-with-passphrase`), file: `.awwan.env`, passphrase: "invalids3cret\r", - expError: `LoadPrivateKeyInteractive: x509: decryption password incorrect`, + expError: `Encrypt: LoadPrivateKeyInteractive: x509: decryption password incorrect`, }, { baseDir: filepath.Join(`testdata`, `encrypt-without-rsa`), file: `.awwan.env`, passphrase: "s3cret\r", - expError: `the private key type must be RSA, got *ed25519.PrivateKey`, + expError: `Encrypt: the private key type must be RSA, got *ed25519.PrivateKey`, }, { baseDir: filepath.Join(`testdata`, `encrypt-without-passphrase`), file: `.awwan.env`, @@ -127,17 +127,17 @@ func TestAwwanEncrypt(t *testing.T) { var aww = Awwan{} filePlain = filepath.Join(c.baseDir, c.file) - // Write the passphrase to standard input to be read - // interactively. - mockrw.BufRead.WriteString(c.passphrase) - aww.termrw = &mockrw - err = aww.init(c.baseDir) if err != nil { test.Assert(t, `Encrypt`, c.expError, err.Error()) continue } + // Write the passphrase to standard input to be read + // interactively. + mockrw.BufRead.WriteString(c.passphrase) + aww.cryptoc.termrw = &mockrw + fileVault, err = aww.Encrypt(filePlain) if err != nil { test.Assert(t, `Encrypt`, c.expError, err.Error()) @@ -176,15 +176,15 @@ func TestAwwanLocal_withEncryption(t *testing.T) { aww = Awwan{} ) - // Mock terminal to read passphrase for private key. - mockrw.BufRead.WriteString("s3cret\r") - aww.termrw = &mockrw - err = aww.init(basedir) if err != nil { t.Fatal(err) } + // Mock terminal to read passphrase for private key. + mockrw.BufRead.WriteString("s3cret\r") + aww.cryptoc.termrw = &mockrw + var cases = []testCase{{ script: filepath.Join(basedir, `local.aww`), lineRange: `1`, @@ -221,8 +221,8 @@ func TestAwwanLocalPut_withEncryption(t *testing.T) { passphrase string expError string - // If true, the Awwan.privateKey will be set to nil before - // running Local. + // If true, the Awwan.cryptoc.privateKey will be set to nil + // before running Local. resetPrivateKey bool } @@ -246,15 +246,15 @@ func TestAwwanLocalPut_withEncryption(t *testing.T) { aww = Awwan{} ) - // Mock terminal to read passphrase for private key. - mockrw.BufRead.WriteString("s3cret\r") - aww.termrw = &mockrw - err = aww.init(baseDir) if err != nil { t.Fatal(err) } + // Mock terminal to read passphrase for private key. + mockrw.BufRead.WriteString("s3cret\r") + aww.cryptoc.termrw = &mockrw + var ( script = filepath.Join(baseDir, `local.aww`) lineRange = `3` @@ -270,7 +270,7 @@ func TestAwwanLocalPut_withEncryption(t *testing.T) { }, { desc: `WithInvalidPassphrase`, passphrase: "invalid\r", - expError: `Local: loadEnvFromPaths: private key is missing or not loaded`, + expError: `Local: loadEnvFromPaths: LoadPrivateKeyInteractive: x509: decryption password incorrect`, resetPrivateKey: true, }} @@ -283,7 +283,7 @@ func TestAwwanLocalPut_withEncryption(t *testing.T) { _ = os.Remove(fileDest) if c.resetPrivateKey { - aww.privateKey = nil + aww.cryptoc.privateKey = nil // Mock terminal to read passphrase for private key. mockrw.BufRead.Reset() diff --git a/crypto_context.go b/crypto_context.go new file mode 100644 index 0000000..777134c --- /dev/null +++ b/crypto_context.go @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2023 M. Shulhan <ms@kilabit.info> +// SPDX-License-Identifier: GPL-3.0-or-later + +package awwan + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "errors" + "fmt" + "hash" + "io" + "io/fs" + "os" + "path/filepath" + + libcrypto "github.com/shuLhan/share/lib/crypto" +) + +// errPrivateKeyMissing returned when private key file is missing or not +// loaded when command require loading encrypted file. +var errPrivateKeyMissing = errors.New(`private key is missing or not loaded`) + +// cryptoContext hold fields and operation for encryption and decryption. +type cryptoContext struct { + hash hash.Hash + + // privateKey the key for encrypt and decrypt command. + privateKey *rsa.PrivateKey + + // termrw the ReadWriter to prompt and read passphrase for + // privateKey. + // This field should be nil, only used during testing. + termrw io.ReadWriter + + baseDir string + + label []byte +} + +func newCryptoContext(baseDir string) (cryptoc *cryptoContext) { + cryptoc = &cryptoContext{ + hash: sha256.New(), + baseDir: baseDir, + label: []byte(`awwan`), + } + return cryptoc +} + +func (cryptoc *cryptoContext) decrypt(cipher []byte) (plain []byte, err error) { + if cryptoc.privateKey == nil { + err = cryptoc.loadPrivateKey() + if err != nil { + return nil, err + } + if cryptoc.privateKey == nil { + return nil, errPrivateKeyMissing + } + } + + plain, err = libcrypto.DecryptOaep(cryptoc.hash, rand.Reader, + cryptoc.privateKey, cipher, cryptoc.label) + if err != nil { + return nil, err + } + + return plain, nil +} + +func (cryptoc *cryptoContext) encrypt(plain []byte) (cipher []byte, err error) { + if cryptoc.privateKey == nil { + err = cryptoc.loadPrivateKey() + if err != nil { + return nil, err + } + if cryptoc.privateKey == nil { + return nil, errPrivateKeyMissing + } + } + + cipher, err = libcrypto.EncryptOaep(cryptoc.hash, rand.Reader, + &cryptoc.privateKey.PublicKey, plain, cryptoc.label) + if err != nil { + return nil, err + } + + return cipher, nil +} + +// loadPrivateKey from file "{{baseDir}}/.awwan.key" if its exist. +func (cryptoc *cryptoContext) loadPrivateKey() (err error) { + var ( + fileKey = filepath.Join(cryptoc.baseDir, defFilePrivateKey) + + pkey crypto.PrivateKey + ok bool + ) + + _, err = os.Stat(fileKey) + if err != nil { + if errors.Is(err, fs.ErrNotExist) { + return nil + } + return err + } + + fmt.Printf("--- Loading private key file %q (enter to skip passphrase) ...\n", fileKey) + + pkey, err = libcrypto.LoadPrivateKeyInteractive(cryptoc.termrw, fileKey) + if err != nil { + if errors.Is(err, libcrypto.ErrEmptyPassphrase) { + // Ignore empty passphrase error, in case the + // command does not need to decrypt files when + // running. + return nil + } + return err + } + + cryptoc.privateKey, ok = pkey.(*rsa.PrivateKey) + if !ok { + return fmt.Errorf(`the private key type must be RSA, got %T`, pkey) + } + + return nil +} @@ -5,7 +5,6 @@ package awwan import ( "bytes" - "crypto/rsa" "errors" "fmt" "io/fs" @@ -27,9 +26,9 @@ import ( // Session manage and cache SSH client and list of scripts. // One session have one SSH client, but may contains more than one script. type Session struct { - privateKey *rsa.PrivateKey - sftpc *sftp.Client - sshClient *ssh.Client + cryptoc *cryptoContext + sftpc *sftp.Client + sshClient *ssh.Client vars ini.Ini @@ -56,7 +55,7 @@ func NewSession(aww *Awwan, sessionDir string) (ses *Session, err error) { ) ses = &Session{ - privateKey: aww.privateKey, + cryptoc: aww.cryptoc, BaseDir: aww.BaseDir, ScriptDir: sessionDir, @@ -667,7 +666,7 @@ func (ses *Session) loadFileEnv(awwanEnv string, isVault bool) (err error) { fmt.Printf("--- loading %q ...\n", awwanEnv) if isVault { - content, err = decrypt(ses.privateKey, content) + content, err = ses.cryptoc.decrypt(content) if err != nil { return err } @@ -703,7 +702,7 @@ func (ses *Session) loadFileInput(path string) (content []byte, isVault bool, er return nil, false, err } - content, err = decrypt(ses.privateKey, content) + content, err = ses.cryptoc.decrypt(content) if err != nil { return nil, false, err } |
