任意の文字列からパスワードを作成する

2023年5月7日

複雑なパスワードを作るときの支援アプリです。 任意の文字列から、数字・アルファベット・記号の文字種を使ったパスワードを作成します。長さは4~40文字までです。 入力文字列(ここでは合言葉と表現しておきます)は日本語も使えますので、例えばお気に入りの小説の一節からパスワードを作ることもできます。 空白文字(スペース、タブ文字、改行文字)は無視しますので、これが混じっていても生成されるパスワードは変化しません。
合言葉
seed:
パスワードの文字数
8
パスワード
0~9, a~z
0~9, a~z, A~Z
0~9, a~z, A~Z, 記号

参考

Seeding the random number generator in Javascript – Stack Overflow
パスワードに使用可能な記号一覧 – 登記・供託オンライン申請システム

ソース

・html
<div id="p1837">
	<div class="v-flex in">
		<div>合言葉</div>
		<div class="h-flex">
			<div class="f-grow v-flex">
				<div><textarea class="words js-in w100" rows="4" spellcheck="false"></textarea></div>
				<div class="seed ml-auto">seed: <span class="js-seed"></span></div></div>
			<div class="v-flex">
				<div><a href="#" class="btn js-clear">消去</a></div>
			</div>
		</div>
		<div>パスワードの文字数</div>
		<div class="h-flex">
			<div class="f-grow"><input type="range" class="w100 js-range" min="4" max="40" value="8"></div>
			<div class="num js-chars">8</div></div>
	</div>
	<div class="next-arrow">▼</div>
	<div class="v-flex out">
		<div>パスワード</div>
		<div>
			<div class="caption">0~9</div>
			<div class="h-flex">
				<div class="f-grow"><input type="text" class="gray js-out1" spellcheck="false" data-num="1" readonly></div>
				<div><a href="#" class="btn js-copy" data-num="1">コピー</a></div>
			</div>
		</div>
		<div>
			<div class="caption">0~9, a~z</div>
			<div class="h-flex">
				<div class="f-grow"><input type="text" class="gray js-out2" spellcheck="false" data-num="2" readonly></div>
				<div><a href="#" class="btn js-copy" data-num="2">コピー</a></div>
			</div>
		</div>
		<div>
			<div class="caption">0~9, a~z, A~Z</div>
			<div class="h-flex">
				<div class="f-grow"><input type="text" class="gray js-out3" spellcheck="false" data-num="3" readonly></div>
				<div><a href="#" class="btn js-copy" data-num="3">コピー</a></div>
			</div>
		</div>
		<div>
			<div class="caption">0~9, a~z, A~Z, 記号</div>
			<div class="h-flex">
				<div class="f-grow"><input type="text" class="gray js-out4" spellcheck="false" data-num="4" readonly></div>
				<div><a href="#" class="btn js-copy" data-num="4">コピー</a></div>
			</div>
		</div>
</div>
・css
.v-flex { display: flex; flex-direction: column; }
.h-flex { display: flex; flex-direction: row; }
.f-grow { flex-grow: 1; }
.w100 { width: 100%; }
.ml-auto { margin-left: auto; }

.words {
	line-height: 1.25;
	padding: 0.5em 0.5em;
	tab-size: 4;
}

.caption {
	color: #888;
	font-size: 80%;
	line-height: 1;
}

.copy {
	font-size: 80%;
	line-height: 1;
}

.in {
	padding: 1em 2em;
	border: 8px solid #DFD;
	border-radius: 16px;
}

.out {
	padding: 1em 2em;
	background-color: #DFD;
	border-radius: 16px;
}

.gray {
	background-color: #EEE;
}

#p1837 .btn {
	display: block;
	padding: 0.333em 1em;
	margin-left: 1em;
	background-color: #DDD;
	text-decoration: none;
	border-radius: 5px;
	white-space: nowrap;
}

.num {
	margin-left: 2em;
}

.next-arrow {
	text-align: center;
	color: #0A9;
}

