aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2023-09-24 02:13:15 +0700
committerShulhan <ms@kilabit.info>2023-09-26 00:24:08 +0700
commit9ae9a42e37b35e17120045da8bb72b07f6de2a44 (patch)
tree32613a49f0d0f1e2d68ab6475745f791dba9cb37
parent8cc52027d243946c03c6b0d1016ca7cc3d7de09a (diff)
downloadawwan-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.go98
-rw-r--r--awwan_test.go48
-rw-r--r--crypto_context.go128
-rw-r--r--session.go13
4 files changed, 164 insertions, 123 deletions
diff --git a/awwan.go b/awwan.go
index c1b0dbd..5680fb1 100644
--- a/awwan.go
+++ b/awwan.go
@@ -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
+}
diff --git a/session.go b/session.go
index 7470624..cff495a 100644
--- a/session.go
+++ b/session.go
@@ -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
}