htpasswd: refactor and add Passwd methods #3

Merged
amery merged 1 commits from pr-karasz-refactor into master 2023-09-28 20:18:36 +02:00
9 changed files with 451 additions and 357 deletions
+22
View File
@@ -0,0 +1,22 @@
package htpasswd
import "github.com/GehirnInc/crypt/apr1_crypt"
// Apr1 facilitates apr1 style hashing
type Apr1 struct{}
// Hash returns the hashed variant of the password or an error
func (*Apr1) Hash(passwd string) (string, error) {
return apr1_crypt.New().Generate([]byte(passwd), nil)
}
// Match verifier the hashed password using the original
func (*Apr1) Match(password, hashedPassword string) error {
return apr1_crypt.New().Verify(hashedPassword, []byte(password))
}
// Name returns the name of the hasher
func (*Apr1) Name() string { return "apr1" }
// Prefix returns the hasher's prefix
func (*Apr1) Prefix() string { return "$apr1$" }
+26
View File
@@ -0,0 +1,26 @@
package htpasswd
import "golang.org/x/crypto/bcrypt"
// Bcrypt facilitates bcrypt style hashing
type Bcrypt struct{}
// Hash returns the hashed variant of the password or an error
func (*Bcrypt) Hash(password string) (string, error) {
passwordBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(passwordBytes), nil
}
// Match verifier the hashed password using the original
func (*Bcrypt) Match(password, hashedPassword string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}
// Name returns the name of the hasher
func (*Bcrypt) Name() string { return "bcrypt" }
// Prefix returns the hasher's prefix
func (*Bcrypt) Prefix() string { return "$2a$" }
+144 -340
View File
@@ -2,41 +2,22 @@
package htpasswd
import (
"bytes"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"errors"
"fmt"
"os"
"strings"
"github.com/GehirnInc/crypt/apr1_crypt"
"golang.org/x/crypto/bcrypt"
)
// Passwds name => hash
type Passwds map[string]string
// HashAlgorithm enum for hashing algorithms
type HashAlgorithm string
const (
// HashAPR1 Apache MD5 crypt
HashAPR1 HashAlgorithm = "apr1"
// HashBCrypt bcrypt
HashBCrypt HashAlgorithm = "bcrypt"
// HashSHA SHA
HashSHA HashAlgorithm = "sha"
// HashSSHA Salted SHA
HashSSHA HashAlgorithm = "ssha"
// HashSHA256 256 variant of SHA
HashSHA256 HashAlgorithm = "sha256"
// HashSHA512 512 variant of SHA
HashSHA512 HashAlgorithm = "sha512"
)
// Hasher interface implemented by hash algos
type Hasher interface {
Hash(password string) (string, error)
Match(password, hashedPassword string) error
Name() string
Prefix() string
}
// ParseHtpasswdFile parses a .htpasswd file
// and returns a Passwd type
@@ -79,6 +60,143 @@ func ParseHtpasswd(htpasswdBytes []byte) (Passwds, error) {
return passwords, err
}
// CreateUser creates a record in the named file with
// the named password and hash algorithm
func CreateUser(file, user, passwd string, algo Hasher) error {
pp, err := ParseHtpasswdFile(file)
if err != nil {
return err
}
err = pp.CreateUser(user, passwd, algo)
if err != nil {
return err
}
return pp.Write(file)
}
// CreateUser will create a new user in the given Passwd object
// using the given name, password and hashing algorithm
func (pp Passwds) CreateUser(user, passwd string, algo Hasher) error {
if _, exists := pp[user]; exists {
return fmt.Errorf("user %s already exists", user)
}
h, err := algo.Hash(passwd)
if err != nil {
return err
}
pp[user] = h
return nil
}
karasz marked this conversation as resolved Outdated
Outdated
Review

as it's a map, newpp and pp are actually the same map and both contain the new user.

if we really want a new map this should be via a newpp, err := pp.Clone().AddUser(...)

