Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update iam password generation #3989

Merged
merged 4 commits into from
Mar 30, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 57 additions & 28 deletions aws/resource_aws_iam_user_login_profile.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package aws

import (
"bytes"
"crypto/rand"
"fmt"
"log"
"math/rand"
"time"
"math/big"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
Expand Down Expand Up @@ -40,7 +41,7 @@ func resourceAwsIamUserLoginProfile() *schema.Resource {
Type: schema.TypeInt,
Optional: true,
Default: 20,
ValidateFunc: validation.IntBetween(4, 128),
ValidateFunc: validation.IntBetween(5, 128),
},

"key_fingerprint": {
Expand All @@ -55,35 +56,62 @@ func resourceAwsIamUserLoginProfile() *schema.Resource {
}
}

// generatePassword generates a random password of a given length using
// characters that are likely to satisfy any possible AWS password policy
// (given sufficient length).
func generatePassword(length int) string {
charsets := []string{
"abcdefghijklmnopqrstuvwxyz",
"ABCDEFGHIJKLMNOPQRSTUVWXYZ",
"012346789",
"!@#$%^&*()_+-=[]{}|'",
}
const (
charLower = "abcdefghijklmnopqrstuvwxyz"
charUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
charNumbers = "0123456789"
charSymbols = "!@#$%^&*()_+-=[]{}|'"
)

// Use all character sets
random := rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
components := make(map[int]byte, length)
for i := 0; i < length; i++ {
charset := charsets[i%len(charsets)]
components[i] = charset[random.Intn(len(charset))]
}
// generateIAMPassword generates a random password of a given length, matching the
// most restrictive iam password policy.
func generateIAMPassword(length int) string {
const charset = charLower + charUpper + charNumbers + charSymbols

// Randomise the ordering so we don't end up with a predictable
// lower case, upper case, numeric, symbol pattern
result := make([]byte, length)
i := 0
for _, b := range components {
result[i] = b
i = i + 1
charsetSize := big.NewInt(int64(len(charset)))

// rather than trying to artifically add specific characters from each
// class to the password to match the policy, we generate passwords
// randomly and reject those that don't match.
//
// Even in the worst case, this tends to take less than 10 tries to find a
// matching password. Any sufficiently long password is likely to succeed
// on the first try
for n := 0; n < 100000; n++ {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather rename the function from the previous commit to generateOnePassword (or whatever is appropriate), add a datatype describing password policies, and make generatePassword take a policy argument.

for i := range result {
r, err := rand.Int(rand.Reader, charsetSize)
if err != nil {
panic(err)
}
if !r.IsInt64() {
panic("rand.Int() not representable as an Int64")
}

result[i] = charset[r.Int64()]
}

if !checkIAMPwdPolicy(result) {
continue
}

return string(result)
}

panic("failed to generate acceptable password")
}

// Check the generated password contains all character classes listed in the
// IAM password policy.
func checkIAMPwdPolicy(pass []byte) bool {
if !(bytes.ContainsAny(pass, charLower) &&
bytes.ContainsAny(pass, charNumbers) &&
bytes.ContainsAny(pass, charSymbols) &&
bytes.ContainsAny(pass, charUpper)) {
return false
}

return string(result)
return true
}

func resourceAwsIamUserLoginProfileCreate(d *schema.ResourceData, meta interface{}) error {
Expand Down Expand Up @@ -113,7 +141,8 @@ func resourceAwsIamUserLoginProfileCreate(d *schema.ResourceData, meta interface
}
}

initialPassword := generatePassword(passwordLength)
initialPassword := generateIAMPassword(passwordLength)

fingerprint, encrypted, err := encryption.EncryptValue(encryptionKey, initialPassword, "Password")
if err != nil {
return err
Expand Down
38 changes: 37 additions & 1 deletion aws/resource_aws_iam_user_login_profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,42 @@ import (
"github.com/hashicorp/vault/helper/pgpkeys"
)

func TestGenerateIAMPassword(t *testing.T) {
p := generateIAMPassword(6)
if len(p) != 6 {
t.Fatalf("expected a 6 character password, got: %q", p)
}

p = generateIAMPassword(128)
if len(p) != 128 {
t.Fatalf("expected a 128 character password, got: %q", p)
}
}

func TestIAMPasswordPolicyCheck(t *testing.T) {
for _, tc := range []struct {
pass string
valid bool
}{
// no symbol
{pass: "abCD12", valid: false},
// no number
{pass: "abCD%$", valid: false},
// no upper
{pass: "abcd1#", valid: false},
// no lower
{pass: "ABCD1#", valid: false},
{pass: "abCD11#$", valid: true},
} {
t.Run(tc.pass, func(t *testing.T) {
valid := checkIAMPwdPolicy([]byte(tc.pass))
if valid != tc.valid {
t.Fatalf("expected %q to be valid==%t, got %t", tc.pass, tc.valid, valid)
}
})
}
}

func TestAccAWSUserLoginProfile_basic(t *testing.T) {
var conf iam.GetLoginProfileOutput

Expand Down Expand Up @@ -189,7 +225,7 @@ func testDecryptPasswordAndTest(nProfile, nAccessKey, key string) resource.TestC
iamAsCreatedUser := iam.New(iamAsCreatedUserSession)
_, err = iamAsCreatedUser.ChangePassword(&iam.ChangePasswordInput{
OldPassword: aws.String(decryptedPassword.String()),
NewPassword: aws.String(generatePassword(20)),
NewPassword: aws.String(generateIAMPassword(20)),
})
if err != nil {
if awserr, ok := err.(awserr.Error); ok && awserr.Code() == "InvalidClientTokenId" {
Expand Down