From 9aa9b1865817d05e374d6e0ffb9be3b37e80415e Mon Sep 17 00:00:00 2001 From: Kristin Laemmert Date: Wed, 23 May 2018 09:16:42 -0700 Subject: [PATCH] porting crypto functions --- lang/funcs/crypto.go | 115 ++++++++++++++++++ lang/funcs/crypto_test.go | 110 +++++++++++++++++ lang/functions.go | 8 +- .../functions/base64sha256.html.md | 3 +- .../functions/base64sha512.html.md | 3 +- 5 files changed, 234 insertions(+), 5 deletions(-) diff --git a/lang/funcs/crypto.go b/lang/funcs/crypto.go index bfa8543d9..1e1fef65a 100644 --- a/lang/funcs/crypto.go +++ b/lang/funcs/crypto.go @@ -1,9 +1,16 @@ package funcs import ( + "crypto/sha256" + "crypto/sha512" + "encoding/base64" + "fmt" + uuid "github.com/hashicorp/go-uuid" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/function" + "github.com/zclconf/go-cty/cty/gocty" + "golang.org/x/crypto/bcrypt" ) var UUIDFunc = function.New(&function.Spec{ @@ -18,6 +25,82 @@ var UUIDFunc = function.New(&function.Spec{ }, }) +// Base64Sha256Func constructs a function that computes the SHA256 hash of a given string and encodes it with +// Base64. +var Base64Sha256Func = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + s := args[0].AsString() + h := sha256.New() + h.Write([]byte(s)) + shaSum := h.Sum(nil) + return cty.StringVal(base64.StdEncoding.EncodeToString(shaSum[:])), nil + }, +}) + +// Base64Sha512Func constructs a function that computes the SHA256 hash of a given string and encodes it with +// Base64. +var Base64Sha512Func = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + s := args[0].AsString() + h := sha512.New() + h.Write([]byte(s)) + shaSum := h.Sum(nil) + return cty.StringVal(base64.StdEncoding.EncodeToString(shaSum[:])), nil + }, +}) + +// BcryptFunc constructs a function that computes a hash of the given string using the Blowfish cipher. +var BcryptFunc = function.New(&function.Spec{ + Params: []function.Parameter{ + { + Name: "str", + Type: cty.String, + }, + }, + VarParam: &function.Parameter{ + Name: "cost", + Type: cty.Number, + }, + Type: function.StaticReturnType(cty.String), + Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) { + defaultCost := 10 + + if len(args) > 1 { + var val int + if err := gocty.FromCtyValue(args[1], &val); err != nil { + return cty.UnknownVal(cty.String), err + } + defaultCost = val + } + + if len(args) > 2 { + return cty.UnknownVal(cty.String), fmt.Errorf("bcrypt() takes no more than two arguments") + } + + input := args[0].AsString() + out, err := bcrypt.GenerateFromPassword([]byte(input), defaultCost) + if err != nil { + return cty.UnknownVal(cty.String), fmt.Errorf("error occured generating password %s", err.Error()) + } + + return cty.StringVal(string(out)), nil + }, +}) + // UUID generates and returns a Type-4 UUID in the standard hexadecimal string // format. // @@ -27,3 +110,35 @@ var UUIDFunc = function.New(&function.Spec{ func UUID() (cty.Value, error) { return UUIDFunc.Call(nil) } + +// Base64sha256 computes the SHA256 hash of a given string and encodes it with +// Base64. +// +// The given string is first encoded as UTF-8 and then the SHA256 algorithm is applied +// as defined in [RFC 4634](https://tools.ietf.org/html/rfc4634). The raw hash is +// then encoded with Base64 before returning. Terraform uses the "standard" Base64 +// alphabet as defined in [RFC 4648 section 4](https://tools.ietf.org/html/rfc4648#section-4). +func Base64Sha256(str cty.Value) (cty.Value, error) { + return Base64Sha256Func.Call([]cty.Value{str}) +} + +// Base64sha512 computes the SHA512 hash of a given string and encodes it with +// Base64. +// +// The given string is first encoded as UTF-8 and then the SHA256 algorithm is applied +// as defined in [RFC 4634](https://tools.ietf.org/html/rfc4634). The raw hash is +// then encoded with Base64 before returning. Terraform uses the "standard" Base64 +// alphabet as defined in [RFC 4648 section 4](https://tools.ietf.org/html/rfc4648#section-4). +func Base64Sha512(str cty.Value) (cty.Value, error) { + return Base64Sha512Func.Call([]cty.Value{str}) +} + +// Bcrypt computes a hash of the given string using the Blowfish cipher, +// returning a string in the Modular Crypt Format(https://passlib.readthedocs.io/en/stable/modular_crypt_format.html) +// usually expected in the shadow password file on many Unix systems. +func Bcrypt(str cty.Value, cost ...cty.Value) (cty.Value, error) { + args := make([]cty.Value, len(cost)+1) + args[0] = str + copy(args[1:], cost) + return BcryptFunc.Call(args) +} diff --git a/lang/funcs/crypto_test.go b/lang/funcs/crypto_test.go index 29affc6da..1d3506901 100644 --- a/lang/funcs/crypto_test.go +++ b/lang/funcs/crypto_test.go @@ -1,7 +1,11 @@ package funcs import ( + "fmt" "testing" + + "github.com/zclconf/go-cty/cty" + "golang.org/x/crypto/bcrypt" ) func TestUUID(t *testing.T) { @@ -15,3 +19,109 @@ func TestUUID(t *testing.T) { t.Errorf("wrong result length %d; want %d", got, want) } } + +func TestBase64Sha256(t *testing.T) { + tests := []struct { + String cty.Value + Want cty.Value + Err bool + }{ + { + cty.StringVal("test"), + cty.StringVal("n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg="), + false, + }, + // This would differ because we're base64-encoding hex represantiation, not raw bytes. + // base64encode(sha256("test")) = + // "OWY4NmQwODE4ODRjN2Q2NTlhMmZlYWEwYzU1YWQwMTVhM2JmNGYxYjJiMGI4MjJjZDE1ZDZjMTViMGYwMGEwOA==" + } + + for _, test := range tests { + t.Run(fmt.Sprintf("base64sha256(%#v)", test.String), func(t *testing.T) { + got, err := Base64Sha256(test.String) + + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + return + } else { + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + } + + if !got.RawEquals(test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} + +func TestBase64Sha512(t *testing.T) { + tests := []struct { + String cty.Value + Want cty.Value + Err bool + }{ + { + cty.StringVal("test"), + cty.StringVal("7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="), + false, + }, + // This would differ because we're base64-encoding hex represantiation, not raw bytes + // base64encode(sha512("test")) = + // "OZWUyNmIwZGQ0YWY3ZTc0OWFhMWE4ZWUzYzEwYWU5OTIzZjYxODk4MDc3MmU0NzNmODgxOWE1ZDQ5NDBlMGRiMjdhYzE4NWY4YTBlMWQ1Zjg0Zjg4YmM4ODdmZDY3YjE0MzczMmMzMDRjYzVmYTlhZDhlNmY1N2Y1MDAyOGE4ZmY=" + } + + for _, test := range tests { + t.Run(fmt.Sprintf("base64sha512(%#v)", test.String), func(t *testing.T) { + got, err := Base64Sha512(test.String) + + if test.Err { + if err == nil { + t.Fatal("succeeded; want error") + } + return + } else { + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + } + + if !got.RawEquals(test.Want) { + t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) + } + }) + } +} + +func TestBcrypt(t *testing.T) { + // single variable test + p, err := Bcrypt(cty.StringVal("test")) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = bcrypt.CompareHashAndPassword([]byte(p.AsString()), []byte("test")) + if err != nil { + t.Fatalf("Error comparing hash and password: %s", err) + } + + // testing with two parameters + p, err = Bcrypt(cty.StringVal("test"), cty.NumberIntVal(5)) + if err != nil { + t.Fatalf("err: %s", err) + } + + err = bcrypt.CompareHashAndPassword([]byte(p.AsString()), []byte("test")) + if err != nil { + t.Fatalf("Error comparing hash and password: %s", err) + } + + // Negative test for more than two parameters + _, err = Bcrypt(cty.StringVal("test"), cty.NumberIntVal(10), cty.NumberIntVal(11)) + if err == nil { + t.Fatal("succeeded; want error") + } +} diff --git a/lang/functions.go b/lang/functions.go index 7b4c8afac..480143fe6 100644 --- a/lang/functions.go +++ b/lang/functions.go @@ -33,9 +33,9 @@ func (s *Scope) Functions() map[string]function.Function { "base64decode": funcs.Base64DecodeFunc, "base64encode": funcs.Base64EncodeFunc, "base64gzip": funcs.Base64GzipFunc, - "base64sha256": unimplFunc, // TODO - "base64sha512": unimplFunc, // TODO - "bcrypt": unimplFunc, // TODO + "base64sha256": funcs.Base64Sha256Func, + "base64sha512": funcs.Base64Sha512Func, + "bcrypt": funcs.BcryptFunc, "ceil": unimplFunc, // TODO "chomp": unimplFunc, // TODO "cidrhost": unimplFunc, // TODO @@ -63,6 +63,7 @@ func (s *Scope) Functions() map[string]function.Function { "join": funcs.JoinFunc, "jsondecode": stdlib.JSONDecodeFunc, "jsonencode": stdlib.JSONEncodeFunc, + "keys": unimplFunc, // TODO "length": funcs.LengthFunc, "list": unimplFunc, // TODO "log": unimplFunc, // TODO @@ -93,6 +94,7 @@ func (s *Scope) Functions() map[string]function.Function { "upper": stdlib.UpperFunc, "urlencode": funcs.UrlEncodeFunc, "uuid": funcs.UUIDFunc, + "values": unimplFunc, // TODO "zipmap": unimplFunc, // TODO } diff --git a/website/docs/configuration/functions/base64sha256.html.md b/website/docs/configuration/functions/base64sha256.html.md index 6d05e3791..9f77d81bc 100644 --- a/website/docs/configuration/functions/base64sha256.html.md +++ b/website/docs/configuration/functions/base64sha256.html.md @@ -10,7 +10,8 @@ description: |- # `base64sha256` Function `base64sha256` computes the SHA256 hash of a given string and encodes it with -Base64. +Base64. This is not equivalent to base64encode(sha256512("test")) since sha512() +returns hexadecimal representation. The given string is first encoded as UTF-8 and then the SHA256 algorithm is applied as defined in [RFC 4634](https://tools.ietf.org/html/rfc4634). The raw hash is diff --git a/website/docs/configuration/functions/base64sha512.html.md b/website/docs/configuration/functions/base64sha512.html.md index befcc94ad..0e7fa6eb4 100644 --- a/website/docs/configuration/functions/base64sha512.html.md +++ b/website/docs/configuration/functions/base64sha512.html.md @@ -10,7 +10,8 @@ description: |- # `base64sha512` Function `base64sha512` computes the SHA512 hash of a given string and encodes it with -Base64. +Base64. This is not equivalent to base64encode(sha512("test")) since sha512() +returns hexadecimal representation. The given string is first encoded as UTF-8 and then the SHA512 algorithm is applied as defined in [RFC 4634](https://tools.ietf.org/html/rfc4634). The raw hash is