.seed {
	color: #888;
	font-size: 80%;
	line-height: 1;
}
・javascript
jQuery(($) => {
	let seed;

	main();

	function main() {
		$('.js-in')
			.on('keyup', updateSeedAndPasswords)
			.change(updateSeedAndPasswords);

		$('.js-range')
			.on('input', updatePasswords)	// on dragging thumb
			.change(updatePasswords);		// on changed

		$('.js-copy').click((ev) => {
			const num = $(ev.currentTarget).data('num');
			const pw = $(`input[data-num="${num}"]`).val();
			console.log(pw);
			copyToClipboard(pw);
		});

		$('.js-clear').click(() => {
			$('.js-in').val('');
			clearAll();
		});

		clearAll();
	}

	// クリップボードへテキストをコピー
	function copyToClipboard(s) {
		navigator.clipboard.writeText(s).then(() => {
			alert('コピーしました。');
		});
	}

	// 合言葉から seed を計算&パスワード作成
	function updateSeedAndPasswords() {
		const s = getIn();
		if (s == '') {
			clearAll();
		} else {
			seed = cyrb128(s);
			printSeed(seed);
			updatePasswords();
		}
	}

	// 合言葉の取得&不要文字の削除
	function getIn() {
		let s = $('.js-in').val();
		s = s.replace(/[ \t\r\n\u3000]/g, '');	// 空白文字の削除
		return s;
	}

	// 空欄にする
	function clearAll() {
		$('.js-seed').val('');
		for (let i = 1; i <= 4; i++) {
			$(`.js-out${i}`).val('');
		}
	}

	// seed の表示
	function printSeed(seed) {
		const out = [];
		for (const v of seed) {
			out.push(('00000000' + v.toString(16)).slice(-8));
		}
		$('.js-seed').text(out.join(' '));
	}

	// パスワード作成&表示
	function updatePasswords() {
		const NUM = '0123456789';						// 10
		const LOWER = 'abcdefghijklmnopqrstuvwxyz';		// 26
		const UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';	// 26
		const MARK = '`~!@#$%^&*()_+-={}[]\\|:;"\'<>,.?/';	// 32

		const pwLength = getChars();
		$('.js-chars').text(pwLength);

		const retry = 1000;
		let pw;

		pw = makePasswordByFamily(seed, pwLength, [NUM], retry);
		$('.js-out1').val(pw);

		pw = makePasswordByFamily(seed, pwLength, [NUM, LOWER], retry);
		$('.js-out2').val(pw);

		pw = makePasswordByFamily(seed, pwLength, [NUM, LOWER, UPPER], retry);
		$('.js-out3').val(pw);

		pw = makePasswordByFamily(seed, pwLength, [NUM, LOWER, UPPER, MARK], retry);
		$('.js-out4').val(pw);
	}

	// パスワード文字数を取得
	function getChars() {
		return $('.js-range').val() | 0;
	}

	// 文字種からパスワードを作成
	function makePasswordByFamily(seed, pwLength, family, retry = 1) {
		const f = family.join('');
		for (skip = 0; skip < retry; skip++) {
			pw = makePasswordByLetters(seed, pwLength, f, skip);
			if (checkPassword(pw, family)) return pw;
		}
		return '';
	}

	// パスワードの作成
	function makePasswordByLetters(seed, pwLength, letters, skip = 0) {
		const rand = sfc32(...seed);
		const ltLength = letters.length;
		let pw = '';
		for (let i = 0; i < skip; i++) rand();
		for (let i = 0; i < pwLength; i++) {
			const j = rand() % ltLength;
			pw += letters[j];
		}
		return pw;
	}

	//	各文字種から最低1文字は含めるようにする。
	function checkPassword(pw, family) {
		for (const s of family) {
			if (!containChar(pw, s)) return false;
		}
		return true;
	}

	// subject の中に、chars のうちのどれか1文字が存在するか?
	function containChar(subject, chars) {
		for (const ch of chars) {
			if (subject.indexOf(ch) != -1) return true;
		}
		return false;
	}

	// 文字列から seed を作成
	function cyrb128(s) {
		let	h1 = 1779033703,
			h2 = 3144134277,
			h3 = 1013904242,
			h4 = 2773480762;

		for (let i = 0; i < s.length; i++) {
			const k = s.charCodeAt(i);
			h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
			h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
			h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
			h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
		}
		h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
		h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
		h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
		h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);

		return [
			(h1 ^ h2 ^ h3 ^ h4) >>> 0,
			(h2 ^ h1) >>> 0,
			(h3 ^ h1) >>> 0,
			(h4 ^ h1) >>> 0
		];
	}

	// 指定された seed 値の乱数関数を作成
	function sfc32(a, b, c, d) {
		return function() {
			a >>>= 0;
			b >>>= 0;
			c >>>= 0;
			d >>>= 0; 
			let t = (a + b) | 0;
			a = b ^ b >>> 9;
			b = c + (c << 3) | 0;
			c = (c << 21 | c >>> 11);
			d = d + 1 | 0;
			t = t + d | 0;
			c = c + t | 0;

			return t >>> 0;
		}
	}
});

javascript,password

Posted by plkl