summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDave Henderson <dhenderson@gmail.com>2022-02-12 16:57:23 -0500
committerGitHub <noreply@github.com>2022-02-12 16:57:23 -0500
commitd31b7c6d47dfdbcf0fbd3a323d41aa1935b558e4 (patch)
tree022c388f2ab70f004bcf318a01aacbe0b5969c08
parent37a172433ae63e659675033b193b9eace7bf0666 (diff)
New crypto.EncryptAES/DecryptAES functions (#1305)
Signed-off-by: Dave Henderson <dhenderson@gmail.com>
-rw-r--r--crypto/aes.go88
-rw-r--r--crypto/aes_test.go67
-rw-r--r--docs-src/content/functions/crypto.yml98
-rw-r--r--docs/content/functions/crypto.md138
-rw-r--r--funcs/crypto.go47
-rw-r--r--funcs/crypto_test.go34
6 files changed, 472 insertions, 0 deletions
diff --git a/crypto/aes.go b/crypto/aes.go
new file mode 100644
index 00000000..b3fab8dc
--- /dev/null
+++ b/crypto/aes.go
@@ -0,0 +1,88 @@
+package crypto
+
+import (
+ "bytes"
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/rand"
+ "io"
+)
+
+// EncryptAESCBC - use a 128, 192, or 256 bit key to encrypt the given content
+// using AES-CBC. The output will not be encoded. Usually the output would be
+// base64-encoded for display. Empty content will not be encrypted.
+//
+// This function is compatible with Helm's decryptAES function, when the output
+// is base64-encoded, and when the key is 256 bits long.
+func EncryptAESCBC(key []byte, in []byte) ([]byte, error) {
+ if len(in) == 0 {
+ return in, nil
+ }
+
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, err
+ }
+
+ // Pad the content to be a multiple of the block size, with the pad length
+ // as the content. Note that the padding will be a full block when the
+ // content is already a multiple of the block size.
+ // This algorithm is described in the TLS spec:
+ // https://datatracker.ietf.org/doc/html/rfc5246#section-6.2.3.2
+ bs := block.BlockSize()
+ pl := bs - len(in)%bs
+
+ // pad with pl, repeated pl times
+ in = append(in, bytes.Repeat([]byte{byte(pl)}, pl)...)
+
+ out := make([]byte, bs+len(in))
+
+ // Generate a random IV. Must be the same length as the block size, and is
+ // stored at the beginning of the output slice unencrypted, so that it can
+ // be used for decryption.
+ iv := out[:bs]
+ if _, err := io.ReadFull(rand.Reader, iv); err != nil {
+ return nil, err
+ }
+
+ // encrypt the content into the rest of the output slice
+ mode := cipher.NewCBCEncrypter(block, iv)
+ mode.CryptBlocks(out[bs:], in)
+
+ return out, nil
+}
+
+// DecryptAESCBC - use a 128, 192, or 256 bit key to decrypt the given content
+// using AES-CBC. The output will not be encoded. Empty content will not be
+// decrypted.
+//
+// This function is compatible with Helm's encryptAES function, when the input
+// is base64-decoded, and when the key is 256 bits long.
+func DecryptAESCBC(key []byte, in []byte) ([]byte, error) {
+ if len(in) == 0 {
+ return nil, nil
+ }
+
+ block, err := aes.NewCipher(key)
+ if err != nil {
+ return nil, err
+ }
+
+ // the first block is the IV, unencrypted
+ iv := in[:aes.BlockSize]
+
+ // the rest of the content is encrypted
+ in = in[aes.BlockSize:]
+
+ out := make([]byte, len(in))
+
+ mode := cipher.NewCBCDecrypter(block, iv)
+ mode.CryptBlocks(out, in)
+
+ // content must always be padded with at least one byte, and the padding
+ // byte must be the padding length
+ pl := int(out[len(out)-1])
+ out = out[:len(out)-pl]
+
+ return out, nil
+}
diff --git a/crypto/aes_test.go b/crypto/aes_test.go
new file mode 100644
index 00000000..df00b14b
--- /dev/null
+++ b/crypto/aes_test.go
@@ -0,0 +1,67 @@
+package crypto
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestEncryptDecryptAESCBC(t *testing.T) {
+ // empty key is invalid
+ _, err := EncryptAESCBC([]byte{}, []byte("foo"))
+ assert.Error(t, err)
+
+ // wrong-length keys are invalid
+ _, err = EncryptAESCBC(bytes.Repeat([]byte{'a'}, 1), []byte("foo"))
+ assert.Error(t, err)
+
+ _, err = EncryptAESCBC(bytes.Repeat([]byte{'a'}, 15), []byte("foo"))
+ assert.Error(t, err)
+
+ key := make([]byte, 32)
+ copy(key, []byte("password"))
+
+ // empty content is a pass-through
+ out, err := EncryptAESCBC(key, []byte{})
+ assert.NoError(t, err)
+ assert.Equal(t, []byte{}, out)
+
+ testdata := [][]byte{
+ bytes.Repeat([]byte{'a'}, 1),
+ bytes.Repeat([]byte{'a'}, 15),
+ bytes.Repeat([]byte{'a'}, 16),
+ bytes.Repeat([]byte{'a'}, 31),
+ bytes.Repeat([]byte{'a'}, 32),
+ }
+
+ for _, d := range testdata {
+ out, err = EncryptAESCBC(key, d)
+ assert.NoError(t, err)
+ assert.NotEqual(t, d, out)
+
+ out, err = DecryptAESCBC(key, out)
+ assert.NoError(t, err)
+ assert.Equal(t, d, out)
+ }
+
+ // 128-bit key
+ key = bytes.Repeat([]byte{'a'}, 16)
+ out, err = EncryptAESCBC(key, []byte("foo"))
+ assert.NoError(t, err)
+ assert.NotEqual(t, []byte("foo"), out)
+
+ out, err = DecryptAESCBC(key, out)
+ assert.NoError(t, err)
+ assert.Equal(t, []byte("foo"), out)
+
+ // 192-bit key
+ key = bytes.Repeat([]byte{'a'}, 24)
+ out, err = EncryptAESCBC(key, []byte("foo"))
+ assert.NoError(t, err)
+ assert.NotEqual(t, []byte("foo"), out)
+
+ out, err = DecryptAESCBC(key, out)
+ assert.NoError(t, err)
+ assert.Equal(t, []byte("foo"), out)
+}
diff --git a/docs-src/content/functions/crypto.yml b/docs-src/content/functions/crypto.yml
index de64a8d8..d1778c67 100644
--- a/docs-src/content/functions/crypto.yml
+++ b/docs-src/content/functions/crypto.yml
@@ -27,6 +27,104 @@ funcs:
- |
$ gomplate -i '{{ crypto.Bcrypt 4 "foo" }}
$2a$04$zjba3N38sjyYsw0Y7IRCme1H4gD0MJxH8Ixai0/sgsrf7s1MFUK1C
+ - name: crypto.DecryptAES
+ description: |
+ Decrypts the given input using the given key. By default,
+ uses AES-256-CBC, but supports 128- and 192-bit keys as well.
+
+ _Note: This function is compatible with Helm's encryptAES function, when
+ the input is base64-decoded, and when using 256-bit keys._
+ pipeline: true
+ arguments:
+ - name: key
+ required: true
+ description: the key to use for decryption
+ - name: keyBits
+ required: false
+ description: the key length to use - defaults to `256`
+ - name: input
+ required: true
+ description: the input to decrypt
+ examples:
+ - |
+ $ gomplate -i '{{ base64.Decode "Gp2WG/fKOUsVlhcpr3oqgR+fRUNBcO1eZJ9CW+gDI18=" | crypto.DecryptAES "swordfish" 128 | conv.ToString }}'
+ hello world
+ - name: crypto.DecryptAES
+ description: |
+ Decrypts the given input using the given key. By default,
+ uses AES-256-CBC, but supports 128- and 192-bit keys as well.
+
+ This function prints the output as a string. Note that this may result in
+ unreadable text if the decrypted payload is binary. See
+ [`crypto.DecryptAESBytes`](#crypto.DecryptAESBytes) for another method.
+
+ This function is suitable for decrypting data that was encrypted by
+ Helm's `encryptAES` function, when the input is base64-decoded, and when
+ using 256-bit keys.
+ pipeline: true
+ arguments:
+ - name: key
+ required: true
+ description: the key to use for decryption
+ - name: keyBits
+ required: false
+ description: the key length to use - defaults to `256`
+ - name: input
+ required: true
+ description: the input to decrypt
+ examples:
+ - |
+ $ gomplate -i '{{ base64.Decode "Gp2WG/fKOUsVlhcpr3oqgR+fRUNBcO1eZJ9CW+gDI18=" | crypto.DecryptAES "swordfish" 128 }}'
+ hello world
+ - name: crypto.DecryptAESBytes
+ description: |
+ Decrypts the given input using the given key. By default,
+ uses AES-256-CBC, but supports 128- and 192-bit keys as well.
+
+ This function outputs the raw byte array, which may be sent as input to
+ other functions.
+
+ This function is suitable for decrypting data that was encrypted by
+ Helm's `encryptAES` function, when the input is base64-decoded, and when
+ using 256-bit keys.
+ pipeline: true
+ arguments:
+ - name: key
+ required: true
+ description: the key to use for decryption
+ - name: keyBits
+ required: false
+ description: the key length to use - defaults to `256`
+ - name: input
+ required: true
+ description: the input to decrypt
+ examples:
+ - |
+ $ gomplate -i '{{ base64.Decode "Gp2WG/fKOUsVlhcpr3oqgR+fRUNBcO1eZJ9CW+gDI18=" | crypto.DecryptAES "swordfish" 128 }}'
+ hello world
+ - name: crypto.EncryptAES
+ description: |
+ Encrypts the given input using the given key. By default,
+ uses AES-256-CBC, but supports 128- and 192-bit keys as well.
+
+ This function is suitable for encrypting data that will be decrypted by
+ Helm's `decryptAES` function, when the output is base64-encoded, and when
+ using 256-bit keys.
+ pipeline: true
+ arguments:
+ - name: key
+ required: true
+ description: the key to use for encryption
+ - name: keyBits
+ required: false
+ description: the key length to use - defaults to `256`
+ - name: input
+ required: true
+ description: the input to encrypt
+ examples:
+ - |
+ $ gomplate -i '{{ "hello world" | crypto.EncryptAES "swordfish" 128 | base64.Encode }}'
+ MnRutHovsh/9JN3YrJtBVjZtI6xXZh33bCQS2iZ4SDI=
- name: crypto.PBKDF2
description: |
Run the Password-Based Key Derivation Function &num;2 as defined in
diff --git a/docs/content/functions/crypto.md b/docs/content/functions/crypto.md
index 2e587038..d68b3125 100644
--- a/docs/content/functions/crypto.md
+++ b/docs/content/functions/crypto.md
@@ -45,6 +45,144 @@ $ gomplate -i '{{ crypto.Bcrypt 4 "foo" }}
$2a$04$zjba3N38sjyYsw0Y7IRCme1H4gD0MJxH8Ixai0/sgsrf7s1MFUK1C
```
+## `crypto.DecryptAES`
+
+Decrypts the given input using the given key. By default,
+uses AES-256-CBC, but supports 128- and 192-bit keys as well.
+
+_Note: This function is compatible with Helm's encryptAES function, when
+the input is base64-decoded, and when using 256-bit keys._
+
+### Usage
+
+```go
+crypto.DecryptAES key [keyBits] input
+```
+```go
+input | crypto.DecryptAES key [keyBits]
+```
+
+### Arguments
+
+| name | description |
+|------|-------------|
+| `key` | _(required)_ the key to use for decryption |
+| `keyBits` | _(optional)_ the key length to use - defaults to `256` |
+| `input` | _(required)_ the input to decrypt |
+
+### Examples
+
+```console
+$ gomplate -i '{{ base64.Decode "Gp2WG/fKOUsVlhcpr3oqgR+fRUNBcO1eZJ9CW+gDI18=" | crypto.DecryptAES "swordfish" 128 | conv.ToString }}'
+hello world
+```
+
+## `crypto.DecryptAES`
+
+Decrypts the given input using the given key. By default,
+uses AES-256-CBC, but supports 128- and 192-bit keys as well.
+
+This function prints the output as a string. Note that this may result in
+unreadable text if the decrypted payload is binary. See
+[`crypto.DecryptAESBytes`](#crypto.DecryptAESBytes) for another method.
+
+This function is suitable for decrypting data that was encrypted by
+Helm's `encryptAES` function, when the input is base64-decoded, and when
+using 256-bit keys.
+
+### Usage
+
+```go
+crypto.DecryptAES key [keyBits] input
+```
+```go
+input | crypto.DecryptAES key [keyBits]
+```
+
+### Arguments
+
+| name | description |
+|------|-------------|
+| `key` | _(required)_ the key to use for decryption |
+| `keyBits` | _(optional)_ the key length to use - defaults to `256` |
+| `input` | _(required)_ the input to decrypt |
+
+### Examples
+
+```console
+$ gomplate -i '{{ base64.Decode "Gp2WG/fKOUsVlhcpr3oqgR+fRUNBcO1eZJ9CW+gDI18=" | crypto.DecryptAES "swordfish" 128 }}'
+hello world
+```
+
+## `crypto.DecryptAESBytes`
+
+Decrypts the given input using the given key. By default,
+uses AES-256-CBC, but supports 128- and 192-bit keys as well.
+
+This function outputs the raw byte array, which may be sent as input to
+other functions.
+
+This function is suitable for decrypting data that was encrypted by
+Helm's `encryptAES` function, when the input is base64-decoded, and when
+using 256-bit keys.
+
+### Usage
+
+```go
+crypto.DecryptAESBytes key [keyBits] input
+```
+```go
+input | crypto.DecryptAESBytes key [keyBits]
+```
+
+### Arguments
+
+| name | description |
+|------|-------------|
+| `key` | _(required)_ the key to use for decryption |
+| `keyBits` | _(optional)_ the key length to use - defaults to `256` |
+| `input` | _(required)_ the input to decrypt |
+
+### Examples
+
+```console
+$ gomplate -i '{{ base64.Decode "Gp2WG/fKOUsVlhcpr3oqgR+fRUNBcO1eZJ9CW+gDI18=" | crypto.DecryptAES "swordfish" 128 }}'
+hello world
+```
+
+## `crypto.EncryptAES`
+
+Encrypts the given input using the given key. By default,
+uses AES-256-CBC, but supports 128- and 192-bit keys as well.
+
+This function is suitable for encrypting data that will be decrypted by
+Helm's `decryptAES` function, when the output is base64-encoded, and when
+using 256-bit keys.
+
+### Usage
+
+```go
+crypto.EncryptAES key [keyBits] input
+```
+```go
+input | crypto.EncryptAES key [keyBits]
+```
+
+### Arguments
+
+| name | description |
+|------|-------------|
+| `key` | _(required)_ the key to use for encryption |
+| `keyBits` | _(optional)_ the key length to use - defaults to `256` |
+| `input` | _(required)_ the input to encrypt |
+
+### Examples
+
+```console
+$ gomplate -i '{{ "hello world" | crypto.EncryptAES "swordfish" 128 | base64.Encode }}'
+MnRutHovsh/9JN3YrJtBVjZtI6xXZh33bCQS2iZ4SDI=
+```
+
## `crypto.PBKDF2`
Run the Password-Based Key Derivation Function &num;2 as defined in
diff --git a/funcs/crypto.go b/funcs/crypto.go
index 99d2cce5..8fc28f69 100644
--- a/funcs/crypto.go
+++ b/funcs/crypto.go
@@ -197,3 +197,50 @@ func (f *CryptoFuncs) RSADerivePublicKey(privateKey string) (string, error) {
out, err := crypto.RSADerivePublicKey([]byte(privateKey))
return string(out), err
}
+
+// EncryptAES -
+func (f *CryptoFuncs) EncryptAES(key string, args ...interface{}) ([]byte, error) {
+ k, msg, err := parseAESArgs(key, args...)
+ if err != nil {
+ return nil, err
+ }
+
+ return crypto.EncryptAESCBC(k, msg)
+}
+
+// DecryptAES -
+func (f *CryptoFuncs) DecryptAES(key string, args ...interface{}) (string, error) {
+ out, err := f.DecryptAESBytes(key, args...)
+ return conv.ToString(out), err
+}
+
+// DecryptAESBytes -
+func (f *CryptoFuncs) DecryptAESBytes(key string, args ...interface{}) ([]byte, error) {
+ k, msg, err := parseAESArgs(key, args...)
+ if err != nil {
+ return nil, err
+ }
+
+ return crypto.DecryptAESCBC(k, msg)
+}
+
+func parseAESArgs(key string, args ...interface{}) ([]byte, []byte, error) {
+ keyBits := 256 // default to AES-256-CBC
+
+ var msg []byte
+
+ switch len(args) {
+ case 1:
+ msg = toBytes(args[0])
+ case 2:
+ keyBits = conv.ToInt(args[0])
+ msg = toBytes(args[1])
+ default:
+ return nil, nil, fmt.Errorf("wrong number of args: want 2 or 3, got %d", len(args))
+ }
+
+ k := make([]byte, keyBits/8)
+ copy(k, []byte(key))
+
+ return k, msg, nil
+}
diff --git a/funcs/crypto_test.go b/funcs/crypto_test.go
index 4cb968c0..5faa3c6a 100644
--- a/funcs/crypto_test.go
+++ b/funcs/crypto_test.go
@@ -138,3 +138,37 @@ func TestRSACrypt(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, dec, string(b))
}
+
+func TestAESCrypt(t *testing.T) {
+ c := testCryptoNS()
+ key := "0123456789012345"
+ in := "hello world"
+
+ _, err := c.EncryptAES(key, 1, 2, 3, 4)
+ assert.Error(t, err)
+
+ _, err = c.DecryptAES(key, 1, 2, 3, 4)
+ assert.Error(t, err)
+
+ enc, err := c.EncryptAES(key, in)
+ assert.NoError(t, err)
+
+ dec, err := c.DecryptAES(key, enc)
+ assert.NoError(t, err)
+ assert.Equal(t, in, dec)
+
+ b, err := c.DecryptAESBytes(key, enc)
+ assert.NoError(t, err)
+ assert.Equal(t, dec, string(b))
+
+ enc, err = c.EncryptAES(key, 128, in)
+ assert.NoError(t, err)
+
+ dec, err = c.DecryptAES(key, 128, enc)
+ assert.NoError(t, err)
+ assert.Equal(t, in, dec)
+
+ b, err = c.DecryptAESBytes(key, 128, enc)
+ assert.NoError(t, err)
+ assert.Equal(t, dec, string(b))
+}