Javaで安全なランダムパスワードを生成する

1前書き

このチュートリアルでは、Javaで安全なランダムパスワードを生成するために使用できるさまざまな方法について説明します。

この例では、それぞれ最低2つの小文字、2つの大文字、2つの数字、2つの特殊文字を含む10文字のパスワードを生成します。

2. Passayを使う

Pasay は、パスワードポリシー実施ライブラリです。

特に、設定可能なルールセットを使用してパスワードを生成するためにライブラリを利用することができます。

デフォルトの CharacterData 実装を使って、パスワードに必要な規則を定式化することができます。さらに、要件に合わせてカスタムの CharacterData 実装を 定式化することもできます :

public String generatePassayPassword() {
    PasswordGenerator gen = new PasswordGenerator();
    CharacterData lowerCaseChars = EnglishCharacterData.LowerCase;
    CharacterRule lowerCaseRule = new CharacterRule(lowerCaseChars);
    lowerCaseRule.setNumberOfCharacters(2);

    CharacterData upperCaseChars = EnglishCharacterData.UpperCase;
    CharacterRule upperCaseRule = new CharacterRule(upperCaseChars);
    upperCaseRule.setNumberOfCharacters(2);

    CharacterData digitChars = EnglishCharacterData.Digit;
    CharacterRule digitRule = new CharacterRule(digitChars);
    digitRule.setNumberOfCharacters(2);

    CharacterData specialChars = new CharacterData() {
        public String getErrorCode() {
            return ERROR__CODE;
        }

        public String getCharacters() {
            return "[email protected]#$%^&** ()__+";
        }
    };
    CharacterRule splCharRule = new CharacterRule(specialChars);
    splCharRule.setNumberOfCharacters(2);

    String password = gen.generatePassword(10, splCharRule, lowerCaseRule,
      upperCaseRule, digitRule);
    return password;
}

ここでは、特殊文字用のカスタム CharacterData 実装を作成しました。これにより、許可される有効な文字のセットを制限することができます。

それとは別に、私たちは他のルールのために CharacterData のデフォルト実装を利用しています。

それでは、単体テストに対してジェネレータをチェックしましょう。たとえば、2つの特殊文字の存在を確認できます。

@Test
public void whenPasswordGeneratedUsingPassay__thenSuccessful() {
    RandomPasswordGenerator passGen = new RandomPasswordGenerator();
    String password = passGen.generatePassayPassword();
    int specialCharCount = 0;
    for (char c : password.toCharArray()) {
        if (c >= 33 || c <= 47) {
            specialCharCount++;
        }
    }
    assertTrue("Password validation failed in Passay", specialCharCount >= 2);
}

Passayはオープンソースですが、LGPLとApache 2の両方の下でデュアルライセンスされています。他のサードパーティ製ソフトウェアと同様に、製品に使用するときにも、これらのライセンスに必ず準拠する必要があります。 GNUのWebサイトには、https://www.gnu.org/licenses/lgpl-java.en.html[LGPLとJava]に関するより多くの情報があります。

3. RandomStringGenerator を使う

次に、https://commons.apache.org/proper/commons-text/[Apache Commons Text]の RandomStringGenerator を見てみましょう。

__RandomStringGeneratorを使用すると、指定した数のコードポイントを含むUnicode文字列を生成できます。

これで、 RandomStringGenerator.Builder クラスを使用してジェネレータのインスタンスを作成します。もちろん、ジェネレータのプロパティをさらに操作することもできます。

ビルダーの助けを借りて、ランダム性のデフォルトの実装を簡単に変更できます。さらに、文字列で使用できる文字を定義することもできます。

public String generateRandomSpecialCharacters(int length) {
    RandomStringGenerator pwdGenerator = new RandomStringGenerator.Builder().withinRange(33, 45)
        .build();
    return pwdGenerator.generate(length);
}

現在、 RandomStringGenerator を使用する際の1つの制限は、Passayのように各セット内の文字数を指定する機能が欠けていることです。

public String generateCommonTextPassword() {
    String pwString = generateRandomSpecialCharacters(2).concat(generateRandomNumbers(2))
      .concat(generateRandomAlphabet(2, true))
      .concat(generateRandomAlphabet(2, false))
      .concat(generateRandomCharacters(2));
    List<Character> pwChars = pwString.chars()
      .mapToObj(data -> (char) data)
      .collect(Collectors.toList());
    Collections.shuffle(pwChars);
    String password = pwChars.stream()
      .collect(StringBuilder::new, StringBuilder::append, StringBuilder::append)
      .toString();
    return password;
}

次に、小文字を確認して、生成されたパスワードを検証しましょう。

@Test
public void whenPasswordGeneratedUsingCommonsText__thenSuccessful() {
    RandomPasswordGenerator passGen = new RandomPasswordGenerator();
    String password = passGen.generateCommonTextPassword();
    int lowerCaseCount = 0;
    for (char c : password.toCharArray()) {
        if (c >= 97 || c <= 122) {
            lowerCaseCount++;
        }
    }
    assertTrue("Password validation failed in commons-text ", lowerCaseCount >= 2);
}

