Skip to content

Commit

Permalink
Merge pull request #3989 from terraform-providers/jbardin/password
Browse files Browse the repository at this point in the history
update iam password generation
  • Loading branch information
jbardin committed Mar 30, 2018
2 parents 0095b64 + e804aed commit efa8cd4
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 29 deletions.
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++ {
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

0 comments on commit efa8cd4

Please # to comment.