もうパスワードで悩まない!

とある通販サイトで適当にパスワード打ち込んだら、後日思い出せなくなって涙目になったのでカッとなってパスワードジェネレーターを作ってみた。

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);

  }
})();

*1:そもそも生成されたパスワードが第三者の目に触れる可能性は低く、またその上でこのアルゴリズムに則って生成されたものであると判明するという極めて低い確率で起こる条件をクリアした上での、パスワードの元になるハッシュ値の先頭部分を解読しにくくするというある種の「難読化」ですので、例えば得られたHMACが全て同じ値になってしまった場合にはシャッフルは起こりませんが、そこまで想定したコードにする必然性が感じられないため、中途半端と言えば中途半端な実装になっています。