From 4749eb5d014c86fd2debd2d1da17243c2283e22e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Moritz=20Schmitz=20von=20H=C3=BClst?= Date: Wed, 31 May 2023 13:45:13 +0200 Subject: [PATCH] Add pbkdf2hash function --- crypto.go | 20 +++++ crypto_test.go | 197 ++++++++++++++++++++++++++++++++++++++++++++++ docs/crypto.md | 21 +++++ functions.go | 30 +++---- functions_test.go | 14 +++- 5 files changed, 266 insertions(+), 16 deletions(-) diff --git a/crypto.go b/crypto.go index 13a5cd55..96d8d029 100644 --- a/crypto.go +++ b/crypto.go @@ -14,6 +14,7 @@ import ( "crypto/rsa" "crypto/sha1" "crypto/sha256" + "crypto/sha512" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" @@ -23,6 +24,7 @@ import ( "encoding/pem" "errors" "fmt" + "hash" "hash/adler32" "io" "math/big" @@ -33,6 +35,7 @@ import ( "github.com/google/uuid" bcrypt_lib "golang.org/x/crypto/bcrypt" + "golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/scrypt" ) @@ -651,3 +654,20 @@ func decryptAES(password string, crypt64 string) (string, error) { return string(decrypted[:len(decrypted)-int(decrypted[len(decrypted)-1])]), nil } + +func pbkdf2hash(password string, salt string, iterations int, keyLen int, hashFunc string) string { + var h func() hash.Hash + switch hashFunc { + case "sha1": + h = sha1.New + case "sha224": + h = crypto.SHA224.New + case "sha256": + h = sha256.New + case "sha384": + h = crypto.SHA384.New + case "sha512": + h = sha512.New + } + return string(pbkdf2.Key([]byte(password), []byte(salt), iterations, keyLen, h)) +} diff --git a/crypto_test.go b/crypto_test.go index 449e7ffd..20d7a6ca 100644 --- a/crypto_test.go +++ b/crypto_test.go @@ -1,8 +1,10 @@ package sprig import ( + "bytes" "crypto/x509" "encoding/base64" + "encoding/hex" "encoding/pem" "fmt" "strings" @@ -422,3 +424,198 @@ func TestEncryptDecryptAES(t *testing.T) { t.Error(err) } } + +type testVector struct { + password string + salt string + iter int + output []byte +} + +// Test vectors from RFC 6070, http://tools.ietf.org/html/rfc6070 +var sha1TestVectors = []testVector{ + { + "password", + "salt", + 1, + []byte{ + 0x0c, 0x60, 0xc8, 0x0f, 0x96, 0x1f, 0x0e, 0x71, + 0xf3, 0xa9, 0xb5, 0x24, 0xaf, 0x60, 0x12, 0x06, + 0x2f, 0xe0, 0x37, 0xa6, + }, + }, + { + "password", + "salt", + 2, + []byte{ + 0xea, 0x6c, 0x01, 0x4d, 0xc7, 0x2d, 0x6f, 0x8c, + 0xcd, 0x1e, 0xd9, 0x2a, 0xce, 0x1d, 0x41, 0xf0, + 0xd8, 0xde, 0x89, 0x57, + }, + }, + { + "password", + "salt", + 4096, + []byte{ + 0x4b, 0x00, 0x79, 0x01, 0xb7, 0x65, 0x48, 0x9a, + 0xbe, 0xad, 0x49, 0xd9, 0x26, 0xf7, 0x21, 0xd0, + 0x65, 0xa4, 0x29, 0xc1, + }, + }, + { + "passwordPASSWORDpassword", + "saltSALTsaltSALTsaltSALTsaltSALTsalt", + 4096, + []byte{ + 0x3d, 0x2e, 0xec, 0x4f, 0xe4, 0x1c, 0x84, 0x9b, + 0x80, 0xc8, 0xd8, 0x36, 0x62, 0xc0, 0xe4, 0x4a, + 0x8b, 0x29, 0x1a, 0x96, 0x4c, 0xf2, 0xf0, 0x70, + 0x38, + }, + }, + { + "pass\000word", + "sa\000lt", + 4096, + []byte{ + 0x56, 0xfa, 0x6a, 0xa7, 0x55, 0x48, 0x09, 0x9d, + 0xcc, 0x37, 0xd7, 0xf0, 0x34, 0x25, 0xe0, 0xc3, + }, + }, +} + +// Test vectors from +// http://stackoverflow.com/questions/5130513/pbkdf2-hmac-sha2-test-vectors +var sha256TestVectors = []testVector{ + { + "password", + "salt", + 1, + []byte{ + 0x12, 0x0f, 0xb6, 0xcf, 0xfc, 0xf8, 0xb3, 0x2c, + 0x43, 0xe7, 0x22, 0x52, 0x56, 0xc4, 0xf8, 0x37, + 0xa8, 0x65, 0x48, 0xc9, + }, + }, + { + "password", + "salt", + 2, + []byte{ + 0xae, 0x4d, 0x0c, 0x95, 0xaf, 0x6b, 0x46, 0xd3, + 0x2d, 0x0a, 0xdf, 0xf9, 0x28, 0xf0, 0x6d, 0xd0, + 0x2a, 0x30, 0x3f, 0x8e, + }, + }, + { + "password", + "salt", + 4096, + []byte{ + 0xc5, 0xe4, 0x78, 0xd5, 0x92, 0x88, 0xc8, 0x41, + 0xaa, 0x53, 0x0d, 0xb6, 0x84, 0x5c, 0x4c, 0x8d, + 0x96, 0x28, 0x93, 0xa0, + }, + }, + { + "passwordPASSWORDpassword", + "saltSALTsaltSALTsaltSALTsaltSALTsalt", + 4096, + []byte{ + 0x34, 0x8c, 0x89, 0xdb, 0xcb, 0xd3, 0x2b, 0x2f, + 0x32, 0xd8, 0x14, 0xb8, 0x11, 0x6e, 0x84, 0xcf, + 0x2b, 0x17, 0x34, 0x7e, 0xbc, 0x18, 0x00, 0x18, + 0x1c, + }, + }, + { + "pass\000word", + "sa\000lt", + 4096, + []byte{ + 0x89, 0xb6, 0x9d, 0x05, 0x16, 0xf8, 0x29, 0x89, + 0x3c, 0x69, 0x62, 0x26, 0x65, 0x0a, 0x86, 0x87, + }, + }, +} + +// Test vectors from +// https://github.com/Anti-weakpasswords/PBKDF2-Test-Vectors/releases/tag/1.0 +var sha224TestVectors = []testVector{ + { + "passDATAb00AB7YxDTTlRH2dqxD", + "saltKEYbcTcXHCBxtjD2PnBh44A", + 1, + decodeHexString("86AB2F3D0CB39839B46DA2DD8F210915D79AD2E6F2093D155D75C8D9"), + }, + { + "passDATAb00AB7YxDTTlRH2dqxD", + "saltKEYbcTcXHCBxtjD2PnBh44A", + 100000, + decodeHexString("0ADF2D99E7FF8DBC6B1DF4382D32959021BFDACB99B796BF9089D0E3"), + }, +} + +// Test vectors from +// https://github.com/Anti-weakpasswords/PBKDF2-Test-Vectors/releases/tag/1.0 +var sha384TestVectors = []testVector{ + { + "passDATAb00AB7YxDTTlRH2dqxDx19GDxDV1zFMz7E6QVqK", + "saltKEYbcTcXHCBxtjD2PnBh44AIQ6XUOCESOhXpEp3HrcG", + 1, + decodeHexString("0644A3489B088AD85A0E42BE3E7F82500EC18936699151A2C90497151BAC7BB69300386A5E798795BE3CEF0A3C803227"), + }, + { + "passDATAb00AB7YxDTTlRH2dqxDx19GDxDV1zFMz7E6QVqK", + "saltKEYbcTcXHCBxtjD2PnBh44AIQ6XUOCESOhXpEp3HrcG", + 100000, + decodeHexString("BF625685B48FE6F187A1780C5CB8E1E4A7B0DBD6F551827F7B2B598735EAC158D77AFD3602383D9A685D87F8B089AF30"), + }, +} + +// Test vectors from +// https://github.com/Anti-weakpasswords/PBKDF2-Test-Vectors/releases/tag/1.0 +var sha512TestVectors = []testVector{ + { + "passDATAb00AB7YxDTT", + "saltKEYbcTcXHCBxtjD", + 1, + decodeHexString("CBE6088AD4359AF42E603C2A33760EF9D4017A7B2AAD10AF46F992C660A0B461ECB0DC2A79C2570941BEA6A08D15D6887E79F32B132E1C134E9525EEDDD744FA"), + }, + { + "passDATAb00AB7YxDTT", + "saltKEYbcTcXHCBxtjD", + 100000, + decodeHexString("ACCDCD8798AE5CD85804739015EF2A11E32591B7B7D16F76819B30B0D49D80E1ABEA6C9822B80A1FDFE421E26F5603ECA8A47A64C9A004FB5AF8229F762FF41F"), + }, +} + +func decodeHexString(decode string) []byte { + if value, err := hex.DecodeString(decode); err == nil { + return value + } + return []byte(`failed to decode hex string`) +} + +func testHash(t *testing.T, hashName string, vectors []testVector) { + for i, v := range vectors { + tpl := fmt.Sprintf("{{ pbkdf2hash \"%s\" \"%s\" %d %d \"%s\" }}", v.password, v.salt, v.iter, len(v.output), hashName) + o, err := runBytes(tpl, map[string]string{}) + if err != nil { + t.Error(err) + } + if !bytes.Equal(o, v.output) { + t.Errorf("%s %d: expected %x, got %x", hashName, i, v.output, o) + } + } +} + +func TestPbkdf2hash(t *testing.T) { + testHash(t, "sha1", sha1TestVectors) + testHash(t, "sha224", sha224TestVectors) + testHash(t, "sha256", sha256TestVectors) + testHash(t, "sha384", sha384TestVectors) + testHash(t, "sha512", sha512TestVectors) +} diff --git a/docs/crypto.md b/docs/crypto.md index b889d72a..651541cb 100644 --- a/docs/crypto.md +++ b/docs/crypto.md @@ -256,3 +256,24 @@ algorithm and returns the decoded text. ``` "30tEfhuJSVRhpG97XCuWgz2okj7L8vQ1s6V9zVUPeDQ=" | decryptAES "secretkey" ``` + +## pbkdf2hash + +The `pbkdf2hash` function derives a key by applying a pseudorandom function to a given password and salt. + +It takes the following parameters: + +- Password (master password to derive the key from) +- Salt (sequence of bits) +- Desired number of iterations +- Desired key length of the derived key +- Hash-function to use. Supported values are: + - sha1 + - sha224 + - sha256 + - sha384 + - sha512 + +``` +pbkdf2hash "password" "salt" 4096 32 sha1 +``` diff --git a/functions.go b/functions.go index 57fcec1d..c4eb37cc 100644 --- a/functions.go +++ b/functions.go @@ -22,8 +22,7 @@ import ( // // Use this to pass the functions into the template engine: // -// tpl := template.New("foo").Funcs(sprig.FuncMap())) -// +// tpl := template.New("foo").Funcs(sprig.FuncMap())) func FuncMap() template.FuncMap { return HtmlFuncMap() } @@ -336,20 +335,21 @@ var genericMap = map[string]interface{}{ "mustChunk": mustChunk, // Crypto: - "bcrypt": bcrypt, - "htpasswd": htpasswd, - "genPrivateKey": generatePrivateKey, - "derivePassword": derivePassword, - "buildCustomCert": buildCustomCertificate, - "genCA": generateCertificateAuthority, - "genCAWithKey": generateCertificateAuthorityWithPEMKey, - "genSelfSignedCert": generateSelfSignedCertificate, + "bcrypt": bcrypt, + "htpasswd": htpasswd, + "genPrivateKey": generatePrivateKey, + "derivePassword": derivePassword, + "buildCustomCert": buildCustomCertificate, + "genCA": generateCertificateAuthority, + "genCAWithKey": generateCertificateAuthorityWithPEMKey, + "genSelfSignedCert": generateSelfSignedCertificate, "genSelfSignedCertWithKey": generateSelfSignedCertificateWithPEMKey, - "genSignedCert": generateSignedCertificate, - "genSignedCertWithKey": generateSignedCertificateWithPEMKey, - "encryptAES": encryptAES, - "decryptAES": decryptAES, - "randBytes": randBytes, + "genSignedCert": generateSignedCertificate, + "genSignedCertWithKey": generateSignedCertificateWithPEMKey, + "encryptAES": encryptAES, + "decryptAES": decryptAES, + "randBytes": randBytes, + "pbkdf2hash": pbkdf2hash, // UUIDs: "uuidv4": uuidv4, diff --git a/functions_test.go b/functions_test.go index af78976b..b5f3d2cd 100644 --- a/functions_test.go +++ b/functions_test.go @@ -110,7 +110,7 @@ func runtv(tpl, expect string, vars interface{}) error { return nil } -// runRaw runs a template with the given variables and returns the result. +// runRaw runs a template with the given variables and returns the result as string. func runRaw(tpl string, vars interface{}) (string, error) { fmap := TxtFuncMap() t := template.Must(template.New("test").Funcs(fmap).Parse(tpl)) @@ -121,3 +121,15 @@ func runRaw(tpl string, vars interface{}) (string, error) { } return b.String(), nil } + +// runBytes runs a template with the given variables and returns the result as byte slice. +func runBytes(tpl string, vars interface{}) ([]byte, error) { + fmap := TxtFuncMap() + t := template.Must(template.New("test").Funcs(fmap).Parse(tpl)) + var b bytes.Buffer + err := t.Execute(&b, vars) + if err != nil { + return []byte(``), err + } + return b.Bytes(), err +}