diff --git a/internal/blueprint/customizations.go b/internal/blueprint/customizations.go index f5f58eab7..7497c202d 100644 --- a/internal/blueprint/customizations.go +++ b/internal/blueprint/customizations.go @@ -1,6 +1,7 @@ package blueprint import ( + "github.com/osbuild/osbuild-composer/internal/crypt" "github.com/osbuild/osbuild-composer/internal/pipeline" "strconv" "strings" @@ -161,8 +162,16 @@ func (c *Customizations) customizeUserAndSSHKey(p *pipeline.Pipeline) error { users := make(map[string]pipeline.UsersStageOptionsUser) for _, user := range c.User { - // TODO: only hashed password are currently supported as an input - // plain-text passwords should be also supported due to parity with lorax-composer + + if user.Password != nil && !crypt.PasswordIsCrypted(*user.Password) { + cryptedPassword, err := crypt.CryptSHA512(*user.Password) + if err != nil { + return err + } + + user.Password = &cryptedPassword + } + userData := pipeline.UsersStageOptionsUser{ Groups: user.Groups, Description: user.Description, diff --git a/internal/crypt/crypt.go b/internal/crypt/crypt.go new file mode 100644 index 000000000..48adce4a7 --- /dev/null +++ b/internal/crypt/crypt.go @@ -0,0 +1,49 @@ +package crypt + +import ( + "crypto/rand" + "math/big" + "strings" +) + +func CryptSHA512(phrase string) (string, error) { + const SHA512SaltLength = 16 + + salt, err := genSalt(SHA512SaltLength) + + if err != nil { + return "", nil + } + + hashSettings := "$6$" + salt + return crypt(phrase, hashSettings) +} + +func genSalt(length int) (string, error) { + saltChars := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./" + + b := make([]byte, length) + + for i := range b { + runeIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(saltChars)))) + if err != nil { + return "", err + } + b[i] = saltChars[runeIndex.Int64()] + } + + return string(b), nil +} + +func PasswordIsCrypted(s string) bool { + // taken from lorax src: src/pylorax/api/compose.py:533 + prefixes := [...]string{"$2b$", "$6$", "$5$"} + + for _, prefix := range prefixes { + if strings.HasPrefix(s, prefix) { + return true + } + } + + return false +} diff --git a/internal/crypt/crypt_test.go b/internal/crypt/crypt_test.go new file mode 100644 index 000000000..4a29404b8 --- /dev/null +++ b/internal/crypt/crypt_test.go @@ -0,0 +1,43 @@ +package crypt + +import ( + "testing" +) + +func Test_crypt_PasswordIsCrypted(t *testing.T) { + + tests := []struct { + name string + password string + want bool + }{ + { + name: "bcrypt", + password: "$2b$04$123465789012345678901uac5A8egfBuZVHMrDZsQzR96IqNBivCy", + want: true, + }, { + name: "sha256", + password: "$5$1234567890123456$v.2bOKKLlpmUSKn0rxJmgnh.e3wOKivAVNZmNrOsoA3", + want: true, + }, { + name: "sha512", + password: "$6$1234567890123456$d.pgKQFaiD8bRiExg5NesbGR/3u51YvxeYaQXPzx4C6oSYREw8VoReiuYZjx0V9OhGVTZFqhc6emAxT1RC5BV.", + want: true, + }, { + name: "scrypt", + password: "$7$123456789012345", //not actual hash output from scrypt + want: false, + }, { + name: "plain", + password: "password", + want: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := PasswordIsCrypted(test.password); got != test.want { + t.Errorf("PasswordIsCrypted() =%v, want %v", got, test.want) + } + }) + } +}