🐧

[JavaScript]AWSのデフォルトのパスワードポリシーから正規表現を作成する

2021/04/25に公開

前提:正規表現とは

正規表現とは、文字列内で文字の組み合わせを照合するために用いられるパターンです。JavaScript では、正規表現はオブジェクトでもあります。これらのパターンは RegExp の exec および test メソッドや、String の match、 matchAll、replace、search、および split メソッドで使用できます。

上記はMDN[1]の引用です

前提:AWSのデフォルトのパスワードポリシー

2021/04/24現在、AWSのデフォルトのパスワードポリシー[2]は以下のように記載されています。

条件1: パスワードの文字数制限: 8~128 文字
条件2: 大文字、小文字、数字、! @ # $ % ^ & * ( ) _ + - = [ ] { } | ' 記号のうち、最低 3つの文字タイプの組み合わせ
条件3: AWSアカウント名またはEメールアドレスと同じでないこと

条件3は、if文で完全一致するかどうかを判別すればいいので、条件1と2を正規表現でチェックします。

実装その1:条件1と2を肯定的先読みを用いて1行で表現する

以下、完成形のソースコードです。

const symbol = "!@#$%^&*()_+\\-=\\]\\[\\{\\}\\|'";
const regex = new RegExp(
 `^((?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])|(?=.*[a-z])(?=.*[A-Z])(?=.*[${symbol}])|(?=.*[A-Z])(?=.*[0-9])(?=.*[${symbol}])|(?=.*[a-z])(?=.*[0-9])(?=.*[${symbol}]))[a-zA-Z0-9${symbol}]{8,128}$`);

// 条件1と条件2を満たしていない
console.log(regex.test('tT'));
> false
// 条件1を満たしていない
console.log(regex.test('tT@'));
> false
// 条件2を満たしていない
console.log(regex.test('testTEST'));
> false
// 条件1と条件2を満たしている
console.log(regex.test('testTEST@'));
> true

正規表現の部分を噛み砕いていきます。まず、改行して整理すると以下のようになります。

const regex = new RegExp(
 `^
 ((?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])            // ①
 |(?=.*[a-z])(?=.*[A-Z])(?=.*[${symbol}])      // ②
 |(?=.*[A-Z])(?=.*[0-9])(?=.*[${symbol}])      // ③
 |(?=.*[a-z])(?=.*[0-9])(?=.*[${symbol}]))     // ④
 [a-zA-Z0-9${symbol}]                          // ⑤
 {8,128}
 $`
 )

外側から順番に見ていきます。^・・・{8,128}$は8文字以上128文字以内であればマッチします。

文字 意味
^ 入力の先頭にマッチ /^A/はAMのAにマッチするがamのaにはマッチしない
$ 入力の末尾にマッチ /M$/はAMのMにマッチするがamのmにはマッチしない
{n,m} 直前の文字がn回以上m回以内の範囲で出現するものにマッチ /a{2,4}/はaaaやaaaaaaaにマッチするがabababにはマッチしない

次に①〜④は一旦飛ばして⑤を見ていきます。^[a-zA-Z0-9${symbol}]{8,128}$は大文字、小文字、数字、指定された記号の、8文字以上128文字以内の文字列にマッチします。ただ、このままではaaaaaaaaといった1種類の文字だけであってもマッチしてしまうため、条件2の「大文字、小文字、数字、指定された記号の最低3つの文字タイプの組み合わせ」(以降パターンAと定義します)という条件が満たされていません。

文字 意味
[abc] 角括弧で囲まれた文字のいずれか1個にマッチする文字集合 /^[a-z.]+$/はpasswordにはマッチするがpassw0rdにはマッチしない

①〜④はパターンAを表現しています。つまり、^(①〜④)[a-zA-Z0-9${symbol}]{8,128}$は、最初の(①〜④)の部分でパターンAにマッチするかをみて、満たしていた場合は、その後続く文字が0文字以上の指定した文字かどうかをみて([a-zA-Z0-9${symbol}]の部分)、文字長が8文字以上128文字以内のパターンを表現しています。

①〜④でやっていることを言語化すると以下の通りです。

