とある通販サイトで適当にパスワード打ち込んだら、後日思い出せなくなって涙目になったのでカッとなってパスワードジェネレーターを作ってみた。
Bookmarklet にしてご利用ください。Firefox 8.0でのみ動作確認しています。
概要
ソースコードは最下部に記載しています。ドメイン名とマスターパスワード(Salt)から、都度パスワードを算出する Bookmarklet です。マスターパスワードが変わらない限り、同じドメインのページでは常に同じパスワードが発行されますので、もうこのサイトのパスワード何だったっけ、と頭を悩ます必要がありません。ただし、マスターパスワードが漏洩すると元も子もありませんので、マスターパスワードの管理だけはしっかりするようにしてください。
初回実行時には記号使用の有無と文字数を聞かれます。これらの値はブラウザの LocalStorage に保存されるため、履歴が有効であれば次回以降は生成されたパスワードのみが表示されます。
下記ソースをコピーし、都度マスターパスワードを打ち込む場合は
var salt = prompt('Salt?');
に、いちいち打ち込むのが面倒くさい場合は
var salt = '適当なパスワードに書き変えてください';
としてください。書き換える場合は日本語でもアルファベットでも構いませんが、強度的には日本語の方が望ましいです。
詳細
元ネタはこちら。ソースが Obsolete になってたので、書き直すついでに Bookmarklet 化して、HMACを使うようにアルゴリズムを変更。
設定した Salt を秘密鍵に、ドメイン名をメッセージとして HMAC を算出し、求めたバイト列を元に、パスワードとして使う文字列を切り出しています。一応、なんちゃって重複排除機能を搭載。
Salt は UTF-8 対応の文字列であれば何でも構いません。日本語の文章でも問題はなく、むしろ攻撃回避の観点からはそちらの方が望ましいとも言えます。また、HMACの構造上、仮に算出されたパスワードから HMAC ハッシュ値が判明したとしても、元の秘密鍵にたどり着くことはできませんが、念のためパスワード文字列の算出に用いる文字列をシャッフルして、パスワードからハッシュ値への解読をしにくいような対策を施しています*1。
HMAC-SHA256 の算出には Crypto-js を使用しており、動的にコードを読みに行っています。このため、外部コードの読み込みにかかるタイムラグを回避するため、 setTimeout で時間差実行しています。
パスワード算出には乱数・変数を使用していませんので、 salt を変更しない限り、あるドメインに対して常に同じパスワードが返されます。これは、例えば毎月パスワードを変えている、などという場合は不便になるので、
var hmacBytes = Crypto.HMAC(Crypto.SHA256, document.domain + '_' + (new Date()).getFullYear().toString() + ((new Date()).getMonth() + 1).toString(), salt, { asBytes: true });
というように書き換える等の対処が必要です。
問題点
といったところでしょうか。
強度の確認
英大文字・小文字、数字および指示がある場合は記号も、必ず一字は使用するような設計にしています。強度確認は コンピューターとインターネット セキュリティ | Microsoft セーフティとセキュリティ センター が便利です。
ソース
javascript: (function(){ /* Salt がマスターパスワードになります。漏らさないよう注意。 */ var salt = prompt('Salt?') || '適当なパスワードに書き変えてください'; if (salt == '' || typeof(salt) != 'string') { alert('salt が設定されていません!'); }else{ if(!document.getElementById('crypto-sha256-hmac')){ var s = document.createElement('script'); s.id = 'crypto-sha256-hmac'; s.src = 'https://crypto-js.googlecode.com/files/2.3.0-crypto-sha256-hmac.js'; void(document.body.appendChild(s)); } var mod = function(e, s){ return Math.abs(parseInt(e)) % s.length; }; var shuffle = function(s, seed){ var q = ''; var p = -2; while(mod(seed.slice(p-1, p), s) == mod(seed.slice(p, p+1), s)){ if ( Math.abs(p-2) > seed.length ){ p = -2; break; }else{ p--; } } var border = [ mod(seed.slice(p-1, p), s), mod(seed.slice(p, p+1), s)].sort(function(a,b){return a-b;}); switch ( mod(seed.slice(p-2, p-1), 'aaa') ){ case 0: q = s.substring(0, border[0]) + s.substring(border[1]) + s.substring(border[0], border[1]); break; case 1: q = s.substring(border[0], border[1]) + s.substring(0, border[0]) + s.substring(border[1]); break; case 2: q = s.substring(border[1]) + s.substring(border[0], border[1]) + s.substring(0, border[0]); break; } return q; }; var digit = '0123456789'; var uc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; var lc = 'abcdefghijklmnopqrstuvwxyz'; var symbol = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"; var allstr = ''; for(var i=33; i<127; i++){ allstr += String.fromCharCode(i); } var genPass = function(binarray, len, isUseSymbol){ var i = 3; var src = ''; var str = []; var dupCheck = true; var isDuplicated = false; var duplication = []; str.push(uc.charAt(mod(binarray[0], uc))); str.push(lc.charAt(mod(binarray[1], lc))); str.push(digit.charAt(mod(binarray[2], digit))); if(isUseSymbol){ str.push(symbol.charAt(mod(binarray[3], symbol))); i = 4; src = shuffle(allstr, binarray); }else{ src = shuffle(digit + uc + lc, binarray); } while(str.length < len){ if(dupCheck){ for(var j = 0; j < str.length; j++){ if( str[j] == src.charAt(mod(binarray[i], src)) ){ isDuplicated = true; duplication.push(mod(binarray[i], src)); break; } } } if(!isDuplicated){ str.push(src.charAt(mod(binarray[i], src))); } isDuplicated = false; i++; if (i >= binarray.length) { for(var k = 0; k < duplication.length; k++){ if(str.length < len){ str.push(src.charAt(duplication[k])); } } dupCheck = false; i = 0; } } return shuffle(str.join(''), binarray); }; setTimeout(function(){ var hmacBytes = Crypto.HMAC(Crypto.SHA256, document.domain, salt, { asBytes: true }); var l = parseInt(localStorage.getItem('length')); var useSymbol = eval(localStorage.getItem('useSymbol')); var q = 3; if ( ( isNaN(l) ) || ( typeof(useSymbol) != 'boolean' ) ) { useSymbol = window.confirm('記号を使用しますか?'); if (useSymbol) { q = 4; } l = prompt(q + '文字以上、' + hmacBytes.length + '文字以下で文字数を設定してください。', 10); if ( ( !isNaN(parseInt(l)) ) && ( parseInt(l) <= hmacBytes.length ) && ( parseInt(l) >= q ) ){ localStorage.setItem('length', l.toString()); localStorage.setItem('useSymbol', useSymbol.toString()); }else{ return 0; } } window.prompt('Pass phrase:', genPass(hmacBytes, l, useSymbol)); }, 500); } })();