github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/lang/funcs/crypto.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  package funcs
     5  
     6  import (
     7  	"crypto/md5"
     8  	"crypto/rsa"
     9  	"crypto/sha1"
    10  	"crypto/sha256"
    11  	"crypto/sha512"
    12  	"encoding/asn1"
    13  	"encoding/base64"
    14  	"encoding/hex"
    15  	"fmt"
    16  	"hash"
    17  	"io"
    18  	"strings"
    19  
    20  	uuidv5 "github.com/google/uuid"
    21  	uuid "github.com/hashicorp/go-uuid"
    22  	"github.com/zclconf/go-cty/cty"
    23  	"github.com/zclconf/go-cty/cty/function"
    24  	"github.com/zclconf/go-cty/cty/gocty"
    25  	"golang.org/x/crypto/bcrypt"
    26  	"golang.org/x/crypto/ssh"
    27  )
    28  
    29  var UUIDFunc = function.New(&function.Spec{
    30  	Params:       []function.Parameter{},
    31  	Type:         function.StaticReturnType(cty.String),
    32  	RefineResult: refineNotNull,
    33  	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
    34  		result, err := uuid.GenerateUUID()
    35  		if err != nil {
    36  			return cty.UnknownVal(cty.String), err
    37  		}
    38  		return cty.StringVal(result), nil
    39  	},
    40  })
    41  
    42  var UUIDV5Func = function.New(&function.Spec{
    43  	Params: []function.Parameter{
    44  		{
    45  			Name: "namespace",
    46  			Type: cty.String,
    47  		},
    48  		{
    49  			Name: "name",
    50  			Type: cty.String,
    51  		},
    52  	},
    53  	Type:         function.StaticReturnType(cty.String),
    54  	RefineResult: refineNotNull,
    55  	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
    56  		var namespace uuidv5.UUID
    57  		switch {
    58  		case args[0].AsString() == "dns":
    59  			namespace = uuidv5.NameSpaceDNS
    60  		case args[0].AsString() == "url":
    61  			namespace = uuidv5.NameSpaceURL
    62  		case args[0].AsString() == "oid":
    63  			namespace = uuidv5.NameSpaceOID
    64  		case args[0].AsString() == "x500":
    65  			namespace = uuidv5.NameSpaceX500
    66  		default:
    67  			if namespace, err = uuidv5.Parse(args[0].AsString()); err != nil {
    68  				return cty.UnknownVal(cty.String), fmt.Errorf("uuidv5() doesn't support namespace %s (%v)", args[0].AsString(), err)
    69  			}
    70  		}
    71  		val := args[1].AsString()
    72  		return cty.StringVal(uuidv5.NewSHA1(namespace, []byte(val)).String()), nil
    73  	},
    74  })
    75  
    76  // Base64Sha256Func constructs a function that computes the SHA256 hash of a given string
    77  // and encodes it with Base64.
    78  var Base64Sha256Func = makeStringHashFunction(sha256.New, base64.StdEncoding.EncodeToString)
    79  
    80  // MakeFileBase64Sha256Func constructs a function that is like Base64Sha256Func but reads the
    81  // contents of a file rather than hashing a given literal string.
    82  func MakeFileBase64Sha256Func(baseDir string) function.Function {
    83  	return makeFileHashFunction(baseDir, sha256.New, base64.StdEncoding.EncodeToString)
    84  }
    85  
    86  // Base64Sha512Func constructs a function that computes the SHA256 hash of a given string
    87  // and encodes it with Base64.
    88  var Base64Sha512Func = makeStringHashFunction(sha512.New, base64.StdEncoding.EncodeToString)
    89  
    90  // MakeFileBase64Sha512Func constructs a function that is like Base64Sha512Func but reads the
    91  // contents of a file rather than hashing a given literal string.
    92  func MakeFileBase64Sha512Func(baseDir string) function.Function {
    93  	return makeFileHashFunction(baseDir, sha512.New, base64.StdEncoding.EncodeToString)
    94  }
    95  
    96  // BcryptFunc constructs a function that computes a hash of the given string using the Blowfish cipher.
    97  var BcryptFunc = function.New(&function.Spec{
    98  	Params: []function.Parameter{
    99  		{
   100  			Name: "str",
   101  			Type: cty.String,
   102  		},
   103  	},
   104  	VarParam: &function.Parameter{
   105  		Name: "cost",
   106  		Type: cty.Number,
   107  	},
   108  	Type:         function.StaticReturnType(cty.String),
   109  	RefineResult: refineNotNull,
   110  	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
   111  		defaultCost := 10
   112  
   113  		if len(args) > 1 {
   114  			var val int
   115  			if err := gocty.FromCtyValue(args[1], &val); err != nil {
   116  				return cty.UnknownVal(cty.String), err
   117  			}
   118  			defaultCost = val
   119  		}
   120  
   121  		if len(args) > 2 {
   122  			return cty.UnknownVal(cty.String), fmt.Errorf("bcrypt() takes no more than two arguments")
   123  		}
   124  
   125  		input := args[0].AsString()
   126  		out, err := bcrypt.GenerateFromPassword([]byte(input), defaultCost)
   127  		if err != nil {
   128  			return cty.UnknownVal(cty.String), fmt.Errorf("error occured generating password %s", err.Error())
   129  		}
   130  
   131  		return cty.StringVal(string(out)), nil
   132  	},
   133  })
   134  
   135  // Md5Func constructs a function that computes the MD5 hash of a given string and encodes it with hexadecimal digits.
   136  var Md5Func = makeStringHashFunction(md5.New, hex.EncodeToString)
   137  
   138  // MakeFileMd5Func constructs a function that is like Md5Func but reads the
   139  // contents of a file rather than hashing a given literal string.
   140  func MakeFileMd5Func(baseDir string) function.Function {
   141  	return makeFileHashFunction(baseDir, md5.New, hex.EncodeToString)
   142  }
   143  
   144  // RsaDecryptFunc constructs a function that decrypts an RSA-encrypted ciphertext.
   145  var RsaDecryptFunc = function.New(&function.Spec{
   146  	Params: []function.Parameter{
   147  		{
   148  			Name: "ciphertext",
   149  			Type: cty.String,
   150  		},
   151  		{
   152  			Name: "privatekey",
   153  			Type: cty.String,
   154  		},
   155  	},
   156  	Type:         function.StaticReturnType(cty.String),
   157  	RefineResult: refineNotNull,
   158  	Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
   159  		s := args[0].AsString()
   160  		key := args[1].AsString()
   161  
   162  		b, err := base64.StdEncoding.DecodeString(s)
   163  		if err != nil {
   164  			return cty.UnknownVal(cty.String), function.NewArgErrorf(0, "failed to decode input %q: cipher text must be base64-encoded", s)
   165  		}
   166  
   167  		rawKey, err := ssh.ParseRawPrivateKey([]byte(key))
   168  		if err != nil {
   169  			var errStr string
   170  			switch e := err.(type) {
   171  			case asn1.SyntaxError:
   172  				errStr = strings.ReplaceAll(e.Error(), "asn1: syntax error", "invalid ASN1 data in the given private key")
   173  			case asn1.StructuralError:
   174  				errStr = strings.ReplaceAll(e.Error(), "asn1: struture error", "invalid ASN1 data in the given private key")
   175  			default:
   176  				errStr = fmt.Sprintf("invalid private key: %s", e)
   177  			}
   178  			return cty.UnknownVal(cty.String), function.NewArgErrorf(1, errStr)
   179  		}
   180  		privateKey, ok := rawKey.(*rsa.PrivateKey)
   181  		if !ok {
   182  			return cty.UnknownVal(cty.String), function.NewArgErrorf(1, "invalid private key type %t", rawKey)
   183  		}
   184  
   185  		out, err := rsa.DecryptPKCS1v15(nil, privateKey, b)
   186  		if err != nil {
   187  			return cty.UnknownVal(cty.String), fmt.Errorf("failed to decrypt: %s", err)
   188  		}
   189  
   190  		return cty.StringVal(string(out)), nil
   191  	},
   192  })
   193  
   194  // Sha1Func contructs a function that computes the SHA1 hash of a given string
   195  // and encodes it with hexadecimal digits.
   196  var Sha1Func = makeStringHashFunction(sha1.New, hex.EncodeToString)
   197  
   198  // MakeFileSha1Func constructs a function that is like Sha1Func but reads the
   199  // contents of a file rather than hashing a given literal string.
   200  func MakeFileSha1Func(baseDir string) function.Function {
   201  	return makeFileHashFunction(baseDir, sha1.New, hex.EncodeToString)
   202  }
   203  
   204  // Sha256Func contructs a function that computes the SHA256 hash of a given string
   205  // and encodes it with hexadecimal digits.
   206  var Sha256Func = makeStringHashFunction(sha256.New, hex.EncodeToString)
   207  
   208  // MakeFileSha256Func constructs a function that is like Sha256Func but reads the
   209  // contents of a file rather than hashing a given literal string.
   210  func MakeFileSha256Func(baseDir string) function.Function {
   211  	return makeFileHashFunction(baseDir, sha256.New, hex.EncodeToString)
   212  }
   213  
   214  // Sha512Func contructs a function that computes the SHA512 hash of a given string
   215  // and encodes it with hexadecimal digits.
   216  var Sha512Func = makeStringHashFunction(sha512.New, hex.EncodeToString)
   217  
   218  // MakeFileSha512Func constructs a function that is like Sha512Func but reads the
   219  // contents of a file rather than hashing a given literal string.
   220  func MakeFileSha512Func(baseDir string) function.Function {
   221  	return makeFileHashFunction(baseDir, sha512.New, hex.EncodeToString)
   222  }
   223  
   224  func makeStringHashFunction(hf func() hash.Hash, enc func([]byte) string) function.Function {
   225  	return function.New(&function.Spec{
   226  		Params: []function.Parameter{
   227  			{
   228  				Name: "str",
   229  				Type: cty.String,
   230  			},
   231  		},
   232  		Type:         function.StaticReturnType(cty.String),
   233  		RefineResult: refineNotNull,
   234  		Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
   235  			s := args[0].AsString()
   236  			h := hf()
   237  			h.Write([]byte(s))
   238  			rv := enc(h.Sum(nil))
   239  			return cty.StringVal(rv), nil
   240  		},
   241  	})
   242  }
   243  
   244  func makeFileHashFunction(baseDir string, hf func() hash.Hash, enc func([]byte) string) function.Function {
   245  	return function.New(&function.Spec{
   246  		Params: []function.Parameter{
   247  			{
   248  				Name: "path",
   249  				Type: cty.String,
   250  			},
   251  		},
   252  		Type:         function.StaticReturnType(cty.String),
   253  		RefineResult: refineNotNull,
   254  		Impl: func(args []cty.Value, retType cty.Type) (ret cty.Value, err error) {
   255  			path := args[0].AsString()
   256  			f, err := openFile(baseDir, path)
   257  			if err != nil {
   258  				return cty.UnknownVal(cty.String), err
   259  			}
   260  			defer f.Close()
   261  
   262  			h := hf()
   263  			_, err = io.Copy(h, f)
   264  			if err != nil {
   265  				return cty.UnknownVal(cty.String), err
   266  			}
   267  			rv := enc(h.Sum(nil))
   268  			return cty.StringVal(rv), nil
   269  		},
   270  	})
   271  }
   272  
   273  // UUID generates and returns a Type-4 UUID in the standard hexadecimal string
   274  // format.
   275  //
   276  // This is not a pure function: it will generate a different result for each
   277  // call. It must therefore be registered as an impure function in the function
   278  // table in the "lang" package.
   279  func UUID() (cty.Value, error) {
   280  	return UUIDFunc.Call(nil)
   281  }
   282  
   283  // UUIDV5 generates and returns a Type-5 UUID in the standard hexadecimal string
   284  // format.
   285  func UUIDV5(namespace cty.Value, name cty.Value) (cty.Value, error) {
   286  	return UUIDV5Func.Call([]cty.Value{namespace, name})
   287  }
   288  
   289  // Base64Sha256 computes the SHA256 hash of a given string and encodes it with
   290  // Base64.
   291  //
   292  // The given string is first encoded as UTF-8 and then the SHA256 algorithm is applied
   293  // as defined in RFC 4634. The raw hash is then encoded with Base64 before returning.
   294  // Terraform uses the "standard" Base64 alphabet as defined in RFC 4648 section 4.
   295  func Base64Sha256(str cty.Value) (cty.Value, error) {
   296  	return Base64Sha256Func.Call([]cty.Value{str})
   297  }
   298  
   299  // Base64Sha512 computes the SHA512 hash of a given string and encodes it with
   300  // Base64.
   301  //
   302  // The given string is first encoded as UTF-8 and then the SHA256 algorithm is applied
   303  // as defined in RFC 4634. The raw hash is then encoded with Base64 before returning.
   304  // Terraform uses the "standard" Base64  alphabet as defined in RFC 4648 section 4
   305  func Base64Sha512(str cty.Value) (cty.Value, error) {
   306  	return Base64Sha512Func.Call([]cty.Value{str})
   307  }
   308  
   309  // Bcrypt computes a hash of the given string using the Blowfish cipher,
   310  // returning a string in the Modular Crypt Format
   311  // usually expected in the shadow password file on many Unix systems.
   312  func Bcrypt(str cty.Value, cost ...cty.Value) (cty.Value, error) {
   313  	args := make([]cty.Value, len(cost)+1)
   314  	args[0] = str
   315  	copy(args[1:], cost)
   316  	return BcryptFunc.Call(args)
   317  }
   318  
   319  // Md5 computes the MD5 hash of a given string and encodes it with hexadecimal digits.
   320  func Md5(str cty.Value) (cty.Value, error) {
   321  	return Md5Func.Call([]cty.Value{str})
   322  }
   323  
   324  // RsaDecrypt decrypts an RSA-encrypted ciphertext, returning the corresponding
   325  // cleartext.
   326  func RsaDecrypt(ciphertext, privatekey cty.Value) (cty.Value, error) {
   327  	return RsaDecryptFunc.Call([]cty.Value{ciphertext, privatekey})
   328  }
   329  
   330  // Sha1 computes the SHA1 hash of a given string and encodes it with hexadecimal digits.
   331  func Sha1(str cty.Value) (cty.Value, error) {
   332  	return Sha1Func.Call([]cty.Value{str})
   333  }
   334  
   335  // Sha256 computes the SHA256 hash of a given string and encodes it with hexadecimal digits.
   336  func Sha256(str cty.Value) (cty.Value, error) {
   337  	return Sha256Func.Call([]cty.Value{str})
   338  }
   339  
   340  // Sha512 computes the SHA512 hash of a given string and encodes it with hexadecimal digits.
   341  func Sha512(str cty.Value) (cty.Value, error) {
   342  	return Sha512Func.Call([]cty.Value{str})
   343  }