as it's a map, newpp and pp are actually the same map and both contain the new user. if we really want a new map this should be via a `newpp, err := pp.Clone().AddUser(...)`
// UpdateUser will update the password for the named user
// in the named file
func UpdateUser(file, user, passwd string, algo Hasher) error {
pp, err := ParseHtpasswdFile(file)
if err != nil {
return err
}
err = pp.UpdateUser(user, passwd, algo)
if err != nil {
return err
Outdated
Review

we may want errors that can be tested without parsing the content

we may want errors that can be tested without parsing the content
Outdated
Review

Not sure what you mean

Not sure what you mean
Outdated
Review

usage or wrapping predefined errors so one can use errors.Is() and similar

usage or wrapping predefined errors so one can use errors.Is() and similar
}
return pp.Write(file)
}
// UpdateUser will update the password for the named user
// using the given name, password and hashing algorithm
func (pp Passwds) UpdateUser(user, passwd string, algo Hasher) error {
if _, exists := pp[user]; !exists {
return fmt.Errorf("user %s does not exist", user)
}
h, err := algo.Hash(passwd)
if err != nil {
return err
}
pp[user] = h
return nil
}
// DeleteUser deletes the named user from the named file
func DeleteUser(file, user string) error {
pp, err := ParseHtpasswdFile(file)
if err != nil {
return err
}
err = pp.DeleteUser(user)
if err != nil {
return err
}
return pp.Write(file)
}
// DeleteUser deletes the named user from the named file
func (pp Passwds) DeleteUser(user string) error {
if _, exists := pp[user]; !exists {
return fmt.Errorf("user %s does not exist", user)
}
delete(pp, user)
return nil
}
// VerifyUser will check if the given user and password are matching
// with the content of the given file
func VerifyUser(file, user, passwd string) error {
pp, err := ParseHtpasswdFile(file)
if err != nil {
return err
}
return pp.VerifyUser(user, passwd)
}
// VerifyUser will check if the given user and password are matching
// with the given Passwd object
func (pp Passwds) VerifyUser(user, passwd string) error {
if _, ok := pp[user]; !ok {
return fmt.Errorf("user %s does not exist", user)
}
alg, err := identifyHash(pp[user])
if err != nil {
return fmt.Errorf("cannot identify algo %v", alg)
}
return alg.Match(passwd, pp[user])
}
// Write will cwrite the Passwd object to the given file
func (pp Passwds) Write(file string) error {
return os.WriteFile(file, pp.Bytes(), os.ModePerm)
}
// Bytes will return the Passwd as a byte slice
func (pp Passwds) Bytes() []byte {
pass := []byte{}
for name, hash := range pp {
pass = append(pass, []byte(name+":"+hash+"\n")...)
}
return pass
}
func identifyHash(h string) (Hasher, error) {
switch {
case strings.HasPrefix(h, "$2a$"), strings.HasPrefix(h, "$2y$"),
strings.HasPrefix(h, "$2x$"), strings.HasPrefix(h, "$2b$"):
return new(Bcrypt), nil
case strings.HasPrefix(h, "$apr1$"):
return new(Apr1), nil
case strings.HasPrefix(h, "{SHA}"):
return new(Sha), nil
case strings.HasPrefix(h, "{SSHA}"):
return new(Ssha), nil
case strings.HasPrefix(h, "$5$"):
karasz marked this conversation as resolved Outdated
Outdated
Review

standard is Bytes() as the returned value contains many.
WriteTo() is also a nice interface

standard is `Bytes()` as the returned value contains many. `WriteTo()` is also a nice interface
return new(Sha256), nil
case strings.HasPrefix(h, "$6$"):
return new(Sha512), nil
}
return nil, fmt.Errorf("unsupported hash algorithm")
}
func validLine(parts []string, lineNumber int, line string) (bool, error) {
var err error
if len(parts) != 2 {
@@ -96,317 +214,3 @@ func trimParts(parts []string) []string {
}
return parts
}
// CreateUser creates a record in the named file with
// the named password and hash algorithm
func CreateUser(file, user, passwd string, algo HashAlgorithm) error {
pp, err := ParseHtpasswdFile(file)
if err != nil {
return err
}
if _, exists := pp[user]; exists {
return fmt.Errorf("user %s already exists", user)
}
h, err := createHash(passwd, algo)
if err != nil {
return err
}
pp[user] = h
return os.WriteFile(file, pp.toByte(), os.ModePerm)
}
// UpdateUser will update the password for the named user
// in the named file
func UpdateUser(file, user, passwd string, algo HashAlgorithm) error {
pp, err := ParseHtpasswdFile(file)
if err != nil {
return err
}
if _, exists := pp[user]; !exists {
return fmt.Errorf("user %s does not exist", user)
}
h, err := createHash(passwd, algo)
if err != nil {
return err
}
pp[user] = h
return os.WriteFile(file, pp.toByte(), os.ModePerm)
}
// DeleteUser deletes the named user from the named file
func DeleteUser(file, user string) error {
pp, err := ParseHtpasswdFile(file)
if err != nil {
return err
}
if _, exists := pp[user]; !exists {
return fmt.Errorf("user %s does not exist", user)
}
delete(pp, user)
return os.WriteFile(file, pp.toByte(), os.ModePerm)
}
// VerifyUser will check if the given user and password are matching
// with the content of the given file
func VerifyUser(file, user, passwd string) error {
pp, err := ParseHtpasswdFile(file)
if err != nil {
return err
}
return pp.Verify(user, passwd)
}
// Verify will check if the given user and password are matching
// with the given Passwd object content
func (pp Passwds) Verify(user, passwd string) error {
if _, ok := pp[user]; !ok {
return fmt.Errorf("user %s does not exist", user)
}
alg, err := identifyHash(pp[user])
if err != nil {
return fmt.Errorf("cannot identify algo %v", alg)
}
return verifyPass(passwd, pp[user], alg)
}
func verifyPass(pass, hash string, alg HashAlgorithm) error {
switch alg {
case HashAPR1:
return verifyAPR1(pass, hash)
case HashBCrypt:
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(pass))
case HashSHA:
return verifySHA(pass, hash)
case HashSSHA:
return verifySSHA(pass, hash)
case HashSHA256:
return verifySHA256(pass, hash)
case HashSHA512:
return verifySHA512(pass, hash)
}
return fmt.Errorf("unsupported hash algorithm %v", alg)
}
func verifyAPR1(password, hashedPassword string) error {
return apr1_crypt.New().Verify(hashedPassword, []byte(password))
}
func verifySHA(password, hashedPassword string) error {
eppS := hashedPassword[5:]
hash, err := base64.StdEncoding.DecodeString(eppS)
if err != nil {
return fmt.Errorf("cannot base64 decode")
}
sha := sha1.New()
_, err = sha.Write([]byte(password))
if err != nil {
return err
}
sum := sha.Sum(nil)
if !bytes.Equal(sum, hash) {
return fmt.Errorf("wrong password")
}
return nil
}
func verifySSHA(password, hashedPassword string) error {
eppS := hashedPassword[6:]
hash, err := base64.StdEncoding.DecodeString(eppS)
if err != nil {
return fmt.Errorf("cannot base64 decode")
}
salt := hash[len(hash)-4:]
sha := sha1.New()
_, err = sha.Write([]byte(password))
if err != nil {
return err
}
_, err = sha.Write(salt)
if err != nil {
return err
}
sum := sha.Sum(nil)
if !bytes.Equal(sum, hash[:len(hash)-4]) {
return fmt.Errorf("wrong password")
}
return nil
}
func verifySHA256(password, hashedPassword string) error {
eppS := hashedPassword[3:]
hash, err := base64.StdEncoding.DecodeString(eppS)
if err != nil {
return fmt.Errorf("cannot base64 decode")
}
sha := sha256.New()
_, err = sha.Write([]byte(password))
if err != nil {
return err
}
sum := sha.Sum(nil)
if !bytes.Equal(sum, hash) {
return fmt.Errorf("wrong password")
}
return nil
}
func verifySHA512(password, hashedPassword string) error {
eppS := hashedPassword[3:]
hash, err := base64.StdEncoding.DecodeString(eppS)
if err != nil {
return fmt.Errorf("cannot base64 decode")
}
sha := sha512.New()
_, err = sha.Write([]byte(password))
if err != nil {
return err
}
sum := sha.Sum(nil)
if !bytes.Equal(sum, hash) {
return fmt.Errorf("wrong password")
}
return nil
}
func (pp Passwds) toByte() []byte {
pass := []byte{}
for name, hash := range pp {
pass = append(pass, []byte(name+":"+hash+"\n")...)
}
return pass
}
func identifyHash(h string) (HashAlgorithm, error) {
switch {
case strings.HasPrefix(h, "$2a$"), strings.HasPrefix(h, "$2y$"),
strings.HasPrefix(h, "$2x$"), strings.HasPrefix(h, "$2b$"):
return HashBCrypt, nil
case strings.HasPrefix(h, "$apr1$"):
return HashAPR1, nil
case strings.HasPrefix(h, "{SHA}"):
return HashSHA, nil
case strings.HasPrefix(h, "{SSHA}"):
return HashSSHA, nil
case strings.HasPrefix(h, "$5$"):
return HashSHA256, nil
case strings.HasPrefix(h, "$6$"):
return HashSHA512, nil
}
return "", fmt.Errorf("unsupported hash algorithm")
}
func createHash(passwd string, algo HashAlgorithm) (string, error) {
hash := ""
prefix := ""
var err error
switch algo {
case HashAPR1:
hash, err = hashApr1(passwd)
case HashBCrypt:
hash, err = hashBcrypt(passwd)
case HashSHA:
prefix = "{SHA}"
hash, err = hashSha(passwd)
case HashSSHA:
prefix = "{SSHA}"
hash, err = hashSSha(passwd)
case HashSHA256:
prefix = "$5$"
hash, err = hashSha256(passwd)
case HashSHA512:
prefix = "$6$"
hash, err = hashSha512(passwd)
}
if err != nil {
return "", err
}
return prefix + hash, nil
}
func hashApr1(passwd string) (string, error) {
return apr1_crypt.New().Generate([]byte(passwd), nil)
}
func hashBcrypt(passwd string) (string, error) {
passwordBytes, err := bcrypt.GenerateFromPassword([]byte(passwd), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(passwordBytes), nil
}
func hashSha(passwd string) (string, error) {
s := sha1.New()
_, err := s.Write([]byte(passwd))
if err != nil {
return "", err
}
passwordSum := []byte(s.Sum(nil))
return base64.StdEncoding.EncodeToString(passwordSum), nil
}
func hashSha256(passwd string) (string, error) {
s := sha256.New()
_, err := s.Write([]byte(passwd))
if err != nil {
return "", err
}
passwordSum := []byte(s.Sum(nil))
return base64.StdEncoding.EncodeToString(passwordSum), nil
}
func hashSha512(passwd string) (string, error) {
s := sha512.New()
_, err := s.Write([]byte(passwd))
if err != nil {
return "", err
}
passwordSum := []byte(s.Sum(nil))
return base64.StdEncoding.EncodeToString(passwordSum), nil
}
func hashSSha(passwd string) (string, error) {
s := sha1.New()
_, err := s.Write([]byte(passwd))
if err != nil {
return "", err
}
salt := make([]byte, 4)
_, err = rand.Read(salt)
if err != nil {
return "", err
}
_, err = s.Write([]byte(salt))
if err != nil {
return "", err
}
passwordSum := []byte(s.Sum(nil))
ret := append(passwordSum, salt...)
return base64.StdEncoding.EncodeToString(ret), nil
}
5
+28 -13
View File
@@ -5,7 +5,7 @@ import (
"testing"
)
func TestParseHtpassd(t *testing.T) {
func TestParseHtpasswd(t *testing.T) {
passwords, err := ParseHtpasswd([]byte("sha:{SHA}IRRjboXT92QSYXm8lpGPCZUvU1E=\n"))
if err != nil {
t.Fatal(err)
@@ -26,15 +26,15 @@ func TestCreateUser(t *testing.T) {
}
testCases := []struct {
user, password string
hash HashAlgorithm
hash Hasher
expected error
}{
{"apr1", "123456@", HashAPR1, nil},
{"bcrypt", "123456@", HashBCrypt, nil},
{"ssha", "123456@", HashSSHA, nil},
{"sha", "123456@", HashSHA, nil},
{"sha256", "123456@", HashSHA256, nil},
{"sha512", "123456@", HashSHA512, nil},
{"apr1", "123456@", new(Apr1), nil},
{"bcrypt", "123456@", new(Bcrypt), nil},
{"ssha", "123456@", new(Ssha), nil},
{"sha", "123456@", new(Sha), nil},
{"sha256", "123456@", new(Sha256), nil},
{"sha512", "123456@", new(Sha512), nil},
}
for _, tc := range testCases {
@@ -71,13 +71,13 @@ func TestVerifyPassword(t *testing.T) {
for _, tc := range testCases {
err := VerifyUser(f.Name(), tc.user, tc.password)
if err != tc.expected {
t.Errorf("VerifyUser(%s %s, %s) = %v; want %v",
t.Errorf("VerifyUser(%v %v, %v) = %v; want %v",
f, tc.user, tc.password, err, tc.expected)
}
}
}
func TestVerify(t *testing.T) {
func TestVerifyUser(t *testing.T) {
f, err := os.Stat("htpasswd_testdata")
if err != nil {
TestCreateUser(t)
@@ -87,8 +87,23 @@ func TestVerify(t *testing.T) {
if err != nil {
t.Fatal(err)
}
err = pp.Verify("bcrypt", "123456@")
if err != nil {
t.Fatalf(err.Error())
testCases := []struct {
user, password string
expected error
}{
{"apr1", "123456@", nil},
{"bcrypt", "123456@", nil},
{"ssha", "123456@", nil},
{"sha", "123456@", nil},
{"sha256", "123456@", nil},
{"sha512", "123456@", nil},
}
for _, tc := range testCases {
err := pp.VerifyUser(tc.user, tc.password)
if err != tc.expected {
t.Errorf("VerifyUser(%s, %s) = %v; want %v",
tc.user, tc.password, err, tc.expected)
}
}
}
+4 -4
View File
@@ -1,6 +1,6 @@
apr1:$apr1$v0FnbGJM$b2P3y1ltZYHakDaHrWx3N1
bcrypt:$2a$10$pBSUext6NDsFYrm8GviW.OFe6SczH91INRC3YmsfE3HJp/fPmRaee
ssha:{SSHA}LoTRQgCdeGIeJ3nxDQMCmQSWdnMEsLqj
sha512:$6$78RjySv19bx/knbdL6q1cpoV8WblZwc3x+wmPGQUvrSycxc4liKbksvDr9HZj76hgRuZZCyEngP+WEJmePArCQ==
bcrypt:$2a$10$YNEmQcVCrvcA8m1DNCwR9eXaTySDEa1sC/T5xUUTFnVnP.RIP3O2u
ssha:{SSHA}fSLbdty+JHr+q3p/lHAPvNkOU3H8NLmI
sha:{SHA}YEn9/RmoXLdbyB9TEDJ0OqWoPy8=
sha256:$5$ufJ2S16IOhB6XLTGrVLqGQBKv/odjE3rypxnUDLqaS0=
sha512:$6$78RjySv19bx/knbdL6q1cpoV8WblZwc3x+wmPGQUvrSycxc4liKbksvDr9HZj76hgRuZZCyEngP+WEJmePArCQ==
apr1:$apr1$mO.FA9Gg$1LbPaKe7HCVHezEYzMCnn.
+52
View File
@@ -0,0 +1,52 @@
package htpasswd
import (
"bytes"
"crypto/sha1"
"encoding/base64"
"fmt"
)
// Sha facilitates sha1 style hashing
type Sha struct{}
// Hash returns the hashed variant of the password or an error
func (ss *Sha) Hash(passwd string) (string, error) {
s := sha1.New()
_, err := s.Write([]byte(passwd))
if err != nil {
return "", err
}
passwordSum := []byte(s.Sum(nil))
return ss.Prefix() + base64.StdEncoding.EncodeToString(passwordSum), nil
}
// Match verifier the hashed password using the original
func (*Sha) Match(password, hashedPassword string) error {
eppS := hashedPassword[5:]
hash, err := base64.StdEncoding.DecodeString(eppS)
if err != nil {
return fmt.Errorf("cannot base64 decode")
}
sha := sha1.New()
_, err = sha.Write([]byte(password))
if err != nil {
return err
}
sum := sha.Sum(nil)
if !bytes.Equal(sum, hash) {
return fmt.Errorf("wrong password")
}
return nil
}
// Name returns the name of the hasher
func (*Sha) Name() string { return "sha" }
// Prefix returns the hasher's prefix
func (*Sha) Prefix() string { return "{SHA}" }
+52
View File
@@ -0,0 +1,52 @@
package htpasswd
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"fmt"
)
// Sha256 facilitates sha256 style hashing
type Sha256 struct{}
// Hash returns the hashed variant of the password or an error
func (ss *Sha256) Hash(passwd string) (string, error) {
s := sha256.New()
_, err := s.Write([]byte(passwd))
if err != nil {
return "", err
}
passwordSum := []byte(s.Sum(nil))
return ss.Prefix() + base64.StdEncoding.EncodeToString(passwordSum), nil
}
// Match verifier the hashed password using the original
func (*Sha256) Match(password, hashedPassword string) error {
eppS := hashedPassword[3:]
hash, err := base64.StdEncoding.DecodeString(eppS)
if err != nil {
return fmt.Errorf("cannot base64 decode")
}
sha := sha256.New()
_, err = sha.Write([]byte(password))
if err != nil {
return err
}
sum := sha.Sum(nil)
if !bytes.Equal(sum, hash) {
return fmt.Errorf("wrong password")
}
return nil
}
// Name returns the name of the hasher
func (*Sha256) Name() string { return "sha256" }
// Prefix returns the hasher's prefix
func (*Sha256) Prefix() string { return "$5$" }
+52
View File
@@ -0,0 +1,52 @@
package htpasswd
import (
"bytes"
"crypto/sha512"
"encoding/base64"
"fmt"
)
// Sha512 facilitates sha512 style hashing
type Sha512 struct{}
// Hash returns the hashed variant of the password or an error
func (ss *Sha512) Hash(passwd string) (string, error) {
s := sha512.New()
_, err := s.Write([]byte(passwd))
if err != nil {
return "", err
}
passwordSum := []byte(s.Sum(nil))
return ss.Prefix() + base64.StdEncoding.EncodeToString(passwordSum), nil
}
// Match verifier the hashed password using the original
func (*Sha512) Match(password, hashedPassword string) error {
eppS := hashedPassword[3:]
hash, err := base64.StdEncoding.DecodeString(eppS)
if err != nil {
return fmt.Errorf("cannot base64 decode")
}
sha := sha512.New()
_, err = sha.Write([]byte(password))
if err != nil {
return err
}
sum := sha.Sum(nil)
if !bytes.Equal(sum, hash) {
return fmt.Errorf("wrong password")
}
return nil
}
// Name returns the name of the hasher
func (*Sha512) Name() string { return "sha512" }
// Prefix returns the hasher's prefix
func (*Sha512) Prefix() string { return "$6$" }
+71
View File
@@ -0,0 +1,71 @@
package htpasswd
import (
"bytes"
"crypto/rand"
"crypto/sha1"
"encoding/base64"
"fmt"
)
// Ssha facilitates ssha style hashing
type Ssha struct{}
// Hash returns the hashed variant of the password or an error
func (ss *Ssha) Hash(passwd string) (string, error) {
s := sha1.New()
_, err := s.Write([]byte(passwd))
if err != nil {
return "", err
}
salt := make([]byte, 4)
_, err = rand.Read(salt)
if err != nil {
return "", err
}
_, err = s.Write([]byte(salt))
if err != nil {
return "", err
}
passwordSum := []byte(s.Sum(nil))
ret := append(passwordSum, salt...)
return ss.Prefix() + base64.StdEncoding.EncodeToString(ret), nil
}
// Match verifier the hashed password using the original
func (*Ssha) Match(password, hashedPassword string) error {
eppS := hashedPassword[6:]
hash, err := base64.StdEncoding.DecodeString(eppS)
if err != nil {
return fmt.Errorf("cannot base64 decode")
}
salt := hash[len(hash)-4:]
sha := sha1.New()
_, err = sha.Write([]byte(password))
if err != nil {
return err
}
_, err = sha.Write(salt)
if err != nil {
return err
}
sum := sha.Sum(nil)
if !bytes.Equal(sum, hash[:len(hash)-4]) {
return fmt.Errorf("wrong password")
}
return nil
}
// Name returns the name of the hasher
func (*Ssha) Name() string { return "ssha" }
// Prefix returns the hasher's prefix
func (*Ssha) Prefix() string { return "{SSHA}" }