Skip to content

Commit

Permalink
Merge pull request #876 from hairyhenderson/rsa-crypto-800
Browse files Browse the repository at this point in the history
New RSA crypto functions, and new base64.DecodeBytes function
  • Loading branch information
Dave Henderson authored and GitHub committed Jun 15, 2020
2 parents bc9fdd0 + ee3069a commit 2fa8db2
Show file tree
Hide file tree
Showing 13 changed files with 826 additions and 13 deletions.
3 changes: 3 additions & 0 deletions conv/conv.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions conv/conv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ func TestToString(t *testing.T) {
{p, "foo"},
{fmt.Errorf("hi"), "hi"},
{n, "<nil>"},
{[]byte("hello world"), "hello world"},
}

for _, d := range testdata {
Expand Down
104 changes: 104 additions & 0 deletions crypto/rsa.go
Original file line number Diff line number Diff line change
@@ -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
}
109 changes: 109 additions & 0 deletions crypto/rsa_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
27 changes: 24 additions & 3 deletions docs-src/content/functions/base64.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 &sect;4](https://tools.ietf.org/html/rfc4648#section-4) (and _not_ the URL-safe encoding).
pipeline: false
pipeline: true
arguments:
- name: input
required: true
Expand All @@ -20,8 +20,10 @@ funcs:
description: |
Decode a Base64 string. This supports both standard ([RFC4648 &sect;4](https://tools.ietf.org/html/rfc4648#section-4)) and URL-safe ([RFC4648 &sect;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
Expand All @@ -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 &sect;4](https://tools.ietf.org/html/rfc4648#section-4)) and URL-safe ([RFC4648 &sect;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
Loading

0 comments on commit 2fa8db2

Please sign in to comment.