commit dc9b8fee9247fce9a1e7fc258ed1d1064e160f75 Author: GRMrGecko Date: Sat Sep 7 18:39:33 2024 -0500 First commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a6c0c94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +test +.kdev4 +.vscode diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..ade9c16 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2024 Mr. Gecko's Media (James Coleman). http://mrgeckosmedia.com/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e5905bf --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# go-passwd + +This is a libxcrypt compatible password hashing library for the Go language. The passwords generated with this library is fully compatible with libxcrypt which can be used to generate or test passwords in use by software such as MySQL or the Linux shadow system. + +## Install + +``` +go get github.com/GRMrGecko/go-passwd +``` + +## Example + +```go +package main + +import ( + "github.com/GRMrGecko/go-passwd" + "log" +) + +func main() { + result, err := passwd.CheckPassword([]byte("$y$j9T$Q3N1jZa3Cp.yNINNDt5dDgYkHU7k$9o7WJJB5F.tTEhZdz6T6LMWY/0C3JkhvmcNyUPvUBlC"), []byte("Test")) + if err != nil { + log.Fatalln(err) + } + + if result { + log.Println("Password confirmed, saving new password.") + + pw := passwd.NewSHA512CryptPasswd() + hash, err := pw.HashPassword([]byte("New Password!!!")) + if err != nil { + log.Fatalln(err) + } + log.Println("The new password hash to save is:", string(hash)) + } +} + +``` + +Example output: +``` +$ ./test +2024/09/07 18:42:35 Password confirmed, saving new password. +2024/09/07 18:42:35 The new password hash to save is: $6$4Eu/l5e.otcRj0rJ$YAlwxJD9pZY9.Z2TjseCbkXiUIrFU2AXh9DPEm5Z1SagxP..xaQCsz7jAgfW4nmUbLh.o23pEZGvvxPCLltf11 +``` + +## Docs + +[https://pkg.go.dev/github.com/GRMrGecko/go-passwd](https://pkg.go.dev/github.com/GRMrGecko/go-passwd) + +## Known issues + + - It is possible to generate password hashes that are incompatible with libxcrypt by setting a large round count. This may be mitigated in the future by adding an option to disable compatibility and otherwise require compatible parameters to be set. + - The bcrypt hashing algorithms are not implemented yet, it may be implemented in the near futre. diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..88c4280 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module github.com/GRMrGecko/go-passwd + +go 1.22.4 + +toolchain go1.23.1 + +require ( + github.com/openwall/yescrypt-go v1.0.0 + github.com/pedroalbanese/gogost v0.0.0-20240430171730-f95129c7a5af + golang.org/x/crypto v0.25.0 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..252b9a7 --- /dev/null +++ b/go.sum @@ -0,0 +1,6 @@ +github.com/openwall/yescrypt-go v1.0.0 h1:jsGk48zkFvtUjGVOhYPGh+CS595JmTRcKnpggK2AON4= +github.com/openwall/yescrypt-go v1.0.0/go.mod h1:e6CWtFizUEOUttaOjeVMiv1lJaJie3mfOtLJ9CCD6sA= +github.com/pedroalbanese/gogost v0.0.0-20240430171730-f95129c7a5af h1:8jbTN9e84FOzAJtCPdy/NEz8983YdD7nqTBMQlTRP4w= +github.com/pedroalbanese/gogost v0.0.0-20240430171730-f95129c7a5af/go.mod h1:A4x4C7B6z2POO1x5CZzKXZVCOFPfjzxxVUbWl2Thhp0= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= diff --git a/gost_yes_crypt.go b/gost_yes_crypt.go new file mode 100644 index 0000000..723b2bd --- /dev/null +++ b/gost_yes_crypt.go @@ -0,0 +1,81 @@ +package passwd + +import ( + "crypto/hmac" + "fmt" + + "github.com/openwall/yescrypt-go" + "github.com/pedroalbanese/gogost/gost34112012256" +) + +type GostYesCrypt struct { + Passwd +} + +// Make an MD5Crypt password instance. +func NewGostYesCryptPasswd() PasswdInterface { + m := new(GostYesCrypt) + m.Magic = GOST_YES_CRYPT_MAGIC + m.SetSCryptParams(11, 31) + m.SaltLength = 22 + // Set the interface to allow parents to call overriden functions. + m.i = m + return m +} + +// Sets the SCrypt params using integers. +func (a *GostYesCrypt) SetSCryptParams(N, r int) (err error) { + Nval, err := IToA64(N) + if err != nil { + return + } + rval, err := IToA64(r) + if err != nil { + return + } + a.Params = fmt.Sprintf("j%c%c", Nval, rval) + return +} + +// Decode SCrypt params. +func (a *GostYesCrypt) DecodeSCriptParams() (N, r int) { + b64 := []byte(a.Params) + if len(b64) != 3 { + return + } + N = AToI64(b64[1]) + r = AToI64(b64[2]) + return +} + +// Hash a password with salt using gost yes crypt standard. +func (a *GostYesCrypt) Hash(password []byte, salt []byte) (hash []byte, err error) { + output := []byte(fmt.Sprintf("%s%s$%s", YES_CRYPT_MAGIC, a.Params, salt)) + yescryptHash, err := yescrypt.Hash(password, output) + if err != nil { + return + } + bytes := SCryptBase64Decode(yescryptHash[len(output)+1:]) + + h := gost34112012256.New() + h.Write(password) + hmacKey := h.Sum(nil) + + settings := []byte(fmt.Sprintf("%s%s$%s", a.Magic, a.Params, salt)) + hm := hmac.New(gost34112012256.New, hmacKey) + hm.Write(settings) + hmacKey = hm.Sum(nil) + hm = hmac.New(gost34112012256.New, hmacKey) + hm.Write(bytes) + b64 := SCryptBase64Encode(hm.Sum(nil)) + + hash = append(settings, '$') + hash = append(hash, b64...) + return +} + +// Override the passwd hash with salt function to hash with gost yes crypt. +func (a *GostYesCrypt) HashPasswordWithSalt(password []byte, salt []byte) (hash []byte, err error) { + hash, err = a.Hash(password, salt) + return +} diff --git a/md5_crypt.go b/md5_crypt.go new file mode 100644 index 0000000..fe61b44 --- /dev/null +++ b/md5_crypt.go @@ -0,0 +1,110 @@ +package passwd + +import "crypto/md5" + +type MD5Crypt struct { + Passwd +} + +// Make an MD5Crypt password instance. +func NewMD5CryptPasswd() PasswdInterface { + m := new(MD5Crypt) + m.Magic = MD5_CRYPT_MAGIC + // Max of 8 characters in the salt per the spec. + m.SaltLength = 8 + // Set the interface to allow parents to call overriden functions. + m.i = m + return m +} + +// Hash a password with salt using MD5 crypt standard. +func (a *MD5Crypt) Hash(password []byte, salt []byte) (hash []byte) { + magic := []byte(a.Magic) + + // Salt should be a maximum of 8 characters. + if len(salt) > 8 { + salt = salt[0:8] + } + + // Encode pass, salt, pass hash to feed into the next hash. + h := md5.New() + h.Write(password) + h.Write(salt) + h.Write(password) + result := h.Sum(nil) + + // Encode pass, magic, salt, and some extra stuff to help limit brute force attacks. + h.Reset() + h.Write(password) + h.Write(magic) + h.Write(salt) + + // Append characters from the prior encode until it equals the length of the password. + HashBlockRecycle(h, result, len(password)) + + // For compatibility: Every 1 bit of the password length, append null. + // Every 0 bit of the password length, append the first character of the + // password. Yes, this is a weird thing. But think, weird thing equals + // harder for brute forcers. + result = []byte{'\000'} + var cnt int + for cnt = len(password); cnt > 0; cnt >>= 1 { + if cnt&1 != 0 { + h.Write(result[:1]) + } else { + h.Write(password[:1]) + } + } + + // Compute the hash to feed into the 1000 iterations. + result = h.Sum(nil) + + // For 1000 iterations, make a new hash feeding the prior hash, + // password, and salt at different points. This is designed to + // limit brute force attempts, although todays tech is fast. + for cnt = 0; cnt < 1000; cnt++ { + h.Reset() + + // Add pass or prior result depending on bit of current iteration. + if cnt&1 != 0 { + h.Write(password) + } else { + h.Write(result) + } + + // Add salt for numbers not divisible by 3. + if cnt%3 != 0 { + h.Write(salt) + } + + // Add password for numbers not divisible by 7. + if cnt%7 != 0 { + h.Write(password) + } + + // Add the reverse of the above pass or prior result. + // This ensures we at a minimum have both the password, + // and the prior result in the hash calculation for the round. + if cnt&1 != 0 { + h.Write(result) + } else { + h.Write(password) + } + + // Compute hash for next round. + result = h.Sum(nil) + } + + // Create hash with result. + b64 := MD5Base64Encode(result) + hash = append(magic, salt...) + hash = append(hash, '$') + hash = append(hash, b64...) + return +} + +// Override the passwd hash with salt function to hash with MD5 crypt. +func (a *MD5Crypt) HashPasswordWithSalt(password []byte, salt []byte) (hash []byte, err error) { + hash = a.Hash(password, salt) + return +} diff --git a/nt_hash.go b/nt_hash.go new file mode 100644 index 0000000..7dc68c2 --- /dev/null +++ b/nt_hash.go @@ -0,0 +1,84 @@ +package passwd + +import ( + "encoding/binary" + "encoding/hex" + "unicode/utf16" + "unicode/utf8" + + "golang.org/x/crypto/md4" +) + +type NTHash struct { + Passwd +} + +// Make NTHash password interface. +func NewNTPasswd() PasswdInterface { + m := new(NTHash) + m.Magic = NT_HASH_MAGIC + // NT hashes has no salt, so we disable it. + m.SaltLength = -1 + // Set the interface to allow parents to call overriden functions. + m.i = m + return m +} + +// Encode UTF-8 bytes to UCS-2LE bytes. +// The NT hash uses UCS-2LE, so we need to convert for compatibility. +func (a *NTHash) UTF8ToUCS2LE(src []byte) []byte { + // If there is no source data, return nil. + if len(src) == 0 { + return nil + } + + // Convert bytes to UTF-8 runes. + var runes []rune + for len(src) > 0 { + r, size := utf8.DecodeRune(src) + runes = append(runes, r) + src = src[size:] + } + + // Re-encode UTF-8 to UTF-16. + u := utf16.Encode(runes) + + // Setup new byte array to match length of UCS-2LE. + dst := make([]byte, len(u)*2) + + // Index for inserting new bytes. + i := 0 + + // Convert each UTF-16 byte to UCS-2LE. + for _, r := range u { + binary.LittleEndian.PutUint16(dst[i:], r) + i += 2 + } + return dst +} + +// Hash an NT compatible hash. +func (a *NTHash) Hash(password []byte) (hash []byte) { + // Convert to UCS-2. + ucsPw := a.UTF8ToUCS2LE(password) + + // Encoe MD4 hash with UCS-2LE bytes. + h := md4.New() + h.Write(ucsPw) + buf := h.Sum(nil) + + // Hex encode MD4 hash. + dst := make([]byte, hex.EncodedLen(len(buf))) + hex.Encode(dst, buf) + + // Make crypt compatible hash from encoded hash. + hash = append([]byte(a.Magic), '$') + hash = append(hash, dst...) + return +} + +// Override the hash with salt function with one that encodes the NT hash, ignoring the salt. +func (a *NTHash) HashPasswordWithSalt(password []byte, salt []byte) (hash []byte, err error) { + hash = a.Hash(password) + return +} diff --git a/passwd.go b/passwd.go new file mode 100644 index 0000000..61273d2 --- /dev/null +++ b/passwd.go @@ -0,0 +1,325 @@ +package passwd + +import ( + "bytes" + "crypto/rand" + "errors" + "fmt" + "strconv" + "strings" +) + +const ( + SHA1_CRYPT_MAGIC = "$sha1$" + SHA1_SIZE = 20 + SUN_MD5_MAGIC = "$md5" + MD5_CRYPT_MAGIC = "$1$" + MD5_SIZE = 16 + NT_HASH_MAGIC = "$3$" + MD4_SIZE = 16 + SHA256_CRYPT_MAGIC = "$5$" + SHA256_SIZE = 32 + SHA512_CRYPT_MAGIC = "$6$" + SHA512_SIZE = 64 + S_CRYPT_MAGIC = "$7$" + YES_CRYPT_MAGIC = "$y$" + GOST_YES_CRYPT_MAGIC = "$gy$" +) + +// Standard protocol for working with all hash algorithms. +type PasswdInterface interface { + SetParams(p string) + SetSalt(s []byte) + GenerateSalt() ([]byte, error) + HashPassword(password []byte) (hash []byte, err error) + HashPasswordWithSalt(password []byte, salt []byte) (hash []byte, err error) +} + +// Base structure. +type Passwd struct { + Magic string + Params string + SaltLength int + Salt []byte + i PasswdInterface +} + +// Get a password interface based on hash settings string. +func NewPasswd(settings string) (PasswdInterface, error) { + // SHA1 $sha1$$[$] + if strings.HasPrefix(settings, SHA1_CRYPT_MAGIC) { + // Split by $ to get options. + s := strings.Split(settings[len(SHA1_CRYPT_MAGIC):], "$") + + // If less than 2 options, this is not a valid setting. + if len(s) < 2 { + return nil, errors.New("Too few parameters for SHA1 hash") + } + + // Confirm that the iterations can be parsed. + iterations, err := strconv.ParseUint(s[0], 10, 64) + if err != nil { + return nil, err + } + + // Make the interface. + passwd := NewSHA1Passwd() + passwd.SetParams(strconv.FormatUint(iterations, 10)) + passwd.SetSalt([]byte(s[1])) + return passwd, nil + } + + // Sun MD5 $md5[,rounds=]$[$] + if strings.HasPrefix(settings, SUN_MD5_MAGIC) { + s := strings.Split(settings[len(SUN_MD5_MAGIC):], "$") + + // If less than 2 options, this is not a valid setting. + if len(s) < 2 { + return nil, errors.New("Too few parameters for Sun MD5 hash") + } + + // Parse iterations from parameter. + if s[0] != "" && s[0][0] == ',' { + s[0] = s[0][1:] + } + var iterations uint64 + if s[0] != "" { + _, err := fmt.Sscanf(s[0], "rounds=%d", &iterations) + if err != nil { + return nil, err + } + } + + // Make the interface. + passwd := NewSunMD5Passwd() + passwd.SetParams(s[0]) + passwd.SetSalt([]byte(s[1])) + return passwd, nil + } + + // MD5 $1$[$] + if strings.HasPrefix(settings, MD5_CRYPT_MAGIC) { + s := strings.Split(settings[len(MD5_CRYPT_MAGIC):], "$") + + // If less than 2 options, this is not a valid setting. + if len(s) < 1 { + return nil, errors.New("Too few parameters for MD5 hash") + } + + // Make the interface. + passwd := NewMD5CryptPasswd() + passwd.SetSalt([]byte(s[0])) + return passwd, nil + } + + // NT $3$[$] + if strings.HasPrefix(settings, NT_HASH_MAGIC) { + // Make the interface. + passwd := NewNTPasswd() + return passwd, nil + } + + // SHA256 $5$[rounds=$][$] + if strings.HasPrefix(settings, SHA256_CRYPT_MAGIC) { + s := strings.Split(settings[len(SHA256_CRYPT_MAGIC):], "$") + + // If less than 2 options, this is not a valid setting. + if len(s) < 1 { + return nil, errors.New("Too few parameters for SHA256 hash") + } + + // If rounds set, parse it. + var iterations uint64 + if strings.HasPrefix(s[0], "rounds=") { + _, err := fmt.Sscanf(s[0], "rounds=%d", &iterations) + if err != nil { + return nil, err + } + if len(s) < 2 { + return nil, errors.New("Too few parameters for SHA256 hash") + } + s[0] = s[1] + } + + // Make the interface. + passwd := NewSHA256CryptPasswd() + if iterations != 0 { + passwd.SetParams(fmt.Sprintf("rounds=%d", iterations)) + } + passwd.SetSalt([]byte(s[0])) + return passwd, nil + } + + // SHA512 $6$[rounds=$][$] + if strings.HasPrefix(settings, SHA512_CRYPT_MAGIC) { + s := strings.Split(settings[len(SHA512_CRYPT_MAGIC):], "$") + + // If less than 2 options, this is not a valid setting. + if len(s) < 1 { + return nil, errors.New("Too few parameters for SHA512 hash") + } + + // If rounds set, parse it. + var iterations uint64 + if strings.HasPrefix(s[0], "rounds=") { + _, err := fmt.Sscanf(s[0], "rounds=%d", &iterations) + if err != nil { + return nil, err + } + if len(s) < 2 { + return nil, errors.New("Too few parameters for SHA512 hash") + } + s[0] = s[1] + } + + // Make the interface. + passwd := NewSHA512CryptPasswd() + if iterations != 0 { + passwd.SetParams(fmt.Sprintf("rounds=%d", iterations)) + } + passwd.SetSalt([]byte(s[0])) + return passwd, nil + } + + // SCrypt $7$

