From ee3069abfc98d3ba0f99e92f1145195cd879f2e7 Mon Sep 17 00:00:00 2001 From: Dave Henderson Date: Sun, 14 Jun 2020 15:02:00 -0400 Subject: New RSA encrypt/decrypt functions, and new base64.DecodeBytes function Signed-off-by: Dave Henderson --- conv/conv.go | 3 + conv/conv_test.go | 1 + crypto/rsa.go | 104 ++++++++++++++ crypto/rsa_test.go | 109 ++++++++++++++ docs-src/content/functions/base64.yml | 27 +++- docs-src/content/functions/crypto.yml | 163 ++++++++++++++++++++- docs/content/functions/base64.md | 41 +++++- docs/content/functions/crypto.md | 232 +++++++++++++++++++++++++++++- funcs/base64.go | 6 + funcs/base64_test.go | 10 +- funcs/crypto.go | 36 +++++ funcs/crypto_test.go | 36 +++++ internal/tests/integration/crypto_test.go | 71 +++++++++ 13 files changed, 826 insertions(+), 13 deletions(-) create mode 100644 crypto/rsa.go create mode 100644 crypto/rsa_test.go create mode 100644 internal/tests/integration/crypto_test.go diff --git a/conv/conv.go b/conv/conv.go index eb8695ac..52a0fb57 100644 --- a/conv/conv.go +++ b/conv/conv.go @@ -132,6 +132,9 @@ func ToString(in interface{}) string { if s, ok := in.(fmt.Stringer); ok { return s.String() } + if s, ok := in.([]byte); ok { + return string(s) + } v, ok := printableValue(reflect.ValueOf(in)) if ok { diff --git a/conv/conv_test.go b/conv/conv_test.go index 72db4f4e..87a3d730 100644 --- a/conv/conv_test.go +++ b/conv/conv_test.go @@ -245,6 +245,7 @@ func TestToString(t *testing.T) { {p, "foo"}, {fmt.Errorf("hi"), "hi"}, {n, ""}, + {[]byte("hello world"), "hello world"}, } for _, d := range testdata { diff --git a/crypto/rsa.go b/crypto/rsa.go new file mode 100644 index 00000000..1cb7d76e --- /dev/null +++ b/crypto/rsa.go @@ -0,0 +1,104 @@ +package crypto + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "fmt" + "strings" +) + +// RSAEncrypt - use the given public key to encrypt the given plaintext. The key +// should be a PEM-encoded RSA public key in PKIX, ASN.1 DER form, typically +// beginning with "PUBLIC KEY". PKCS#1 format is also supported as a fallback. +// The output will not be encoded, so consider base64-encoding it for display. +func RSAEncrypt(key string, in []byte) ([]byte, error) { + block, _ := pem.Decode([]byte(key)) + if block == nil { + return nil, fmt.Errorf("failed to read key %q: no key found", key) + } + + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + if strings.Contains(err.Error(), "use ParsePKCS1PublicKey instead") { + pub, err = x509.ParsePKCS1PublicKey(block.Bytes) + } + if err != nil { + return nil, fmt.Errorf("failed to parse public key: %w", err) + } + } + pubKey, ok := pub.(*rsa.PublicKey) + if !ok { + return nil, fmt.Errorf("public key in wrong format, was %T", pub) + } + + out, err := rsa.EncryptPKCS1v15(rand.Reader, pubKey, in) + return out, err +} + +// RSADecrypt - decrypt the ciphertext with the given private key. The key +// must be a PEM-encoded RSA private key in PKCS#1, ASN.1 DER form, typically +// beginning with "RSA PRIVATE KEY". The input text must be plain ciphertext, +// not base64-encoded. +func RSADecrypt(key string, in []byte) ([]byte, error) { + block, _ := pem.Decode([]byte(key)) + if block == nil { + return nil, fmt.Errorf("failed to read key %q: no key found", key) + } + + priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("invalid private key: %w", err) + } + + out, err := priv.Decrypt(nil, in, nil) + if err != nil { + return nil, fmt.Errorf("failed to decrypt: %w", err) + } + return out, nil +} + +// RSAGenerateKey - +func RSAGenerateKey(bits int) ([]byte, error) { + priv, err := rsa.GenerateKey(rand.Reader, bits) + if err != nil { + return nil, fmt.Errorf("failed to generate RSA private key: %w", err) + } + block := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(priv), + } + buf := &bytes.Buffer{} + err = pem.Encode(buf, block) + if err != nil { + return nil, fmt.Errorf("failed to encode generated RSA private key: pem encoding failed: %w", err) + } + return buf.Bytes(), nil +} + +// RSADerivePublicKey - +func RSADerivePublicKey(privateKey []byte) ([]byte, error) { + block, _ := pem.Decode(privateKey) + if block == nil { + return nil, fmt.Errorf("failed to read key: no key found") + } + + priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, fmt.Errorf("invalid private key: %w", err) + } + + b, err := x509.MarshalPKIXPublicKey(&priv.PublicKey) + if err != nil { + return nil, fmt.Errorf("failed to marshal PKIX public key: %w", err) + } + + block = &pem.Block{ + Type: "PUBLIC KEY", + Bytes: b, + } + + return pem.EncodeToMemory(block), nil +} diff --git a/crypto/rsa_test.go b/crypto/rsa_test.go new file mode 100644 index 00000000..aaf50408 --- /dev/null +++ b/crypto/rsa_test.go @@ -0,0 +1,109 @@ +package crypto + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func genPKCS1PrivKey() (*rsa.PrivateKey, string) { + rsaPriv, _ := rsa.GenerateKey(rand.Reader, 4096) + privBlock := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(rsaPriv), + } + return rsaPriv, string(pem.EncodeToMemory(privBlock)) +} + +func derivePKIXPrivKey(priv *rsa.PrivateKey) string { + privBlock := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(priv), + } + return string(pem.EncodeToMemory(privBlock)) +} + +func derivePKIXPubKey(priv *rsa.PrivateKey) string { + b, _ := x509.MarshalPKIXPublicKey(&priv.PublicKey) + pubBlock := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: b, + } + testPubKey := string(pem.EncodeToMemory(pubBlock)) + return testPubKey +} + +func derivePKCS1PubKey(priv *rsa.PrivateKey) string { + b := x509.MarshalPKCS1PublicKey(&priv.PublicKey) + pubBlock := &pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: b, + } + testPubKey := string(pem.EncodeToMemory(pubBlock)) + return testPubKey +} + +func TestRSACrypt(t *testing.T) { + priv, testPrivKey := genPKCS1PrivKey() + testPubKey := derivePKIXPubKey(priv) + + in := []byte("hello world") + key := "bad key" + _, err := RSAEncrypt(key, in) + assert.Error(t, err) + + _, err = RSADecrypt(key, in) + assert.Error(t, err) + + key = "" + _, err = RSAEncrypt(key, in) + assert.Error(t, err) + _, err = RSADecrypt(key, in) + assert.Error(t, err) + + enc, err := RSAEncrypt(testPubKey, in) + assert.NoError(t, err) + dec, err := RSADecrypt(testPrivKey, enc) + assert.NoError(t, err) + assert.Equal(t, in, dec) + + testPubKey = derivePKCS1PubKey(priv) + enc, err = RSAEncrypt(testPubKey, in) + assert.NoError(t, err) + dec, err = RSADecrypt(testPrivKey, enc) + assert.NoError(t, err) + assert.Equal(t, in, dec) +} + +func TestRSAGenerateKey(t *testing.T) { + _, err := RSAGenerateKey(0) + assert.Error(t, err) + + key, err := RSAGenerateKey(12) + assert.NoError(t, err) + assert.True(t, strings.HasPrefix(string(key), + "-----BEGIN RSA PRIVATE KEY-----")) + assert.True(t, strings.HasSuffix(string(key), + "-----END RSA PRIVATE KEY-----\n")) +} + +func TestRSADerivePublicKey(t *testing.T) { + _, err := RSADerivePublicKey(nil) + assert.Error(t, err) + + _, err = RSADerivePublicKey([]byte(`-----BEGIN FOO----- +-----END FOO-----`)) + assert.Error(t, err) + + priv, privKey := genPKCS1PrivKey() + expected := derivePKIXPubKey(priv) + + actual, err := RSADerivePublicKey([]byte(privKey)) + assert.NoError(t, err) + assert.Equal(t, expected, string(actual)) +} diff --git a/docs-src/content/functions/base64.yml b/docs-src/content/functions/base64.yml index 1f278ddc..fc8cd66f 100644 --- a/docs-src/content/functions/base64.yml +++ b/docs-src/content/functions/base64.yml @@ -4,7 +4,7 @@ funcs: - name: base64.Encode description: | Encode data as a Base64 string. Specifically, this uses the standard Base64 encoding as defined in [RFC4648 §4](https://tools.ietf.org/html/rfc4648#section-4) (and _not_ the URL-safe encoding). - pipeline: false + pipeline: true arguments: - name: input required: true @@ -20,8 +20,10 @@ funcs: description: | Decode a Base64 string. This supports both standard ([RFC4648 §4](https://tools.ietf.org/html/rfc4648#section-4)) and URL-safe ([RFC4648 §5](https://tools.ietf.org/html/rfc4648#section-5)) encodings. - This implementation outputs the data as a string, so it may not be appropriate for decoding binary data. If this functionality is desired, [file an issue](https://github.com/hairyhenderson/gomplate/issues/new). - pipeline: false + This function outputs the data as a string, so it may not be appropriate + for decoding binary data. Use [`base64.DecodeBytes`](#base64.DecodeBytes) + for binary data. + pipeline: true arguments: - name: input required: true @@ -33,3 +35,22 @@ funcs: - | $ gomplate -i '{{ "aGVsbG8gd29ybGQ=" | base64.Decode }}' hello world + - name: base64.DecodeBytes + description: | + Decode a Base64 string. This supports both standard ([RFC4648 §4](https://tools.ietf.org/html/rfc4648#section-4)) and URL-safe ([RFC4648 §5](https://tools.ietf.org/html/rfc4648#section-5)) encodings. + + This function outputs the data as a byte array, so it's most useful for + outputting binary data that will be processed further. + Use [`base64.Decode`](#base64.Decode) to output a plain string. + pipeline: false + arguments: + - name: input + required: true + description: The base64 string to decode + examples: + - | + $ gomplate -i '{{ base64.DecodeBytes "aGVsbG8gd29ybGQ=" }}' + [104 101 108 108 111 32 119 111 114 108 100] + - | + $ gomplate -i '{{ "aGVsbG8gd29ybGQ=" | base64.DecodeBytes | conv.ToString }}' + hello world diff --git a/docs-src/content/functions/crypto.yml b/docs-src/content/functions/crypto.yml index 1f91418d..bf31163e 100644 --- a/docs-src/content/functions/crypto.yml +++ b/docs-src/content/functions/crypto.yml @@ -1,8 +1,13 @@ ns: crypto preamble: | - A set of crypto-related functions to be able to perform hashing and (simple!) encryption operations with `gomplate`. + A set of crypto-related functions to be able to perform hashing and (simple!) + encryption operations with `gomplate`. - _Note: These functions are mostly wrappers of existing functions in the Go standard library. The authors of gomplate are not cryptographic experts, however, and so can not guarantee correctness of implementation. It is recommended to have your resident security experts inspect gomplate's code before using gomplate for critical security infrastructure!_ + _Note: These functions are mostly wrappers of existing functions in the Go + standard library. The authors of gomplate are not cryptographic experts, + however, and so can not guarantee correctness of implementation. It is + recommended to have your resident security experts inspect gomplate's code + before using gomplate for critical security infrastructure!_ funcs: - name: crypto.Bcrypt description: | @@ -49,13 +54,165 @@ funcs: - | $ gomplate -i '{{ crypto.PBKDF2 "foo" "bar" 1024 8 }}' 32c4907c3c80792b + - name: crypto.RSADecrypt + description: | + Decrypt an RSA-encrypted input and print the output as a string. Note that + this may result in unreadable text if the decrypted payload is binary. See + [`crypto.RSADecryptBytes`](#crypto.RSADecryptBytes) for a safer method. + + The private key must be a PEM-encoded RSA private key in PKCS#1, ASN.1 DER + form, which typically begins with `-----BEGIN RSA PRIVATE KEY-----`. + + The input text must be plain ciphertext, as a byte array, or safely + convertible to a byte array. To decrypt base64-encoded input, you must + first decode with the [`base64.DecodeBytes`](../base64/#base64.DecodeBytes) + function. + pipeline: true + arguments: + - name: key + required: true + description: the private key to decrypt the input with + - name: input + required: true + description: the encrypted input + examples: + - | + $ gomplate -c pubKey=./testPubKey -c privKey=./testPrivKey \ + -i '{{ $enc := "hello" | crypto.RSAEncrypt .pubKey -}} + {{ crypto.RSADecrypt .privKey $enc }}' + hello + - | + $ export ENCRYPTED="ScTcX1NZ6p/EeDIf6R7FKLcDFjvP98YgiBhyhPE4jtehajIyTKP1GL8C72qbAWrgdQ6A2cSVjoyo3viqf/PZxpcBDUUMDJuemTaJqUUjMWaDuPG37mQbmRtcvFTuUhw1qSbKyHorDOgTX5d4DvWV4otycGtBT6dXhnmmb5V72J/w3z68vtTJ21m9wREFD7LrYVHdFFtRZiIyMBAF0ngQ+hcujrxilnmgzPkEAg6E7Ccctn28Ie2c4CojrwRbNNxXNlIWCCkC/8Vq8qlDfZ70a+BsTmJDuScE6BZbTyteo9uGYrLn+bTIHNDj90AeLCKUTyWLUJ5Edi9LhlKVBoJUNQ==" + $ gomplate -c ciphertext=env:///ENCRYPTED -c privKey=./testPrivKey \ + -i '{{ base64.DecodeBytes .ciphertext | crypto.RSADecrypt .privKey }}' + hello + - name: crypto.RSADecryptBytes + description: | + Decrypt an RSA-encrypted input and output the decrypted byte array. + + The private key must be a PEM-encoded RSA private key in PKCS#1, ASN.1 DER + form, which typically begins with `-----BEGIN RSA PRIVATE KEY-----`. + + The input text must be plain ciphertext, as a byte array, or safely + convertible to a byte array. To decrypt base64-encoded input, you must + first decode with the [`base64.DecodeBytes`](../base64/#base64.DecodeBytes) + function. + + See [`crypto.RSADecrypt`](#crypto.RSADecrypt) for a function that outputs + a string. + pipeline: true + arguments: + - name: key + required: true + description: the private key to decrypt the input with + - name: input + required: true + description: the encrypted input + examples: + - | + $ gomplate -c pubKey=./testPubKey -c privKey=./testPrivKey \ + -i '{{ $enc := "hello" | crypto.RSAEncrypt .pubKey -}} + {{ crypto.RSADecryptBytes .privKey $enc }}' + [104 101 108 108 111] + - | + $ gomplate -c pubKey=./testPubKey -c privKey=./testPrivKey \ + -i '{{ $enc := "hello" | crypto.RSAEncrypt .pubKey -}} + {{ crypto.RSADecryptBytes .privKey $enc | conv.ToString }}' + hello + - name: crypto.RSAEncrypt + description: | + Encrypt the input with RSA and the padding scheme from PKCS#1 v1.5. + + This function is suitable for encrypting data that will be decrypted by + [Terraform's `rsadecrypt` function](https://www.terraform.io/docs/configuration/functions/rsadecrypt.html). + + The key should be a PEM-encoded RSA public key in PKIX ASN.1 DER form, + which typically begins with `BEGIN PUBLIC KEY`. RSA public keys in PKCS#1 + ASN.1 DER form are also supported (beginning with `RSA PUBLIC KEY`). + + The output will not be encoded, so consider + [base64-encoding](../base64/#base64.Encode) it for display. + + _Note:_ Output encrypted with this function will _not_ be deterministic, + so encrypting the same input twice will not result in the same ciphertext. + + _Warning:_ Using this function may not be safe. See the warning on Go's + [`rsa.EncryptPKCS1v15`](https://golang.org/pkg/crypto/rsa/#EncryptPKCS1v15) + documentation. + pipeline: true + arguments: + - name: key + required: true + description: the public key to encrypt the input with + - name: input + required: true + description: the encrypted input + examples: + - | + $ gomplate -c pubKey=./testPubKey \ + -i '{{ "hello" | crypto.RSAEncrypt .pubKey | base64.Encode }}' + ScTcX1NZ6p/EeDIf6R7FKLcDFjvP98YgiBhyhPE4jtehajIyTKP1GL8C72qbAWrgdQ6A2cSVjoyo3viqf/PZxpcBDUUMDJuemTaJqUUjMWaDuPG37mQbmRtcvFTuUhw1qSbKyHorDOgTX5d4DvWV4otycGtBT6dXhnmmb5V72J/w3z68vtTJ21m9wREFD7LrYVHdFFtRZiIyMBAF0ngQ+hcujrxilnmgzPkEAg6E7Ccctn28Ie2c4CojrwRbNNxXNlIWCCkC/8Vq8qlDfZ70a+BsTmJDuScE6BZbTyteo9uGYrLn+bTIHNDj90AeLCKUTyWLUJ5Edi9LhlKVBoJUNQ== + - | + $ gomplate -c pubKey=./testPubKey \ + -i '{{ $enc := "hello" | crypto.RSAEncrypt .pubKey -}} + Ciphertext in hex: {{ printf "%x" $enc }}' + 71729b87cccabb248b9e0e5173f0b12c01d9d2a0565bad18aef9d332ce984bde06acb8bb69334a01446f7f6430077f269e6fbf2ccacd972fe5856dd4719252ebddf599948d937d96ea41540dad291b868f6c0cf647dffdb5acb22cd33557f9a1ddd0ee6c1ad2bbafc910ba8f817b66ea0569afc06e5c7858fd9dc2638861fe7c97391b2f190e4c682b4aa2c9b0050081efe18b10aa8c2b2b5f5b68a42dcc06c9da35b37fca9b1509fddc940eb99f516a2e0195405bcb3993f0fa31bc038d53d2e7231dff08cc39448105ed2d0ac52d375cb543ca8a399f807cc5d007e2c44c69876d189667eee66361a393c4916826af77479382838cd4e004b8baa05636805a + - name: crypto.RSAGenerateKey + description: | + Generate a new RSA Private Key and output in PEM-encoded PKCS#1 ASN.1 DER + form. + + Default key length is 4096 bits, which should be safe enough for most + uses, but can be overridden with the optional `bits` parameter. + + The output is a string, suitable for use with the other `crypto.RSA*` + functions. + pipeline: true + arguments: + - name: bits + required: false + description: bit size of the generated key. Defaults to `4096` + examples: + - | + $ gomplate -i '{{ crypto.RSAGenerateKey }}' + -----BEGIN RSA PRIVATE KEY----- + ... + - | + $ gomplate -i '{{ $key := crypto.RSAGenerateKey 2048 -}} + {{ $pub := crypto.RSADerivePublicKey $key -}} + {{ $enc := "hello" | crypto.RSAEncrypt $pub -}} + {{ crypto.RSADecrypt $key $enc }}' + hello + - name: crypto.RSADerivePublicKey + description: | + Derive a public key from an RSA private key and output in PKIX ASN.1 DER + form. + + The output is a string, suitable for use with other `crypto.RSA*` + functions. + pipeline: true + arguments: + - name: key + required: true + description: the private key to derive a public key from + examples: + - | + $ gomplate -i '{{ crypto.RSAGenerateKey | crypto.RSADerivePublicKey }}' + -----BEGIN PUBLIC KEY----- + ... + - | + $ gomplate -c privKey=./privKey.pem \ + -i '{{ $pub := crypto.RSADerivePublicKey .privKey -}} + {{ $enc := "hello" | crypto.RSAEncrypt $pub -}} + {{ crypto.RSADecrypt .privKey $enc }}' + hello - rawName: '`crypto.SHA1`, `crypto.SHA224`, `crypto.SHA256`, `crypto.SHA384`, `crypto.SHA512`, `crypto.SHA512_224`, `crypto.SHA512_256`' description: | Compute a checksum with a SHA-1 or SHA-2 algorithm as defined in [RFC 3174](https://tools.ietf.org/html/rfc3174) (SHA-1) and [FIPS 180-4](http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf) (SHA-2). These functions output the binary result as a hexadecimal string. - _Note: SHA-1 is cryptographically broken and should not be used for secure applications._ + _Warning: SHA-1 is cryptographically broken and should not be used for secure applications._ pipeline: false rawUsage: | ``` diff --git a/docs/content/functions/base64.md b/docs/content/functions/base64.md index dfd1c54d..87568ba7 100644 --- a/docs/content/functions/base64.md +++ b/docs/content/functions/base64.md @@ -15,6 +15,9 @@ Encode data as a Base64 string. Specifically, this uses the standard Base64 enco ```go base64.Encode input ``` +```go +input | base64.Encode +``` ### Arguments @@ -37,13 +40,18 @@ aGVsbG8gd29ybGQ= Decode a Base64 string. This supports both standard ([RFC4648 §4](https://tools.ietf.org/html/rfc4648#section-4)) and URL-safe ([RFC4648 §5](https://tools.ietf.org/html/rfc4648#section-5)) encodings. -This implementation outputs the data as a string, so it may not be appropriate for decoding binary data. If this functionality is desired, [file an issue](https://github.com/hairyhenderson/gomplate/issues/new). +This function outputs the data as a string, so it may not be appropriate +for decoding binary data. Use [`base64.DecodeBytes`](#base64.DecodeBytes) +for binary data. ### Usage ```go base64.Decode input ``` +```go +input | base64.Decode +``` ### Arguments @@ -61,3 +69,34 @@ hello world $ gomplate -i '{{ "aGVsbG8gd29ybGQ=" | base64.Decode }}' hello world ``` + +## `base64.DecodeBytes` + +Decode a Base64 string. This supports both standard ([RFC4648 §4](https://tools.ietf.org/html/rfc4648#section-4)) and URL-safe ([RFC4648 §5](https://tools.ietf.org/html/rfc4648#section-5)) encodings. + +This function outputs the data as a byte array, so it's most useful for +outputting binary data that will be processed further. +Use [`base64.Decode`](#base64.Decode) to output a plain string. + +### Usage + +```go +base64.DecodeBytes input +``` + +### Arguments + +| name | description | +|------|-------------| +| `input` | _(required)_ The base64 string to decode | + +### Examples + +```console +$ gomplate -i '{{ base64.DecodeBytes "aGVsbG8gd29ybGQ=" }}' +[104 101 108 108 111 32 119 111 114 108 100] +``` +```console +$ gomplate -i '{{ "aGVsbG8gd29ybGQ=" | base64.DecodeBytes | conv.ToString }}' +hello world +``` diff --git a/docs/content/functions/crypto.md b/docs/content/functions/crypto.md index 8a6b99f8..3af44c08 100644 --- a/docs/content/functions/crypto.md +++ b/docs/content/functions/crypto.md @@ -5,9 +5,14 @@ menu: parent: functions --- -A set of crypto-related functions to be able to perform hashing and (simple!) encryption operations with `gomplate`. +A set of crypto-related functions to be able to perform hashing and (simple!) +encryption operations with `gomplate`. -_Note: These functions are mostly wrappers of existing functions in the Go standard library. The authors of gomplate are not cryptographic experts, however, and so can not guarantee correctness of implementation. It is recommended to have your resident security experts inspect gomplate's code before using gomplate for critical security infrastructure!_ +_Note: These functions are mostly wrappers of existing functions in the Go +standard library. The authors of gomplate are not cryptographic experts, +however, and so can not guarantee correctness of implementation. It is +recommended to have your resident security experts inspect gomplate's code +before using gomplate for critical security infrastructure!_ ## `crypto.Bcrypt` @@ -70,13 +75,234 @@ $ gomplate -i '{{ crypto.PBKDF2 "foo" "bar" 1024 8 }}' 32c4907c3c80792b ``` +## `crypto.RSADecrypt` + +Decrypt an RSA-encrypted input and print the output as a string. Note that +this may result in unreadable text if the decrypted payload is binary. See +[`crypto.RSADecryptBytes`](#crypto.RSADecryptBytes) for a safer method. + +The private key must be a PEM-encoded RSA private key in PKCS#1, ASN.1 DER +form, which typically begins with `-----BEGIN RSA PRIVATE KEY-----`. + +The input text must be plain ciphertext, as a byte array, or safely +convertible to a byte array. To decrypt base64-encoded input, you must +first decode with the [`base64.DecodeBytes`](../base64/#base64.DecodeBytes) +function. + +### Usage + +```go +crypto.RSADecrypt key input +``` +```go +input | crypto.RSADecrypt key +``` + +### Arguments + +| name | description | +|------|-------------| +| `key` | _(required)_ the private key to decrypt the input with | +| `input` | _(required)_ the encrypted input | + +### Examples + +```console +$ gomplate -c pubKey=./testPubKey -c privKey=./testPrivKey \ + -i '{{ $enc := "hello" | crypto.RSAEncrypt .pubKey -}} + {{ crypto.RSADecrypt .privKey $enc }}' +hello +``` +```console +$ export ENCRYPTED="ScTcX1NZ6p/EeDIf6R7FKLcDFjvP98YgiBhyhPE4jtehajIyTKP1GL8C72qbAWrgdQ6A2cSVjoyo3viqf/PZxpcBDUUMDJuemTaJqUUjMWaDuPG37mQbmRtcvFTuUhw1qSbKyHorDOgTX5d4DvWV4otycGtBT6dXhnmmb5V72J/w3z68vtTJ21m9wREFD7LrYVHdFFtRZiIyMBAF0ngQ+hcujrxilnmgzPkEAg6E7Ccctn28Ie2c4CojrwRbNNxXNlIWCCkC/8Vq8qlDfZ70a+BsTmJDuScE6BZbTyteo9uGYrLn+bTIHNDj90AeLCKUTyWLUJ5Edi9LhlKVBoJUNQ==" +$ gomplate -c ciphertext=env:///ENCRYPTED -c privKey=./testPrivKey \ + -i '{{ base64.DecodeBytes .ciphertext | crypto.RSADecrypt .privKey }}' +hello +``` + +## `crypto.RSADecryptBytes` + +Decrypt an RSA-encrypted input and output the decrypted byte array. + +The private key must be a PEM-encoded RSA private key in PKCS#1, ASN.1 DER +form, which typically begins with `-----BEGIN RSA PRIVATE KEY-----`. + +The input text must be plain ciphertext, as a byte array, or safely +convertible to a byte array. To decrypt base64-encoded input, you must +first decode with the [`base64.DecodeBytes`](../base64/#base64.DecodeBytes) +function. + +See [`crypto.RSADecrypt`](#crypto.RSADecrypt) for a function that outputs +a string. + +### Usage + +```go +crypto.RSADecryptBytes key input +``` +```go +input | crypto.RSADecryptBytes key +``` + +### Arguments + +| name | description | +|------|-------------| +| `key` | _(required)_ the private key to decrypt the input with | +| `input` | _(required)_ the encrypted input | + +### Examples + +```console +$ gomplate -c pubKey=./testPubKey -c privKey=./testPrivKey \ + -i '{{ $enc := "hello" | crypto.RSAEncrypt .pubKey -}} + {{ crypto.RSADecryptBytes .privKey $enc }}' +[104 101 108 108 111] +``` +```console +$ gomplate -c pubKey=./testPubKey -c privKey=./testPrivKey \ + -i '{{ $enc := "hello" | crypto.RSAEncrypt .pubKey -}} + {{ crypto.RSADecryptBytes .privKey $enc | conv.ToString }}' +hello +``` + +## `crypto.RSAEncrypt` + +Encrypt the input with RSA and the padding scheme from PKCS#1 v1.5. + +This function is suitable for encrypting data that will be decrypted by +[Terraform's `rsadecrypt` function](https://www.terraform.io/docs/configuration/functions/rsadecrypt.html). + +The key should be a PEM-encoded RSA public key in PKIX ASN.1 DER form, +which typically begins with `BEGIN PUBLIC KEY`. RSA public keys in PKCS#1 +ASN.1 DER form are also supported (beginning with `RSA PUBLIC KEY`). + +The output will not be encoded, so consider +[base64-encoding](../base64/#base64.Encode) it for display. + +_Note:_ Output encrypted with this function will _not_ be deterministic, +so encrypting the same input twice will not result in the same ciphertext. + +_Warning:_ Using this function may not be safe. See the warning on Go's +[`rsa.EncryptPKCS1v15`](https://golang.org/pkg/crypto/rsa/#EncryptPKCS1v15) +documentation. + +### Usage + +```go +crypto.RSAEncrypt key input +``` +```go +input | crypto.RSAEncrypt key +``` + +### Arguments + +| name | description | +|------|-------------| +| `key` | _(required)_ the public key to encrypt the input with | +| `input` | _(required)_ the encrypted input | + +### Examples + +```console +$ gomplate -c pubKey=./testPubKey \ + -i '{{ "hello" | crypto.RSAEncrypt .pubKey | base64.Encode }}' +ScTcX1NZ6p/EeDIf6R7FKLcDFjvP98YgiBhyhPE4jtehajIyTKP1GL8C72qbAWrgdQ6A2cSVjoyo3viqf/PZxpcBDUUMDJuemTaJqUUjMWaDuPG37mQbmRtcvFTuUhw1qSbKyHorDOgTX5d4DvWV4otycGtBT6dXhnmmb5V72J/w3z68vtTJ21m9wREFD7LrYVHdFFtRZiIyMBAF0ngQ+hcujrxilnmgzPkEAg6E7Ccctn28Ie2c4CojrwRbNNxXNlIWCCkC/8Vq8qlDfZ70a+BsTmJDuScE6BZbTyteo9uGYrLn+bTIHNDj90AeLCKUTyWLUJ5Edi9LhlKVBoJUNQ== +``` +```console +$ gomplate -c pubKey=./testPubKey \ + -i '{{ $enc := "hello" | crypto.RSAEncrypt .pubKey -}} + Ciphertext in hex: {{ printf "%x" $enc }}' +71729b87cccabb248b9e0e5173f0b12c01d9d2a0565bad18aef9d332ce984bde06acb8bb69334a01446f7f6430077f269e6fbf2ccacd972fe5856dd4719252ebddf599948d937d96ea41540dad291b868f6c0cf647dffdb5acb22cd33557f9a1ddd0ee6c1ad2bbafc910ba8f817b66ea0569afc06e5c7858fd9dc2638861fe7c97391b2f190e4c682b4aa2c9b0050081efe18b10aa8c2b2b5f5b68a42dcc06c9da35b37fca9b1509fddc940eb99f516a2e0195405bcb3993f0fa31bc038d53d2e7231dff08cc39448105ed2d0ac52d375cb543ca8a399f807cc5d007e2c44c69876d189667eee66361a393c4916826af77479382838cd4e004b8baa05636805a +``` + +## `crypto.RSAGenerateKey` + +Generate a new RSA Private Key and output in PEM-encoded PKCS#1 ASN.1 DER +form. + +Default key length is 4096 bits, which should be safe enough for most +uses, but can be overridden with the optional `bits` parameter. + +The output is a string, suitable for use with the other `crypto.RSA*` +functions. + +### Usage + +```go +crypto.RSAGenerateKey [bits] +``` +```go +bits | crypto.RSAGenerateKey +``` + +### Arguments + +| name | description | +|------|-------------| +| `bits` | _(optional)_ bit size of the generated key. Defaults to `4096` | + +### Examples + +```console +$ gomplate -i '{{ crypto.RSAGenerateKey }}' +-----BEGIN RSA PRIVATE KEY----- +... +``` +```console +$ gomplate -i '{{ $key := crypto.RSAGenerateKey 2048 -}} + {{ $pub := crypto.RSADerivePublicKey $key -}} + {{ $enc := "hello" | crypto.RSAEncrypt $pub -}} + {{ crypto.RSADecrypt $key $enc }}' +hello +``` + +## `crypto.RSADerivePublicKey` + +Derive a public key from an RSA private key and output in PKIX ASN.1 DER +form. + +The output is a string, suitable for use with other `crypto.RSA*` +functions. + +### Usage + +```go +crypto.RSADerivePublicKey key +``` +```go +key | crypto.RSADerivePublicKey +``` + +### Arguments + +| name | description | +|------|-------------| +| `key` | _(required)_ the private key to derive a public key from | + +### Examples + +```console +$ gomplate -i '{{ crypto.RSAGenerateKey | crypto.RSADerivePublicKey }}' +-----BEGIN PUBLIC KEY----- +... +``` +```console +$ gomplate -c privKey=./privKey.pem \ + -i '{{ $pub := crypto.RSADerivePublicKey .privKey -}} + {{ $enc := "hello" | crypto.RSAEncrypt $pub -}} + {{ crypto.RSADecrypt .privKey $enc }}' +hello +``` + ## `crypto.SHA1`, `crypto.SHA224`, `crypto.SHA256`, `crypto.SHA384`, `crypto.SHA512`, `crypto.SHA512_224`, `crypto.SHA512_256` Compute a checksum with a SHA-1 or SHA-2 algorithm as defined in [RFC 3174](https://tools.ietf.org/html/rfc3174) (SHA-1) and [FIPS 180-4](http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf) (SHA-2). These functions output the binary result as a hexadecimal string. -_Note: SHA-1 is cryptographically broken and should not be used for secure applications._ +_Warning: SHA-1 is cryptographically broken and should not be used for secure applications._ ### Usage ``` diff --git a/funcs/base64.go b/funcs/base64.go index ac7cf3ee..da9b315e 100644 --- a/funcs/base64.go +++ b/funcs/base64.go @@ -38,6 +38,12 @@ func (f *Base64Funcs) Decode(in interface{}) (string, error) { return string(out), err } +// DecodeBytes - +func (f *Base64Funcs) DecodeBytes(in interface{}) ([]byte, error) { + out, err := base64.Decode(conv.ToString(in)) + return out, err +} + type byter interface { Bytes() []byte } diff --git a/funcs/base64_test.go b/funcs/base64_test.go index a559e17c..6cfe2b9e 100644 --- a/funcs/base64_test.go +++ b/funcs/base64_test.go @@ -15,7 +15,13 @@ func TestBase64Encode(t *testing.T) { func TestBase64Decode(t *testing.T) { bf := &Base64Funcs{} assert.Equal(t, "foobar", must(bf.Decode("Zm9vYmFy"))) - // assert.Equal(t, "", bf.Decode(nil)) +} + +func TestBase64DecodeBytes(t *testing.T) { + bf := &Base64Funcs{} + out, err := bf.DecodeBytes("Zm9vYmFy") + assert.NoError(t, err) + assert.Equal(t, "foobar", string(out)) } func TestToBytes(t *testing.T) { @@ -24,8 +30,6 @@ func TestToBytes(t *testing.T) { buf := &bytes.Buffer{} buf.WriteString("hi") assert.Equal(t, []byte("hi"), toBytes(buf)) - assert.Equal(t, []byte{}, toBytes(nil)) - assert.Equal(t, []byte("42"), toBytes(42)) } diff --git a/funcs/crypto.go b/funcs/crypto.go index d1bd7c9d..59516ded 100644 --- a/funcs/crypto.go +++ b/funcs/crypto.go @@ -131,3 +131,39 @@ func (f *CryptoFuncs) Bcrypt(args ...interface{}) (string, error) { hash, err := bcrypt.GenerateFromPassword([]byte(input), cost) return string(hash), err } + +// RSAEncrypt - +func (f *CryptoFuncs) RSAEncrypt(key string, in interface{}) ([]byte, error) { + msg := toBytes(in) + return crypto.RSAEncrypt(key, msg) +} + +// RSADecrypt - +func (f *CryptoFuncs) RSADecrypt(key string, in []byte) (string, error) { + out, err := crypto.RSADecrypt(key, in) + return string(out), err +} + +// RSADecryptBytes - +func (f *CryptoFuncs) RSADecryptBytes(key string, in []byte) ([]byte, error) { + out, err := crypto.RSADecrypt(key, in) + return out, err +} + +// RSAGenerateKey - +func (f *CryptoFuncs) RSAGenerateKey(args ...interface{}) (string, error) { + bits := 4096 + if len(args) == 1 { + bits = conv.ToInt(args[0]) + } else if len(args) > 1 { + return "", fmt.Errorf("wrong number of args: want 0 or 1, got %d", len(args)) + } + out, err := crypto.RSAGenerateKey(bits) + return string(out), err +} + +// RSADerivePublicKey - +func (f *CryptoFuncs) RSADerivePublicKey(privateKey string) (string, error) { + out, err := crypto.RSADerivePublicKey([]byte(privateKey)) + return string(out), err +} diff --git a/funcs/crypto_test.go b/funcs/crypto_test.go index c2e14d19..4d2b483f 100644 --- a/funcs/crypto_test.go +++ b/funcs/crypto_test.go @@ -64,3 +64,39 @@ func TestBcrypt(t *testing.T) { _, err = c.Bcrypt() assert.Error(t, err) } + +func TestRSAGenerateKey(t *testing.T) { + c := CryptoNS() + _, err := c.RSAGenerateKey(0) + assert.Error(t, err) + + _, err = c.RSAGenerateKey(0, "foo", true) + assert.Error(t, err) + + key, err := c.RSAGenerateKey(12) + assert.NoError(t, err) + assert.True(t, strings.HasPrefix(key, + "-----BEGIN RSA PRIVATE KEY-----")) + assert.True(t, strings.HasSuffix(key, + "-----END RSA PRIVATE KEY-----\n")) +} + +func TestRSACrypt(t *testing.T) { + c := CryptoNS() + key, err := c.RSAGenerateKey() + assert.NoError(t, err) + pub, err := c.RSADerivePublicKey(key) + assert.NoError(t, err) + + in := "hello world" + enc, err := c.RSAEncrypt(pub, in) + assert.NoError(t, err) + + dec, err := c.RSADecrypt(key, enc) + assert.NoError(t, err) + assert.Equal(t, in, dec) + + b, err := c.RSADecryptBytes(key, enc) + assert.NoError(t, err) + assert.Equal(t, dec, string(b)) +} diff --git a/internal/tests/integration/crypto_test.go b/internal/tests/integration/crypto_test.go new file mode 100644 index 00000000..18f0b6d2 --- /dev/null +++ b/internal/tests/integration/crypto_test.go @@ -0,0 +1,71 @@ +//+build integration + +package integration + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + + "gopkg.in/check.v1" + + "gotest.tools/v3/fs" + "gotest.tools/v3/icmd" +) + +type CryptoSuite struct { + tmpDir *fs.Dir +} + +var _ = check.Suite(&CryptoSuite{}) + +func genTestKeys() (string, string) { + rsaPriv, _ := rsa.GenerateKey(rand.Reader, 4096) + rsaPub := rsaPriv.PublicKey + privBlock := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(rsaPriv), + } + testPrivKey := string(pem.EncodeToMemory(privBlock)) + + b, _ := x509.MarshalPKIXPublicKey(&rsaPub) + pubBlock := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: b, + } + testPubKey := string(pem.EncodeToMemory(pubBlock)) + return testPrivKey, testPubKey +} + +func (s *CryptoSuite) SetUpTest(c *check.C) { + testPrivKey, testPubKey := genTestKeys() + s.tmpDir = fs.NewDir(c, "gomplate-inttests", + fs.WithFile("testPrivKey", testPrivKey), + fs.WithFile("testPubKey", testPubKey), + ) +} + +func (s *CryptoSuite) TearDownTest(c *check.C) { + s.tmpDir.Remove() +} + +func (s *CryptoSuite) TestRSACrypt(c *check.C) { + result := icmd.RunCmd(icmd.Command(GomplateBin, + "-i", `{{ crypto.RSAGenerateKey 2048 -}}`, + "-o", `key.pem`), func(cmd *icmd.Cmd) { + cmd.Dir = s.tmpDir.Path() + }) + result.Assert(c, icmd.Expected{ExitCode: 0}) + + result = icmd.RunCmd(icmd.Command(GomplateBin, + "-c", "privKey=./key.pem", + "-i", `{{ $pub := crypto.RSADerivePublicKey .privKey -}} +{{ $enc := "hello" | crypto.RSAEncrypt $pub -}} +{{ crypto.RSADecryptBytes .privKey $enc | conv.ToString }} +{{ crypto.RSADecrypt .privKey $enc }} +`), func(cmd *icmd.Cmd) { + cmd.Dir = s.tmpDir.Path() + }) + result.Assert(c, icmd.Expected{ExitCode: 0, Out: "hello\nhello\n"}) +} -- cgit v1.2.3