diff --git a/cmdGet.go b/cmdGet.go index 5062366..86e1fd8 100644 --- a/cmdGet.go +++ b/cmdGet.go @@ -1,9 +1,11 @@ package main import ( + "encoding/json" "fmt" "os" + "github.com/Luzifer/password/hasher" "github.com/Luzifer/password/lib" "github.com/spf13/cobra" ) @@ -15,6 +17,7 @@ func getCmdGet() *cobra.Command { Run: actionCmdGet, } + cmd.Flags().BoolVarP(&flags.CLI.JSON, "json", "j", false, "return output in JSON format") cmd.Flags().IntVarP(&flags.CLI.Length, "length", "l", 20, "length of the generated password") cmd.Flags().BoolVarP(&flags.CLI.SpecialCharacters, "special", "s", false, "use special characters in your password") @@ -33,5 +36,16 @@ func actionCmdGet(cmd *cobra.Command, args []string) { os.Exit(1) } - fmt.Println(password) + if !flags.CLI.JSON { + fmt.Println(password) + os.Exit(0) + } + + hashes, err := hasher.GetHashMap(password) + if err != nil { + fmt.Printf("Unable to generate hashes: %s", err) + os.Exit(1) + } + hashes["password"] = password + json.NewEncoder(os.Stdout).Encode(hashes) } diff --git a/flags.go b/flags.go index de130ca..24b971b 100644 --- a/flags.go +++ b/flags.go @@ -4,6 +4,7 @@ var flags = struct { CLI struct { Length int SpecialCharacters bool + JSON bool } Server struct { diff --git a/hasher/hasher.go b/hasher/hasher.go new file mode 100644 index 0000000..cf21b6f --- /dev/null +++ b/hasher/hasher.go @@ -0,0 +1,94 @@ +package hasher + +import ( + "crypto" + "crypto/rand" + _ "crypto/sha1" + _ "crypto/sha256" + _ "crypto/sha512" + + "fmt" + "math/big" + + "golang.org/x/crypto/bcrypt" + + "github.com/tredoe/osutil/user/crypt" + _ "github.com/tredoe/osutil/user/crypt/apr1_crypt" + _ "github.com/tredoe/osutil/user/crypt/sha256_crypt" + _ "github.com/tredoe/osutil/user/crypt/sha512_crypt" +) + +type hasherFunc func(password string) (string, error) + +var ( + implementations = map[string]hasherFunc{ + "htpasswd_apr1": implHTAPR1, + "htpasswd_bcrypt": implBcrypt, + "htpasswd_sha256": implHTSHA256, + "htpasswd_sha512": implHTSHA512, + "sha1": implSHA1, + "sha256": implSHA256, + "sha512": implSHA512, + } + + saltSet = []byte(`abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./`) + saltSize = 12 +) + +func GetHashMap(password string) (map[string]string, error) { + result := map[string]string{} + + for name, hf := range implementations { + h, err := hf(password) + if err != nil { + return nil, err + } + + result[name] = h + } + + return result, nil +} + +func getSalt() ([]byte, error) { + salt := make([]byte, saltSize) + saltSetLength := big.NewInt(int64(len(saltSet))) + + for i := 0; i < saltSize; i++ { + pos, err := rand.Int(rand.Reader, saltSetLength) + if err != nil { + return nil, err + } + salt[i] = saltSet[pos.Int64()] + } + + return salt, nil +} + +func implBcrypt(password string) (string, error) { + bc, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + return string(bc), err +} + +func genericHT(password, prefix string) (string, error) { + salt, err := getSalt() + if err != nil { + return "", err + } + + return crypt.NewFromHash(prefix+string(salt)).Generate([]byte(password), append([]byte(prefix), salt...)) +} + +func implHTAPR1(password string) (string, error) { return genericHT(password, "$apr1$") } +func implHTSHA256(password string) (string, error) { return genericHT(password, "$5$") } +func implHTSHA512(password string) (string, error) { return genericHT(password, "$6$") } + +func generic(password string, h crypto.Hash) (string, error) { + w := h.New() + w.Write([]byte(password)) + return fmt.Sprintf("%x", w.Sum(nil)), nil +} + +func implSHA1(password string) (string, error) { return generic(password, crypto.SHA1) } +func implSHA256(password string) (string, error) { return generic(password, crypto.SHA256) } +func implSHA512(password string) (string, error) { return generic(password, crypto.SHA512) } diff --git a/hasher/hasher_test.go b/hasher/hasher_test.go new file mode 100644 index 0000000..20cc37e --- /dev/null +++ b/hasher/hasher_test.go @@ -0,0 +1,89 @@ +package hasher + +import ( + "strings" + "testing" + + "github.com/Luzifer/go_helpers/str" +) + +func TestAvailableHashs(t *testing.T) { + hashs, err := GetHashMap("testpass") + if err != nil { + t.Fatalf("Hash map generation failed: %s", err) + } + + for _, impl := range []string{ + "htpasswd_apr1", + "htpasswd_bcrypt", + "htpasswd_sha256", + "htpasswd_sha512", + "sha1", + "sha256", + "sha512", + } { + if _, ok := hashs[impl]; !ok { + t.Errorf("Hash implementation %q is missing", impl) + } + } +} + +func TestSalt(t *testing.T) { + knownSalts := []string{} + + for i := 0; i < 100; i++ { + salt, err := getSalt() + if err != nil { + t.Fatalf("Hash generation failed: %s", err) + } + + if len(salt) != saltSize { + t.Errorf("Salt did not have desired size of %d: %d", saltSize, len(salt)) + } + + ssalt := string(salt) + if str.StringInSlice(ssalt, knownSalts) { + t.Fatalf("Received collision of hashes: %q", ssalt) + } + + knownSalts = append(knownSalts, ssalt) + } +} + +func TestHTPasswd(t *testing.T) { + hashs, err := GetHashMap("testpass") + if err != nil { + t.Fatalf("Hash map generation failed: %s", err) + } + + if len(hashs["htpasswd_sha512"]) != 102 || !strings.HasPrefix(hashs["htpasswd_sha512"], "$6$") { + t.Errorf("Invalid htpasswd SHA512 hash: %q", hashs["htpasswd_sha512"]) + } + + if len(hashs["htpasswd_sha256"]) != 59 || !strings.HasPrefix(hashs["htpasswd_sha256"], "$5$") { + t.Errorf("Invalid htpasswd SHA256 hash: %q", hashs["htpasswd_sha256"]) + } + + if len(hashs["htpasswd_apr1"]) != 37 || !strings.HasPrefix(hashs["htpasswd_apr1"], "$apr1$") { + t.Errorf("Invalid htpasswd APR1 hash: %q", hashs["htpasswd_apr1"]) + } +} + +func TestStandardHashs(t *testing.T) { + hashs, err := GetHashMap("testpass") + if err != nil { + t.Fatalf("Hash map generation failed: %s", err) + } + + if hashs["sha1"] != "206c80413b9a96c1312cc346b7d2517b84463edd" { + t.Errorf("Invalid SHA1 hash: %q", hashs["sha1"]) + } + + if hashs["sha256"] != "13d249f2cb4127b40cfa757866850278793f814ded3c587fe5889e889a7a9f6c" { + t.Errorf("Invalid SHA256 hash: %q", hashs["sha256"]) + } + + if hashs["sha512"] != "78ddc8555bb1677ff5af75ba5fc02cb30bb592b0610277ae15055e189b77fe3fda496e5027a3d99ec85d54941adee1cc174b50438fdc21d82d0a79f85b58cf44" { + t.Errorf("Invalid SHA512 hash: %q", hashs["sha512"]) + } +}