Skip to content

Commit

Permalink
Merge pull request #29 from kloeckner-i/METAL-1957-switch-to-password…
Browse files Browse the repository at this point in the history
…-generator-library

[METAL-1957] Switch To Password Generator Library
  • Loading branch information
dpeckett authored Jun 26, 2020
2 parents 97cf355 + 390dbab commit 9a50459
Show file tree
Hide file tree
Showing 4 changed files with 38 additions and 167 deletions.
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/go-openapi/spec v0.19.2
github.com/go-sql-driver/mysql v1.4.1
github.com/google/go-cmp v0.3.1 // indirect
github.com/kloeckner-i/can-haz-password v0.1.0
github.com/lib/pq v1.2.0
github.com/mitchellh/hashstructure v1.0.0
github.com/operator-framework/operator-sdk v0.13.0
Expand All @@ -16,7 +17,7 @@ require (
github.com/sethvargo/go-password v0.1.3
github.com/sirupsen/logrus v1.4.2
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.4.0
github.com/stretchr/testify v1.6.1
golang.org/x/lint v0.0.0-20200302205851-738671d3881b // indirect
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
golang.org/x/tools v0.0.0-20200526224456-8b020aee10d2 // indirect
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,8 @@ github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHqu
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsouza/fake-gcs-server v1.7.0/go.mod h1:5XIRs4YvwNbNoz+1JF8j6KLAyDh7RHGAyAK3EP2EsNk=
github.com/geozelot/intree v1.0.0 h1:xUyiXMt0wD9zbPMOjy2rVShiUc3PGMPddPuTmi+Jy2s=
github.com/geozelot/intree v1.0.0/go.mod h1:JrqfsNwe17AgzOM023tCXPyUB89NhaZAb8o5rzfZQ7Q=
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v0.0.0-20180820084758-c7ce16629ff4/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
Expand Down Expand Up @@ -375,6 +377,8 @@ github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvW
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/kloeckner-i/can-haz-password v0.1.0 h1:jo3akXxuz10V8yX/wnuVmOLxHgT5YAzkoDgOjam4Urw=
github.com/kloeckner-i/can-haz-password v0.1.0/go.mod h1:bA4XBvR0QmlreJyGVMCR5tqDUQOAoP1NOEPtwn2RZBg=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
Expand Down Expand Up @@ -579,6 +583,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/syndtr/gocapability v0.0.0-20160928074757-e7cb7fa329f4/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/technosophos/moniker v0.0.0-20180509230615-a5dbd03a2245/go.mod h1:O1c8HleITsZqzNZDjSNzirUGsMT0oGu9LhHKoJrqO+A=
github.com/thecodeteam/goscaleio v0.1.0/go.mod h1:68sdkZAsK8bvEwBlbQnlLS+xU+hvLYM/iQ8KXej1AwM=
Expand Down Expand Up @@ -839,6 +845,8 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY=
Expand Down
133 changes: 23 additions & 110 deletions pkg/utils/kci/password.go
Original file line number Diff line number Diff line change
@@ -1,134 +1,47 @@
package kci

import (
"crypto/rand"
"errors"
"fmt"
"math/big"
"regexp"

"github.com/kloeckner-i/can-haz-password/password"
"github.com/sirupsen/logrus"
)

const (
mininumLength = 8
digits = "0123456789"
specials = "-_" // include only uri safe special charactors
uppers = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
lowers = "abcdefghijklmnopqrstuvwxyz"
)

// GeneratePass generates secure password string
func GeneratePass() string {
password, err := generatePass(10, 8, 2, true)
generator := password.NewGenerator(newDbPasswordRule())
password, err := generator.Generate()
if err != nil {
logrus.Fatalf("can not generate password - %s", err)
}
return password
}

func generatePass(numLetters, numDigits, numSpecials int, mixCase bool) (string, error) {
length := numLetters + numDigits + numSpecials

if length < mininumLength {
return "", fmt.Errorf("total length of the password should be at least bigger than %d", mininumLength)
}

if mixCase && (numLetters < 2) {
return "", errors.New("can not mix case when the length of letters is smaller than 2")
}

var bufAll []rune

if numDigits != 0 {
bufDigits, err := selectRandomCharacters(digits, numDigits)
if err != nil {
return "", fmt.Errorf("failed to select digits for password: %v", err)
}
bufAll = append(bufAll, bufDigits...)
}

if numSpecials != 0 {
bufSpecials, err := selectRandomCharacters(specials, numSpecials)
if err != nil {
return "", fmt.Errorf("failed to select special characters for password: %v", err)
}
bufAll = append(bufAll, bufSpecials...)
}

minNumUppers := int64(0)
maxNumUppers := int64(numLetters)
if mixCase {
minNumUppers = 1
maxNumUppers = maxNumUppers - 1
}

numUppers, err := secureRandomIntWithinRange(minNumUppers, maxNumUppers)
if err != nil {
return "", err
}

if numUppers != 0 {
bufUppers, err := selectRandomCharacters(uppers, int(numUppers))
if err != nil {
return "", fmt.Errorf("failed to select uppercase letters for password: %v", err)
}
bufAll = append(bufAll, bufUppers...)
}

numLowers := numLetters - int(numUppers)
if numLowers != 0 {
bufLowers, err := selectRandomCharacters(lowers, numLowers)
if err != nil {
return "", fmt.Errorf("failed to select lowercase letters for password: %v", err)
}
bufAll = append(bufAll, bufLowers...)
}

password, err := secureShuffle(string(bufAll))
if err != nil {
return "", fmt.Errorf("failed to shuffle password: %v", err)
}

return password, nil
// Minimum length of 20 characters, maximum length of 30 characters.
// Varied composition including special characters and uppercase and lowercase letters.
// Excludes consecutive dashes (for hybris compatibility) and uses only url safe special characters.
type dbPasswordRule struct {
invalid *regexp.Regexp
}

// Select n random characters from the source string.
func selectRandomCharacters(src string, n int) ([]rune, error) {
c := []rune(src)
selection := make([]rune, n)

for i := 0; i < n; i++ {
r, err := secureRandomIntWithinRange(0, int64(len(c)))
if err != nil {
return nil, err
}
selection[i] = c[r]
func newDbPasswordRule() *dbPasswordRule {
return &dbPasswordRule{
// Hybris does not support consecutive dashes.
invalid: regexp.MustCompile(`[-]{2,}`),
}

return selection, nil
}

// Implementation of the Fisher–Yates shuffle using crypto/rand.
func secureShuffle(src string) (string, error) {
c := []rune(src)
n := int64(len(c))

for i := int64(0); i < n; i++ {
r, err := secureRandomIntWithinRange(i, n)
if err != nil {
return "", err
}
c[r], c[i] = c[i], c[r]
func (r *dbPasswordRule) Config() *password.Configuration {
return &password.Configuration{
Length: 20,
CharacterClasses: []password.CharacterClassConfiguration{
{Characters: password.LowercaseCharacters + password.UppercaseCharacters, Minimum: 10},
{Characters: password.DigitCharacters, Minimum: 8},
{Characters: password.URLSafeSpecialCharacters, Minimum: 2},
},
}

return string(c), nil
}

// Generate a secure random integer within the range min <= x < max.
func secureRandomIntWithinRange(min int64, max int64) (int64, error) {
next, err := rand.Int(rand.Reader, new(big.Int).SetInt64(max-min))
if err != nil {
return -1, err
}
return min + next.Int64(), nil
func (r *dbPasswordRule) Valid(password []rune) bool {
return !r.invalid.MatchString(string(password))
}
61 changes: 5 additions & 56 deletions pkg/utils/kci/password_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"regexp"
"testing"

"github.com/kloeckner-i/can-haz-password/password"
"github.com/stretchr/testify/assert"
)

Expand All @@ -12,65 +13,13 @@ func TestGeneratePass(t *testing.T) {
generatedPassword := GeneratePass()

if assert.NotEmpty(t, generatedPassword) {
assert.Equal(t, 20, len(generatedPassword))
assert.Equal(t, 10, countOccurrences(generatedPassword, uppers+lowers))
assert.Equal(t, 8, countOccurrences(generatedPassword, digits))
assert.Equal(t, 2, countOccurrences(generatedPassword, specials))
assert.True(t, len(generatedPassword) >= 20)
assert.True(t, countOccurrences(generatedPassword, password.UppercaseCharacters+password.LowercaseCharacters) >= 10)
assert.True(t, countOccurrences(generatedPassword, password.DigitCharacters) >= 8)
assert.True(t, countOccurrences(generatedPassword, password.URLSafeSpecialCharacters) >= 2)
}
}

func TestSelectRandomCharacters(t *testing.T) {
src := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

freq := make(map[rune]int)
for i := 0; i < 1000; i++ {
res, err := selectRandomCharacters(src, 5)
if err != nil {
t.Error(err)
}

for _, c := range res {
freq[c]++
}
}

for rune, n := range freq {
if n < 150 || n >= 250 {
t.Errorf("Unexpected outlier: rune = %d, count = %d", rune, n)
}
}
}

func TestSecureShuffle(t *testing.T) {
src := "The 0,u|ck brow/|/ f°? _|u^^ps °\\/er one3 lazj 1)°g$."

s1, err := secureShuffle(src)
if err != nil {
t.Error(err)
}

s2, err := secureShuffle(src)
if err != nil {
t.Error(err)
}

assert.Len(t, s1, len(src))
assert.Len(t, s2, len(src))
assert.Equal(t, runeFrequency(src), runeFrequency(s1))
assert.Equal(t, runeFrequency(src), runeFrequency(s2))
assert.NotEqual(t, src, s1)
assert.NotEqual(t, s1, s2)
}

// The number of occurances per rune in the source string.
func runeFrequency(src string) map[rune]int {
freq := make(map[rune]int)
for _, c := range src {
freq[c]++
}
return freq
}

// The number of occurrences of a rune/s in a string.
func countOccurrences(src string, runes string) int {
re := regexp.MustCompile("[" + runes + "]")
Expand Down

0 comments on commit 9a50459

Please sign in to comment.