[$] + if strings.HasPrefix(settings, S_CRYPT_MAGIC) { + s := strings.Split(settings[len(S_CRYPT_MAGIC):], "$") + + // If less than 2 options, this is not a valid setting. + if len(s) < 1 { + return nil, errors.New("Too few parameters for SCrypt hash") + } + + if len(s[0]) < 12 { + return nil, errors.New("Too few characters in salt for SCrypt") + } + params := s[0][:11] + salt := s[0][11:] + + // Make the interface. + passwd := NewSCryptPasswd() + passwd.SetParams(params) + passwd.SetSalt([]byte(salt)) + return passwd, nil + } + + // Yes Crypt $y$j$[$] + if strings.HasPrefix(settings, YES_CRYPT_MAGIC) { + s := strings.Split(settings[len(YES_CRYPT_MAGIC):], "$") + + // If less than 2 options, this is not a valid setting. + if len(s) < 2 { + return nil, errors.New("Too few parameters for Yes Crypt hash") + } + + if len(s[0]) != 3 { + return nil, errors.New("Invalid length for Yes Crypt parameters") + } + + // Make the interface. + passwd := NewYesCryptPasswd() + passwd.SetParams(s[0]) + passwd.SetSalt([]byte(s[1])) + return passwd, nil + } + + // Gost Yes Crypt $gy$j$[$] + if strings.HasPrefix(settings, GOST_YES_CRYPT_MAGIC) { + s := strings.Split(settings[len(GOST_YES_CRYPT_MAGIC):], "$") + + // If less than 2 options, this is not a valid setting. + if len(s) < 2 { + return nil, errors.New("Too few parameters for Gost Yes Crypt hash") + } + + if len(s[0]) != 3 { + return nil, errors.New("Invalid length for Gost Yes Crypt parameters") + } + + // Make the interface. + passwd := NewGostYesCryptPasswd() + passwd.SetParams(s[0]) + passwd.SetSalt([]byte(s[1])) + return passwd, nil + } + + // End of the line. + return nil, errors.New("No valid matching algorithm") +} + +// Check a password hash against a password. +func CheckPassword(hash []byte, password []byte) (bool, error) { + passwd, err := NewPasswd(string(hash)) + if err != nil { + return false, err + } + newHash, err := passwd.HashPassword(password) + if err != nil { + return false, err + } + if bytes.Equal(hash, newHash) { + return true, nil + } + return false, nil +} + +// Used internally for salt generation. +func generateRandomBytes(n uint) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return nil, err + } + + return b, nil +} + +// Set parameters for password generation. Typically used for iterations, but also used for yes crypt configuration. +func (a *Passwd) SetParams(p string) { + a.Params = p +} + +// Set a salt for hashing, an empty salt will generate a new one. +func (a *Passwd) SetSalt(s []byte) { + a.Salt = s +} + +// Generate a salt based on configs for this paassword algorithm. +func (a *Passwd) GenerateSalt() ([]byte, error) { + var salt []byte + if a.SaltLength > -1 { + if a.SaltLength == 0 { + a.SaltLength = 16 + } + rawSalt, err := generateRandomBytes(uint(a.SaltLength)) + if err != nil { + return nil, err + } + salt = Base64Encode(rawSalt) + } + return salt, nil +} + +// Hash a password. +func (a *Passwd) HashPassword(password []byte) (hash []byte, err error) { + if len(a.Salt) == 0 { + salt, err := a.GenerateSalt() + if err != nil { + return nil, err + } + a.Salt = salt + } + + if a.i != nil { + hash, err = a.i.HashPasswordWithSalt(password, a.Salt) + } else { + hash, err = a.HashPasswordWithSalt(password, a.Salt) + } + return +} + +// Hash a password with a custom salt. +func (a *Passwd) HashPasswordWithSalt(password []byte, salt []byte) (hash []byte, err error) { + err = errors.New("hash algorithm is not implemented") + return +} diff --git a/passwd_test.go b/passwd_test.go new file mode 100644 index 0000000..dfa8979 --- /dev/null +++ b/passwd_test.go @@ -0,0 +1,162 @@ +package passwd + +import ( + "fmt" + "testing" +) + +func TestPasswd(t *testing.T) { + password := []byte("Test") + var res bool + var err error + + // Confirm password hashes conform to libcrypt standards. + res, err = CheckPassword([]byte("$sha1$245081$NabW/sfk3ZVVQc4BnZ/3$YoV1Iva6GK4tkxwahBmyH0TRCwBO"), password) + if err != nil { + t.Fatalf("sha1 error: %s", err) + } + if !res { + t.Fatalf("Password check for sha1 failed") + } + + res, err = CheckPassword([]byte("$md5$lORrojKC$$RD9p64URLn3Wkv4Wa2xOW0"), password) + if err != nil { + t.Fatalf("sun md5 error: %s", err) + } + if !res { + t.Fatalf("Password check for sun md5 failed") + } + + res, err = CheckPassword([]byte("$md5,rounds=53125$qrDebYUd$$3pJWS.a6VTC/cGehIfQb30"), password) + if err != nil { + t.Fatalf("sun md5 with rounds error: %s", err) + } + if !res { + t.Fatalf("Password check for sun md5 with rounds failed") + } + + res, err = CheckPassword([]byte("$1$wuIXYcHV$1ufSGHoD0EkWPr75i52ST/"), password) + if err != nil { + t.Fatalf("md5 error: %s", err) + } + if !res { + t.Fatalf("Password check for md5 failed") + } + + res, err = CheckPassword([]byte("$3$$4a1fab8f6b5441e0493dc7d41304bfb6"), password) + if err != nil { + t.Fatalf("nt error: %s", err) + } + if !res { + t.Fatalf("Password check for nt failed") + } + + res, err = CheckPassword([]byte("$5$AsETvlsIoaTP3w6G$OZY9mWRFXR9Pz0Xv1pS2TS/QCpxECLEG/dru/Y.nba/"), password) + if err != nil { + t.Fatalf("sha256 error: %s", err) + } + if !res { + t.Fatalf("Password check for sha256 failed") + } + + res, err = CheckPassword([]byte("$5$rounds=243006$oCvhLw/Nn9HuQIm4$VPKzWx9t.NHgmNpVHeSpzQ5y01z4BE14J.bvG8g2yi."), password) + if err != nil { + t.Fatalf("sha256 with rounds error: %s", err) + } + if !res { + t.Fatalf("Password check for sha256 with rounds failed") + } + + res, err = CheckPassword([]byte("$6$zt7D9I3Uu.EhrzEv$j50OCJ3oNdO2Ee7RE9XTDF7dhvrgRwc9NmjJUouk7czn4JTc/A6qLJIT1pMk7FUlTCYCLl6uBHm5NoEboAzIo0"), password) + if err != nil { + t.Fatalf("sha512 error: %s", err) + } + if !res { + t.Fatalf("Password check for sha512 failed") + } + + res, err = CheckPassword([]byte("$6$rounds=523044$.zMtRwbPP2sDg5a5$YgKUnqEda6wxkvDMbJoNjNBiFNpX7nP/uDFV3jV4ngmrXlFBua3n8oIi5St/Re8H3WOksLaody3eAhaGtAN0c/"), password) + if err != nil { + t.Fatalf("sha512 with rounds error: %s", err) + } + if !res { + t.Fatalf("Password check for sha512 with rounds failed") + } + + res, err = CheckPassword([]byte("$7$CU..../....PpL3ULxY5DvYyvasS/a4a0$jqgg90svZLt5KQqFTwegHSn1pXU.aKDavZ3Eq8t2wx9"), password) + if err != nil { + t.Fatalf("scrypt error: %s", err) + } + if !res { + t.Fatalf("Password check for scrypt failed") + } + + res, err = CheckPassword([]byte("$y$j9T$G/uoZu1orhwOE/lUtohEa.$SMu/wxtyhBLa5xeRLVnznBx5vE0/VxY7rJZlQX27N84"), password) + if err != nil { + t.Fatalf("yes crypt error: %s", err) + } + if !res { + t.Fatalf("Password check for yes crypt failed") + } + + res, err = CheckPassword([]byte("$gy$j9T$etkZHzB483TIuw/58Df.N/$7DjHx/8jx.E/VLdyzMIIOJULHoZJ1PNlFl71KXaf0s7"), password) + if err != nil { + t.Fatalf("gost yes crypt error: %s", err) + } + if !res { + t.Fatalf("Password check for gost yes crypt failed") + } + + // Confirm new password generation works. + var passwd PasswdInterface + var hash []byte + + passwd = NewSHA1Passwd() + hash, err = passwd.HashPassword(password) + if err != nil { + t.Fatalf("sha1 error: %s", err) + } + fmt.Println("sha1:", string(hash)) + + passwd = NewSunMD5Passwd() + hash, err = passwd.HashPassword(password) + if err != nil { + t.Fatalf("sun md5 error: %s", err) + } + fmt.Println("sun md5:", string(hash)) + + passwd = NewSHA256CryptPasswd() + hash, err = passwd.HashPassword(password) + if err != nil { + t.Fatalf("sha256 error: %s", err) + } + fmt.Println("sha256:", string(hash)) + + passwd = NewSHA512CryptPasswd() + hash, err = passwd.HashPassword(password) + if err != nil { + t.Fatalf("sha512 error: %s", err) + } + fmt.Println("sha512:", string(hash)) + + passwd = NewSCryptPasswd() + hash, err = passwd.HashPassword(password) + if err != nil { + t.Fatalf("scrypt error: %s", err) + } + fmt.Println("scrypt:", string(hash)) + + passwd = NewYesCryptPasswd() + hash, err = passwd.HashPassword(password) + if err != nil { + t.Fatalf("yes crypt error: %s", err) + } + fmt.Println("yes crypt:", string(hash)) + + passwd = NewGostYesCryptPasswd() + hash, err = passwd.HashPassword(password) + if err != nil { + t.Fatalf("gost yes crypterror: %s", err) + } + fmt.Println("gost yes crypt:", string(hash)) +} diff --git a/pbkdf1_sha1_crypt.go b/pbkdf1_sha1_crypt.go new file mode 100644 index 0000000..856f018 --- /dev/null +++ b/pbkdf1_sha1_crypt.go @@ -0,0 +1,70 @@ +package passwd + +import ( + "crypto/hmac" + "crypto/sha1" + "fmt" + "strconv" +) + +type SHA1Crypt struct { + Passwd +} + +func NewSHA1Passwd() PasswdInterface { + m := new(SHA1Crypt) + m.Magic = SHA1_CRYPT_MAGIC + m.Params = "262144" + m.i = m + return m +} + +// PBKDF1 with SHA1 crypt algorithm. +func (a *SHA1Crypt) Hash(password []byte, salt []byte, iterations uint64) (hash []byte) { + // We store the magic bytes as a string as we use sprintf to + // encode the outputs and easily translate the iterations + // from an uint64 to a string. + magic := a.Magic + + // The first bit we encode into the hmac is the salt, + // magic string, and iterations of hmac rounds. + output := fmt.Sprintf("%s%s%d", salt, magic, iterations) + + // Setup hmac with the password as the key. + hm := hmac.New(sha1.New, password) + + // Write the salt and parameters to the hmac. + hm.Write([]byte(output)) + + // Get the first sum for the iterrations. + buf := hm.Sum(nil) + + // Iterate the hmac to the specified number of iterations. + for i := uint64(1); i < iterations; i++ { + // Setup the hmac for this iteration. + hm.Reset() + + // Feed back in the buffer from the last iteration. + hm.Write(buf) + + // Get the buffer from this iteration. + buf = hm.Sum(nil) + } + + // Create hash with result. + b64 := Base64Encode(buf) + hash = []byte(fmt.Sprintf("%s%d$%s$", magic, iterations, salt)) + hash = append(hash, b64...) + return +} + +// Override the hash with salt function to encode PBKDF1 with SHA1 hash. +func (a *SHA1Crypt) HashPasswordWithSalt(password []byte, salt []byte) (hash []byte, err error) { + iterations, err := strconv.ParseUint(a.Params, 10, 64) + if err != nil { + return nil, err + } + + hash = a.Hash(password, salt, iterations) + return +} diff --git a/s_crypt.go b/s_crypt.go new file mode 100644 index 0000000..037b010 --- /dev/null +++ b/s_crypt.go @@ -0,0 +1,61 @@ +package passwd + +import ( + "fmt" + + "github.com/openwall/yescrypt-go" +) + +type SCrypt struct { + Passwd +} + +// Make an MD5Crypt password instance. +func NewSCryptPasswd() PasswdInterface { + m := new(SCrypt) + m.Magic = S_CRYPT_MAGIC + m.SetSCryptParams(14, 32, 1) + m.SaltLength = 22 + // Set the interface to allow parents to call overriden functions. + m.i = m + return m +} + +// Sets the SCrypt params using integers. +func (a *SCrypt) SetSCryptParams(N, r, p int) (err error) { + var b64 []byte + b64 = append(b64, iota64Encoding[N]) + b64 = append(b64, Base64Uint32Encode(uint32(r), 30)...) + b64 = append(b64, Base64Uint32Encode(uint32(p), 30)...) + a.Params = string(b64) + return +} + +// Decode SCrypt params. +func (a *SCrypt) DecodeSCriptParams() (N, r, p int) { + b64 := []byte(a.Params) + if len(b64) != 11 { + return + } + N = AToI64(b64[0]) + r = int(Base64Uint32Decode(b64[1:6], 30)) + p = int(Base64Uint32Decode(b64[6:11], 30)) + return +} + +// Hash a password with salt using scrypt standard. +func (a *SCrypt) Hash(password []byte, salt []byte) (hash []byte, err error) { + N, r, p := a.DecodeSCriptParams() + scryptHash, err := yescrypt.ScryptKey(password, salt, 1< 16 { + salt = salt[0:16] + } + + passwordLen := len(password) + saltLen := len(salt) + + customIterations := true + if iterations == 0 { + customIterations = false + iterations = 5000 + } + + // Encode pass, salt, pass hash to feed into the next hash. + h := sha256.New() + h.Write(password) + h.Write(salt) + h.Write(password) + result := h.Sum(nil) + + // Encod the password and salt, and recycle bytes from prior hash. + h.Reset() + h.Write(password) + h.Write(salt) + + // Append characters from the prior encode until it equals the length of the password. + HashBlockRecycle(h, result, passwordLen) + + // Alternate the prior encode with the password for the binary length of the password. + var cnt uint64 + for cnt = uint64(passwordLen); cnt > 0; cnt >>= 1 { + if cnt&1 != 0 { + h.Write(result) + } else { + h.Write(password) + } + } + + // Calculate sum for iterations. + result = h.Sum(nil) + + // Calculate a hash of password added for each character of the password for recycling in iterations. + h.Reset() + for cnt = 0; cnt < uint64(passwordLen); cnt++ { + h.Write(password) + } + p_bytes := h.Sum(nil) + + // For maximum salt size plus the integer representation of the first byte of the prior hash, + // write the entire salt to the hash for recycling in iterations. + h.Reset() + for cnt = 0; cnt < 16+uint64(result[0]); cnt++ { + h.Write(salt) + } + s_bytes := h.Sum(nil) + + // For the defined number of interations, hash using bytes from + // the above password and salt hashes and prior hash iteration. + for cnt = 0; cnt < iterations; cnt++ { + h.Reset() + + // Add pass or prior result depending on bit of current iteration. + if cnt&1 != 0 { + HashBlockRecycle(h, p_bytes, passwordLen) + } else { + h.Write(result) + } + + // Add salt for numbers not divisible by 3. + if cnt%3 != 0 { + HashBlockRecycle(h, s_bytes, saltLen) + } + + // Add password for numbers not divisible by 7. + if cnt%7 != 0 { + HashBlockRecycle(h, p_bytes, passwordLen) + } + + // Add the reverse of the above pass or prior result. + // This ensures we at a minimum have both the password, + // and the prior result in the hash calculation for the round. + if cnt&1 != 0 { + h.Write(result) + } else { + HashBlockRecycle(h, p_bytes, passwordLen) + } + + // Compute hash for next round. + result = h.Sum(nil) + } + + output := fmt.Sprintf("%s%s$", a.Magic, salt) + if customIterations { + output = fmt.Sprintf("%srounds=%d$%s$", a.Magic, iterations, salt) + } + + // Create hash with result. + b64 := Base64RotateEncode(result, false) + hash = []byte(output) + hash = append(hash, b64...) + return +} + +// Override the passwd hash with salt function to hash with SHA256 crypt. +func (a *SHA256Crypt) HashPasswordWithSalt(password []byte, salt []byte) (hash []byte, err error) { + // Parse iterations from parameter. + var iterations uint64 + if a.Params != "" { + _, err = fmt.Sscanf(a.Params, "rounds=%d", &iterations) + if err != nil { + return + } + } + + // Compute hash. + hash = a.Hash(password, salt, iterations) + return +} diff --git a/sha512_crypt.go b/sha512_crypt.go new file mode 100644 index 0000000..239e67e --- /dev/null +++ b/sha512_crypt.go @@ -0,0 +1,141 @@ +package passwd + +import ( + "crypto/sha512" + "fmt" +) + +type SHA512Crypt struct { + Passwd +} + +// Make an MD5Crypt password instance. +func NewSHA512CryptPasswd() PasswdInterface { + m := new(SHA512Crypt) + m.Magic = SHA512_CRYPT_MAGIC + // Set the interface to allow parents to call overriden functions. + m.i = m + return m +} + +// Hash a password with salt using SHA512 crypt standard. +func (a *SHA512Crypt) Hash(password []byte, salt []byte, iterations uint64) (hash []byte) { + // Salt should be a maximum of 16 characters. + if len(salt) > 16 { + salt = salt[0:16] + } + + passwordLen := len(password) + saltLen := len(salt) + + customIterations := true + if iterations == 0 { + customIterations = false + iterations = 5000 + } + + // Encode pass, salt, pass hash to feed into the next hash. + h := sha512.New() + h.Write(password) + h.Write(salt) + h.Write(password) + result := h.Sum(nil) + + // Encod the password and salt, and recycle bytes from prior hash. + h.Reset() + h.Write(password) + h.Write(salt) + + // Append characters from the prior encode until it equals the length of the password. + HashBlockRecycle(h, result, passwordLen) + + // Alternate the prior encode with the password for the binary length of the password. + var cnt uint64 + for cnt = uint64(passwordLen); cnt > 0; cnt >>= 1 { + if cnt&1 != 0 { + h.Write(result) + } else { + h.Write(password) + } + } + + // Calculate sum for iterations. + result = h.Sum(nil) + + // Calculate a hash of password added for each character of the password for recycling in iterations. + h.Reset() + for cnt = 0; cnt < uint64(passwordLen); cnt++ { + h.Write(password) + } + p_bytes := h.Sum(nil) + + // For maximum salt size plus the integer representation of the first byte of the prior hash, + // write the entire salt to the hash for recycling in iterations. + h.Reset() + for cnt = 0; cnt < 16+uint64(result[0]); cnt++ { + h.Write(salt) + } + s_bytes := h.Sum(nil) + + // For the defined number of interations, hash using bytes from + // the above password and salt hashes and prior hash iteration. + for cnt = 0; cnt < iterations; cnt++ { + h.Reset() + + // Add pass or prior result depending on bit of current iteration. + if cnt&1 != 0 { + HashBlockRecycle(h, p_bytes, passwordLen) + } else { + h.Write(result) + } + + // Add salt for numbers not divisible by 3. + if cnt%3 != 0 { + HashBlockRecycle(h, s_bytes, saltLen) + } + + // Add password for numbers not divisible by 7. + if cnt%7 != 0 { + HashBlockRecycle(h, p_bytes, passwordLen) + } + + // Add the reverse of the above pass or prior result. + // This ensures we at a minimum have both the password, + // and the prior result in the hash calculation for the round. + if cnt&1 != 0 { + h.Write(result) + } else { + HashBlockRecycle(h, p_bytes, passwordLen) + } + + // Compute hash for next round. + result = h.Sum(nil) + } + + output := fmt.Sprintf("%s%s$", a.Magic, salt) + if customIterations { + output = fmt.Sprintf("%srounds=%d$%s$", a.Magic, iterations, salt) + } + + // Create hash with result. + b64 := Base64RotateEncode(result, true) + hash = []byte(output) + hash = append(hash, b64...) + return +} + +// Override the passwd hash with salt function to hash with SHA512 crypt. +func (a *SHA512Crypt) HashPasswordWithSalt(password []byte, salt []byte) (hash []byte, err error) { + // Parse iterations from parameter. + var iterations uint64 + if a.Params != "" { + _, err = fmt.Sscanf(a.Params, "rounds=%d", &iterations) + if err != nil { + return + } + } + + // Compute hash. + hash = a.Hash(password, salt, iterations) + return +} diff --git a/sun_md5.go b/sun_md5.go new file mode 100644 index 0000000..8f602dd --- /dev/null +++ b/sun_md5.go @@ -0,0 +1,178 @@ +package passwd + +import ( + "crypto/md5" + "fmt" + "strconv" +) + +type SunMD5 struct { + Passwd +} + +// Make an MD5Crypt password instance. +func NewSunMD5Passwd() PasswdInterface { + m := new(SunMD5) + m.Magic = SUN_MD5_MAGIC + // Max of 8 characters in the salt per the spec. + m.SaltLength = 8 + // Set the interface to allow parents to call overriden functions. + m.i = m + return m +} + +/* +At each round of the algorithm, this string (including the trailing +NUL) may or may not be included in the input to MD5, depending on a +pseudorandom coin toss. It is Hamlet's famous soliloquy from the +play of the same name, which is in the public domain. Text from + with double +blank lines replaced with `\n`. Note that more recent Project +Gutenberg editions of _Hamlet_ are punctuated differently. +*/ +const hamlet_quotation string = "To be, or not to be,--that is the question:--\n" + + "Whether 'tis nobler in the mind to suffer\n" + + "The slings and arrows of outrageous fortune\n" + + "Or to take arms against a sea of troubles,\n" + + "And by opposing end them?--To die,--to sleep,--\n" + + "No more; and by a sleep to say we end\n" + + "The heartache, and the thousand natural shocks\n" + + "That flesh is heir to,--'tis a consummation\n" + + "Devoutly to be wish'd. To die,--to sleep;--\n" + + "To sleep! perchance to dream:--ay, there's the rub;\n" + + "For in that sleep of death what dreams may come,\n" + + "When we have shuffled off this mortal coil,\n" + + "Must give us pause: there's the respect\n" + + "That makes calamity of so long life;\n" + + "For who would bear the whips and scorns of time,\n" + + "The oppressor's wrong, the proud man's contumely,\n" + + "The pangs of despis'd love, the law's delay,\n" + + "The insolence of office, and the spurns\n" + + "That patient merit of the unworthy takes,\n" + + "When he himself might his quietus make\n" + + "With a bare bodkin? who would these fardels bear,\n" + + "To grunt and sweat under a weary life,\n" + + "But that the dread of something after death,--\n" + + "The undiscover'd country, from whose bourn\n" + + "No traveller returns,--puzzles the will,\n" + + "And makes us rather bear those ills we have\n" + + "Than fly to others that we know not of?\n" + + "Thus conscience does make cowards of us all;\n" + + "And thus the native hue of resolution\n" + + "Is sicklied o'er with the pale cast of thought;\n" + + "And enterprises of great pith and moment,\n" + + "With this regard, their currents turn awry,\n" + + "And lose the name of action.--Soft you now!\n" + + "The fair Ophelia!--Nymph, in thy orisons\n" + + "Be all my sins remember'd.\n\000" + +func (a *SunMD5) get_nth_bit(digest []byte, n uint64) uint { + b := (n % 128) / 8 + bit := (n % 128) % 8 + output := digest[b] & (1 << bit) + if output == 0 { + return 0 + } + return 1 +} + +func (s *SunMD5) MuffetCoinToss(digest []byte, iteration uint64) bool { + var x, y, a, b, r, v, i uint = 0, 0, 0, 0, 0, 0, 0 + for ; i < 8; i++ { + a = uint(digest[(i+0)%16]) + b = uint(digest[(i+3)%16]) + r = a >> (b % 5) + v = uint(digest[r%16]) + if (b & (1 << (a % 8))) != 0 { + v /= 2 + } + x |= s.get_nth_bit(digest, uint64(v)) << i + + a = uint(digest[(i+8)%16]) + b = uint(digest[(i+11)%16]) + r = a >> (b % 5) + v = uint(digest[r%16]) + if (b & (1 << (a % 8))) != 0 { + v /= 2 + } + y |= s.get_nth_bit(digest, uint64(v)) << i + } + + if s.get_nth_bit(digest, iteration) == 1 { + x /= 2 + } + if s.get_nth_bit(digest, iteration+64) == 1 { + y /= 2 + } + + output := s.get_nth_bit(digest, uint64(x)) ^ s.get_nth_bit(digest, uint64(y)) + return output != 0 +} + +// Hash a password with salt using MD5 crypt standard. +func (a *SunMD5) Hash(password []byte, salt []byte, additionalIterations uint64) (hash []byte) { + // Salt should be a maximum of 8 characters. + if len(salt) > 8 { + salt = salt[0:8] + } + + customIterations := false + var iterations uint64 = 4096 + if additionalIterations != 0 { + customIterations = true + iterations += additionalIterations + } + + quoteBytes := []byte(hamlet_quotation) + + output := fmt.Sprintf("%s$%s$", a.Magic, salt) + if customIterations { + output = fmt.Sprintf("%s,rounds=%d$%s$", a.Magic, additionalIterations, salt) + } + + // Encode pass, salt, pass hash to feed into the next hash. + h := md5.New() + h.Write(password) + h.Write([]byte(output)) + result := h.Sum(nil) + + // Perform iterations. + var cnt uint64 + for cnt = 0; cnt < iterations; cnt++ { + h.Reset() + h.Write(result) + + if a.MuffetCoinToss(result, cnt) { + h.Write(quoteBytes) + } + + iterationS := strconv.FormatUint(cnt, 10) + h.Write([]byte(iterationS)) + + // Compute hash for next round. + result = h.Sum(nil) + } + + // Create hash with result. + b64 := MD5Base64Encode(result) + hash = []byte(output) + hash = append(hash, '$') + hash = append(hash, b64...) + return +} + +// Override the passwd hash with salt function to hash with Sun MD5. +func (a *SunMD5) HashPasswordWithSalt(password []byte, salt []byte) (hash []byte, err error) { + // Parse iterations from parameter. + var iterations uint64 + if a.Params != "" { + _, err = fmt.Sscanf(a.Params, "rounds=%d", &iterations) + if err != nil { + return + } + } + + // Compute hash. + hash = a.Hash(password, salt, iterations) + return +} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..287b148 --- /dev/null +++ b/utils.go @@ -0,0 +1,287 @@ +package passwd + +import ( + "errors" + "hash" +) + +// The non-standard alphabet for crypt base64 encoding. +const iota64Encoding = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" + +// Base64 to integer encoding table. +var atoi64Partial = [...]byte{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, + 64, 64, 64, 64, 64, 64, 64, + 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, + 64, 64, 64, 64, 64, 64, + 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, +} + +// Append base64 for provided uint. +func Base64Append(dst []byte, v uint, n int) []byte { + // Until we finish the number of rounds specified, + // loop and encode to base64. + for n > 0 { + // Append base64 of current bit. + dst = append(dst, iota64Encoding[v&0x3F]) + // Jump to the next bit for encoding. + v >>= 6 + n -= 1 + } + // Return new byte array. + return dst +} + +// Encode to crypt base64. +func Base64Encode(src []byte) []byte { + size := len(src) + var b64 []byte + var i int + for i = 0; i < size-3; i += 3 { + l := uint(src[i])<<16 | + uint(src[i+1])<<8 | + uint(src[i+2]) + b64 = Base64Append(b64, l, 4) + } + var l uint + if size-i == 2 { + l = uint(src[i])<<16 | + uint(src[i+1])<<8 | + uint(src[0]) + b64 = Base64Append(b64, l, 4) + } + return b64 +} + +// Takes a prior hash, and recycles bytes until the length provided is covered. +func HashBlockRecycle(h hash.Hash, block []byte, len int) { + size := h.BlockSize() + var cnt int + for cnt = len; cnt > size; cnt -= size { + h.Write(block) + } + // Remaining characters of the length, add sub slice here. + h.Write(block[:cnt]) +} + +// Convert base64 byte to integer value. +func AToI64(c byte) (val int) { + if c >= '.' && c <= 'z' { + val = int(atoi64Partial[c-'.']) + } + return +} + +// Convert integer to bae64. +func IToA64(N int) (val byte, err error) { + if N > 64 { + err = errors.New("maximum itoa64 value is 64") + return + } + Nb := byte(N) + for i, b := range atoi64Partial { + if b == Nb { + val = '.' + byte(i) + } + } + return +} + +// Get the power of 2 value. +func N2log2(N uint64) (N_log2 int) { + if N < 2 { + return + } + + // Find power by bit shifting until shifting results in 0. + N_log2 = 2 + for N>>N_log2 != 0 { + N_log2++ + } + N_log2-- + + // If the result of removing one power level ends up resulting in a shift of not 1, return 0. + if N>>N_log2 != 1 { + return 0 + } + + return +} + +// Encode uint32 into base64 at a fixed length. +func Base64Uint32Encode(src, srcbits uint32) (b64 []byte) { + var bits uint32 + + for bits = 0; bits < srcbits; bits += 6 { + b64 = append(b64, iota64Encoding[src&0x3F]) + src >>= 6 + } + + if src != 0 { + return []byte{} + } + return +} + +// Decode uint32 from base64 at a fixed length. +func Base64Uint32Decode(src []byte, dstbits uint32) (dst uint32) { + var bits uint32 + var i uint + for bits = 0; bits < dstbits; bits += 6 { + c := AToI64(src[i]) + i++ + if c > 63 { + return 0 + } + dst |= uint32(c << bits) + } + return +} + +// Encode base64 in the format used for SCrypt hashes. +func SCryptBase64Encode(src []byte) []byte { + dst := make([]byte, 0, (len(src)*8+5)/6) + for i := 0; i < len(src); { + var val uint32 + var bits int32 + for ; bits < 24 && i < len(src); bits += 8 { + val |= uint32(src[i]) << bits + i++ + } + for ; bits > 0; bits -= 6 { + dst = append(dst, iota64Encoding[val&0x3F]) + val >>= 6 + } + } + return dst +} + +// Decode base64 in the format used for SCrypt hashes. +func SCryptBase64Decode(src []byte) []byte { + dst := make([]byte, 0, len(src)*3/4) + for i := 0; i < len(src); { + var val uint32 + var bits int32 + for ; bits < 24 && i < len(src); bits += 6 { + c := AToI64(src[i]) + if c > 63 { + return nil + } + i++ + val |= uint32(c) << bits + } + if bits < 12 { + return nil + } + for ; bits >= 8; bits -= 8 { + dst = append(dst, byte(val)) + val >>= 8 + } + if val != 0 { + return nil + } + } + return dst +} + +// Encode MD5 result to MD5 crypt base64. +func MD5Base64Encode(src []byte) []byte { + // The way the crypt standards work with base64 encoding of MD5 is odd, because the + // last round rotates some of the hash bytes positions. So we must have this custom + // function just to encode MD5 hashes to base64. + var b64 []byte + l := uint(src[0])<<16 | uint(src[6])<<8 | uint(src[12]) + b64 = Base64Append(b64, l, 4) + l = uint(src[1])<<16 | uint(src[7])<<8 | uint(src[13]) + b64 = Base64Append(b64, l, 4) + l = uint(src[2])<<16 | uint(src[8])<<8 | uint(src[14]) + b64 = Base64Append(b64, l, 4) + l = uint(src[3])<<16 | uint(src[9])<<8 | uint(src[15]) + b64 = Base64Append(b64, l, 4) + l = uint(src[4])<<16 | uint(src[10])<<8 | uint(src[5]) + b64 = Base64Append(b64, l, 4) + l = uint(src[11]) + b64 = Base64Append(b64, l, 2) + return b64 +} + +// The crypt standard likes to rotate bits in base64, +// although it doesn't really do anything for brute force protection. +// This performs the rotation algorithm. +func Base64RotateEncode(src []byte, order bool) []byte { + var b64 []byte + l := len(src) + // Setup indexes. + // Used for the loop. + i := 0 + // Index A. + ia := 0 + // Index C, should be byte length divided by 3 to ensure we start a the 3rd point. + ib := l / 3 + // Index C is just B doubled. + ic := ib + ib + // Index D is used to determine which iteration we're on. + id := 0 + // Loop until we reach the last index that fits all 3 values to b64. + for ; i < l-3; i += 3 { + var a, b, c int + // Depending on index D, rotate the A, B, and C indexes. + // I am not sure why we are rotating byte input, it doesn't do anything + // with regards to brute force protection. Someone can just reverse the + // byte order to decode the base64 back down to binary, then use the binary + // for brute force attacks. + if order { + switch id % 3 { + case 0: + a = ia + b = ib + c = ic + case 1: + a = ib + b = ic + c = ia + case 2: + a = ic + b = ia + c = ib + } + } else { + switch id % 3 { + case 0: + a = ia + b = ib + c = ic + case 1: + a = ic + b = ia + c = ib + case 2: + a = ib + b = ic + c = ia + } + } + + // For this round, append the base64. + l := uint(src[a])<<16 | uint(src[b])<<8 | uint(src[c]) + b64 = Base64Append(b64, l, 4) + + // Increment the indexes. + ia++ + ib++ + ic++ + id++ + } + // For the remaining bytes, append as needed. + if l-i == 2 { + l := uint(0)<<16 | uint(src[l-1])<<8 | uint(src[l-2]) + b64 = Base64Append(b64, l, 3) + } else { + l := uint(0)<<16 | uint(0)<<8 | uint(src[l-1]) + b64 = Base64Append(b64, l, 2) + } + // Return the base64. + return b64 +} diff --git a/yes_crypt.go b/yes_crypt.go new file mode 100644 index 0000000..a7047f8 --- /dev/null +++ b/yes_crypt.go @@ -0,0 +1,60 @@ +package passwd + +import ( + "fmt" + + "github.com/openwall/yescrypt-go" +) + +type YesCrypt struct { + Passwd +} + +// Make an MD5Crypt password instance. +func NewYesCryptPasswd() PasswdInterface { + m := new(YesCrypt) + m.Magic = YES_CRYPT_MAGIC + m.SetSCryptParams(11, 31) + m.SaltLength = 22 + // Set the interface to allow parents to call overriden functions. + m.i = m + return m +} + +// Sets the SCrypt params using integers. +func (a *YesCrypt) SetSCryptParams(N, r int) (err error) { + Nval, err := IToA64(N) + if err != nil { + return + } + rval, err := IToA64(r) + if err != nil { + return + } + a.Params = fmt.Sprintf("j%c%c", Nval, rval) + return +} + +// Decode SCrypt params. +func (a *YesCrypt) DecodeSCriptParams() (N, r int) { + b64 := []byte(a.Params) + if len(b64) != 3 { + return + } + N = AToI64(b64[1]) + r = AToI64(b64[2]) + return +} + +// Hash a password with salt using yes crypt standard. +func (a *YesCrypt) Hash(password []byte, salt []byte) (hash []byte, err error) { + output := fmt.Sprintf("%s%s$%s", a.Magic, a.Params, salt) + hash, err = yescrypt.Hash(password, []byte(output)) + return +} + +// Override the passwd hash with salt function to hash with yes crypt. +func (a *YesCrypt) HashPasswordWithSalt(password []byte, salt []byte) (hash []byte, err error) { + hash, err = a.Hash(password, salt) + return +}