①対象の文字列が小文字、大文字、数字にのみ一致する
②対象の文字列が小文字、大文字、指定した記号にのみ一致する
③対象の文字列が大文字、数字、指定した記号にのみ一致する
④対象の文字列が小文字、数字、指定した記号にのみ一致する

それぞれを | でつなぐことによって、「①または②または③または④にマッチするかどうか」というパターンができます。では、①を例に、具体的に見ていきます。(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[文字])というパターンが3回繰り返されたパターンで、(?=.*[a-z])かつ(?=.*[A-Z])かつ(?=.*[0-9])を満たすかどうかをチェックします。
?=は肯定的先読み(lookahead)という表現で、現在いる位置に続く文字が.*[文字]にマッチする場合のみマッチすることができます。つまり、(?=.*[a-z])というパターンはabcABCという文字列に対し、「a」「b」「c」の前の空文字にマッチします。よって①は、文字列の任意の位置で、後に続く文字が小文字、大文字、数字の全てが揃っているかどうかをチェックできます。

文字 意味
`a b` aまたはbにマッチ
. 改行文字以外のどの1文字にもマッチ /.n/は'an'や'on'にマッチ
* 直前の文字の0回以上の繰り返しにマッチ /abc*/は'ab'や'abcc'にマッチするが'ad'にはマッチしない
a(?=b) aにbが続く場合のみaにマッチ /a(?=b)/は'abc'にマッチするが'ad'にはマッチしない

以上で実装その1の正規表現の説明を終わります。1行で表現できるため、スタイリッシュなものの、可読性はあまり高くない印象は受けました(自分が未熟なだけかもしれません、、)

実装その2:条件1と2を別の正規表現を定義してチェックする

こちらの表現は実装その1と比べるとシンプルな表現となっています。
下の図のように、大文字、小文字、数字、指定した記号のいずれかにマッチする文字列の組み合わせ(パターン1)から、大文字、小文字、数字、指定した記号の1つ、もしくは2つの文字タイプの組み合わせ(パターン2)を除くと、全ての条件のみを満たした文字列のみがマッチします。

パターン1

大文字、小文字、数字、指定した記号のいずれかにマッチする正規表現は以下のように記述します。

const symbol = "!@#$%^&*()_+\\-=\\]\\[\\{\\}\\|'";
const pass_pattern = new RegExp(`^[a-zA-Z0-9${symbol}]{8,128}$`);

パターン2

大文字、小文字、数字、指定した記号の1つ、もしくは2つの文字タイプの組み合わせの正規表現は以下のように記述します。

const symbol = "!@#$%^&*()_+\\-=\\]\\[\\{\\}\\|'";
const nopass_pattern = new RegExp(`^(([a-zA-Z]+)|([a-z0-9]+)|([a-z${symbol}]+)|([A-Z0-9]+)|([A-Z${symbol}]+)|([0-9${symbol}]+))$`);
文字 意味
+ 直前の文字の 1 回以上の繰り返しにマッチ /a+/は'apple'や'aaaapple'の'a'にマッチ

まとめ

const symbol = "!@#$%^&*()_+\\-=\\]\\[\\{\\}\\|'";
const pass_pattern = new RegExp(`^[a-zA-Z0-9${symbol}]{8,128}$`);
const nopass_pattern = new RegExp(`^(([a-zA-Z]+)|([a-z0-9]+)|([a-z${symbol}]+)|([A-Z0-9]+)|([A-Z${symbol}]+)|([0-9${symbol}]+))$`);

// 返り値がtrueなら引数の文字列に条件がマッチ
const test = (str: string) => {
  return pass_pattern.test(str) && !nopass_pattern.test(str);
};

// 条件1と条件2を満たしていない
console.log(test('tT'));
> false
// 条件1を満たしていない
console.log(test('tT@'));
> false
// 条件2を満たしていない
console.log(test('testTEST'));
> false
// 条件1と条件2を満たしている
console.log(test('testTEST@'));
> true

参照

https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Regular_Expressions#using_parentheses

脚注
  1. https://developer.mozilla.org/ja/docs/Web/JavaScript/Guide/Regular_Expressions ↩︎

  2. https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_credentials_passwords_account-policy.html ↩︎

Discussion