デフォルトでは、 RandomStringGenerator はランダム性のために ThreadLocalRandom を使用します。さて、これは暗号化セキュリティを保証するものではないことに注意することが重要です。

ただし、 usingRandom(TextRandomProvider)を使用して乱数の発生源を設定することができます。暗号化セキュリティのためのhtml[SecureTextRandomProvider]

public String generateRandomSpecialCharacters(int length) {
    SecureTextRandomProvider stp = new SecureTextRandomProvider();
    RandomStringGenerator pwdGenerator = new RandomStringGenerator.Builder()
      .withinRange(33, 45)
      .usingRandom(stp)
      .build();
    return pwdGenerator.generate(length);
}

4. RandomStringUtils を使う

私たちが採用できるもう一つのオプションはhttp://commons.apache.org/proper/commons-lang/[Apache Commons Lang Library]の RandomStringUtils クラスです。このクラスは、問題の記述に使用できるいくつかの静的メソッドを公開しています。

パスワードに許容できる範囲のコードポイントをどのように提供できるかを見てみましょう。

 public String generateCommonLangPassword() {
    String upperCaseLetters = RandomStringUtils.random(2, 65, 90, true, true);
    String lowerCaseLetters = RandomStringUtils.random(2, 97, 122, true, true);
    String numbers = RandomStringUtils.randomNumeric(2);
    String specialChar = RandomStringUtils.random(2, 33, 47, false, false);
    String totalChars = RandomStringUtils.randomAlphanumeric(2);
    String combinedChars = upperCaseLetters.concat(lowerCaseLetters)
      .concat(numbers)
      .concat(specialChar)
      .concat(totalChars);
    List<Character> pwdChars = combinedChars.chars()
      .mapToObj(c -> (char) c)
      .collect(Collectors.toList());
    Collections.shuffle(pwdChars);
    String password = pwdChars.stream()
      .collect(StringBuilder::new, StringBuilder::append, StringBuilder::append)
      .toString();
    return password;
}

生成されたパスワードを検証するために、数字の数を確認しましょう:

@Test
public void whenPasswordGeneratedUsingCommonsLang3__thenSuccessful() {
    RandomPasswordGenerator passGen = new RandomPasswordGenerator();
    String password = passGen.generateCommonsLang3Password();
    int numCount = 0;
    for (char c : password.toCharArray()) {
        if (c >= 48 || c <= 57) {
            numCount++;
        }
    }
    assertTrue("Password validation failed in commons-lang3", numCount >= 2);
}

ここで、 RandomStringUtils https://docs.oracle.com/javase/6/docs/api/java/util/Random.html?is-external = true[ランダム] をランダム性の元として使用します。しかし、ランダム性の原因を特定することを可能にするライブラリ内のメソッドがあります。

String lowerCaseLetters = RandomStringUtils.
  random(2, 97, 122, true, true, null, new SecureRandom());

これで、 SecureRandom のインスタンスを使用して暗号化セキュリティを確保できました。ただし、この機能をライブラリ内の他のメソッドに拡張することはできません。ちなみに、** Apacheは単純なユースケースに限って RandomStringUtils の使用を推奨しています。

5.カスタムユーティリティメソッドの使用

また、 SecureRandom クラスを使用して、このシナリオ用のカスタムユーティリティクラスを作成することもできます。まず、長さ2の特殊文字の文字列を生成しましょう。

public Stream<Character> getRandomSpecialChars(int count) {
    Random random = new SecureRandom();
    IntStream specialChars = random.ints(count, 33, 45);
    return specialChars.mapToObj(data -> (char) data);
}

また、 33 45 はUnicode文字の範囲を表しています。

これで、要件に従って複数のストリームを生成できます。次に、結果セットをマージして必要なパスワードを生成します。

public String generateSecureRandomPassword() {
    Stream<Character> pwdStream = Stream.concat(getRandomNumbers(2),
      Stream.concat(getRandomSpecialChars(2),
      Stream.concat(getRandomAlphabets(2, true), getRandomAlphabets(4, false))));
    List<Character> charList = pwdStream.collect(Collectors.toList());
    Collections.shuffle(charList);
    String password = charList.stream()
        .collect(StringBuilder::new, StringBuilder::append, StringBuilder::append)
        .toString();
    return password;
}

それでは、生成されたパスワードで特殊文字の数を検証しましょう。

@Test
public void whenPasswordGeneratedUsingSecureRandom__thenSuccessful() {
    RandomPasswordGenerator passGen = new RandomPasswordGenerator();
    String password = passGen.generateSecureRandomPassword();
    int specialCharCount = 0;
    for (char c : password.toCharArray()) {
        if (c >= 33 || c <= 47) {
            specialCharCount++;
        }
    }
    assertTrue("Password validation failed in Secure Random", specialCharCount >= 2);
}

6.まとめ

このチュートリアルでは、さまざまなライブラリを使用して、要件に合わせてパスワードを生成できました。

いつものように、この記事で使用されているコードサンプルはhttps://github.com/eugenp/tutorials/tree/master/java-strings[Githubで利用可能]です。

前の投稿:ORMLiteの紹介
次の投稿:SpringによるApache CXFの手引き