ブログの書き溜めがなくなり、久方ぶりに書くことになりました山本です。

今回は、暗号化(難読化)です。

といっても、複雑な暗号化は私の数学的知識では出来そうにありませんので、とても簡単なものを考えております。

さて、なぜ難読化を行おうと思ったかというと、あるユーザの操作情報をCookieに入れるのですが、その中身を見て気付かれたくなかったり、簡単に加工されたくなかったりといった事が理由でした。
まあ、Cookieを使わずに、Ajaxか何かでサーバと通信してが一番確実なのですが、そこまでする程の情報でもない場合のケースです。

もっとも、分かる人がソース見れば、復号化も出来るわけですから単純にいえば、足切りですね。

暗号化

さて簡単な暗号に、カエサル暗号というものがあります。

値を左右にシフトする暗号です。

例えば、もとの値が「D」であった場合、左に3シフトすると、「A」となります。

wikiからの引用ですが、以下のように変更されます。

平文: THE QUICK BROWN FOX JUMPS OVER THE LAZY DOG
暗号文: QEB NRFZH YOLTK CLU GRJMP LSBO QEB IXWV ALD

まず、これベースにしていきましょう。

カエサル暗号では、アルファベットしか使用しないので、シフトが簡単ですが、日本語を使うとなると少々面倒になります。

なので、Base64にしてしまいましょう。
(IE10未満は使用出来ないので、切り捨てです)

String.prototype.b64encode = function(){
  return btoa(unescape(encodeURIComponent(this)));
};
String.prototype.b64decode = function(){
  return decodeURIComponent(escape(atob(this))); 
};

var word     = 'あいうえお';
var original = word.b64encode().split('');
var newWord  = [];
var offs     = 10;

for (i in original){
  var code = original[i].charCodeAt(0);
  newWord.push(String.fromCharCode(code + offs));
}

console.log(newWord.join(''));

右に10シフトした結果です。

?As@>>UPAAƒo?As@>>YA>ys:?As@AA5C

簡単に難読化が出来ました。

が、ブラウザ上では分かりませんがAAとo?の間に一箇所抜けが出ています。

これは、y:121に10を加算すると131となり、ASCIIの範囲を超えてしまうからですね。
ですので、ASCIIの範囲を超える場合、循環するように調整を行う必要があります。

また、復号化の際に問題が出そうな気がする制御文字、0~31と127ですが、これも除外します。

方法としてはその文字のコードから32引いて、その数字を(127-32+1)で割り、そこから余りを出し、32を加算することで可能でしょう。
32以下の制御文字が入っていた場合、問題が発生しますが、そもそも制御文字を暗号化する必要があるとは思えないので無視します。

var asciiLimit   = 126;
var asciiExclude = 32;
var asciiOffset  = asciiLimit - asciiExclude + 1;

for (i in original){
  var code = original[i].charCodeAt(0);
  newWord.push(String.fromCharCode(((code - asciiExclude + offs) % asciiOffset) + asciiExclude));
}

調整すると、制御文字が出ないようになりました。

?As@>>UPAA$o?As@>>YA>ys:?As@AA5C

シフト数を調整する

ベースは出来たので、複雑にしていきます。

まず、固定の数値分シフトするのは少々面白くないので、シフトする数を可変としましょう。
とはいえ、ランダムでは復号化出来ないので何かしらの法則性を設ける必要があります。

for (i in original){
  var offs = Math.round(Math.pow(i, 2) + (Math.pow(i, 3) / 10));
  var code = original[i].charCodeAt(0);
  newWord.push(String.fromCharCode(((code - asciiExclude + offs) % asciiOffset) + asciiExclude));
}

少々安易ですが、文字それぞれに、左から数えて何文字目かを二乗して、それに同じ値を三乗して10で割った数を足し、四捨五入した数だけシフトするようにしました。
言葉にすると分かりにくいですが

1 : (1 * 1) + (1 * 1 * 1 / 10) = 1.1 = 1
2 : (2 * 2) + (2 * 2 * 2 / 10) = 4.8 = 5
3 : (3 * 3) + (3 * 3 * 3 / 10) = 11.7 = 12

計算式にするとそれ程複雑ではありません。

今回は手間なので行いませんが、10で割っているところに、振り幅を入れてみたり
現在二乗と、三乗をしていますが、これを偶数、奇数、素数、任意の約数等の条件でn乗になるようにすれば更に分かりにくくなるのではと思います。

ダミーを混ぜる

現在、シフトはしていますが本物のデータのみとなっています。
ここにダミーも混ぜるようにすると強度が増しますね。きっと。

段階的に進めていきましょう。

まずは一つおきに、簡単にダミーが入るように調整します。
とりあえずダミーなので、ランダムの値を入れておきます。

for (i in original){
  var offs = Math.round(Math.pow(i, 2) + (Math.pow(i, 3) / 10));
  var code = original[i].charCodeAt(0);
  newWord.push(String.fromCharCode(((code - asciiExclude + offs) % asciiOffset) + asciiExclude));
  newWord.push(String.fromCharCode($.getRoundom(asciiExclude + 1, asciiLimit)));
}

ダミーが入るようになりました。

で、ここで問題なのですが

1回目:5M8mnVB4J>ZY&W:qKUr#$%FAUz@7dg/{58Hf$!9LpZ5j’RT=oZ:ZEv[OY@iI!_fZ
2回目:5q8an1B-J/Z@&l:iKSr7$HFaUs@&dw/g5WH3$+9’pu5i’rT of:LE1[JYui&!jfB
3回目:5l8?n(BMJvZn&Z:3KXri$LF\UM@tdA/^5<HX$C9Op!5s’OT*o4:<EH[XY5i5!2fX

ダミーがランダムだと、差分でどの値がダミーであるかが判別出来てしまいます。

なので、ランダムは避けたほうが良いでしょう。

var getRangeValue = function(code, offs){
  return ((code - asciiExclude + offs) % asciiOffset) + asciiExclude
};

for (i in original){
  var offs = Math.round(Math.pow(i, 2) + (Math.pow(i, 3) / 10));
  var code = original[i].charCodeAt(0);
  newWord.push(String.fromCharCode(getRangeValue(code, offs)));
  newWord.push(String.fromCharCode(getRangeValue(code + Math.pow(code, 3), 0)));
}

一個前の文字のコードに、同じコードの3乗を足してみました。

1回目:5A8Un<BgJ<Z<&7:xKUrU$zF UA@Ud</g5<H<$D9Up<5z'<T<oA:UE<[gYUiU!#f_
2回目:5A8Un<BgJ<Z<&7:xKUrU$zF UA@Ud</g5<H<$D9Up<5z'<T<oA:UE<[gYUiU!#f_
3回目:5A8Un<BgJ<Z<&7:xKUrU$zF UA@Ud</g5<H<$D9Up<5z'<T<oA:UE<[gYUiU!#f_

並び替える

さて、ダミーを足すところまでやりました。

ただ、一文字おきというのは少々安易です。

正しい文字がバラけて、かつダミーの数、順番に波を設けます。

うまくバラけるようにしたいのですが、私の数学的 知識では良いアイディアがないので

以下のように、ある程度簡単に振り分けを行います。

123456789

864213579

そして、各文字の間にダミー文字を入れていきます。

入れる個数については、ひとつ前の文字のコードから、末尾一桁の数だけ入れていきます。

String.prototype.obfuscateEncode = function(){
  var word     = String(this);
  var original = word.b64encode().split('');
  var newWord  = [];
  var result   = [];

  var asciiLimit   = 126;
  var asciiExclude = 32;
  var asciiOffset  = asciiLimit - asciiExclude + 1;

  var getRangeValue = function(code, offs){
    return ((code - asciiExclude + offs) % asciiOffset) + asciiExclude
  };

  for (var i in original){
    var offs = Math.round(Math.pow(i, 2) + (Math.pow(i, 3) / 10));
    var code = original[i].charCodeAt(0);
    if (i % 2 == 0){
      newWord.push(getRangeValue(code, offs));
    }
    else{
      newWord.unshift(getRangeValue(code, offs));
    }
  }

  for (var i in newWord){
    var keyNumber = String(newWord[i]).charAt(String(newWord[i]).length - 1);
    var prevCode  = newWord[i];
    result.push(String.fromCharCode(newWord[i]));
    for (var j = 0; j < keyNumber; j++){
      prevCode = getRangeValue(prevCode + Math.pow(prevCode, j), 0)
      result.push(String.fromCharCode(prevCode));
    }
  }

  return result.join('');
}
fgoiju~Wb[\:;vM(K7d(TUK__56lW9:tRF7PnHI3/0`ai#s(@A#xFFrs(xF:;vM(K7d(ZBC'(i#s89r____56lWnJK7(i&'NRF7PnUKL9L9&$%J(i#sUVMtCId56lW$%J(i#spq$'(Ps(K7d(}opEF-K7Zd2dUYZUZ<As(#<!"D%

はい、難読化してみました。

もはや、原型をとどめていませんせんね。

復号化

次は、復号化です。

まず、今までおこなってきたものを逆順にするだけです。

バラして、要らないの消す?
のは簡単でした。

次に、シフトした値を戻して終わりになる予定なんですが。。。。
値を戻すことが出来なくなりました。

暗号化した際には、逆から計算すばいいから大丈夫だろうぐらいしか考えて居なかったので
よくよく考えてみると、余りはあるのですが商の値が存在しないので逆算が出来ない。

とりあえず、コードを総当りすれば出来るのでそれでやってみます。

String.prototype.obfuscateDecode = function(){
  var word = String(this).split('');
  var w1   = [];
  var w2   = [];

  for (var i = 0; i < word.length; i++){
    var code = word[i].charCodeAt(0);
    w1.push(code);
    i += parseInt(String(code).charAt(String(code).length - 1));
  }

  var length = w1.length;
  for (var i = 0; i < length; i++){
    if (i % 2){
      var v = w1.pop();
    }
    else{
      var v = w1.shift();
    }

    var foo = length - i - 1
    var offs = Math.round(Math.pow(foo, 2) + (Math.pow(foo, 3) / 10));

    w2.unshift(String.fromCharCode(v.getOriginalCode(offs)));
  }
  return w2.join('').b64decode();
}

一応これで復号化出来るようになりました。
方法としてはかなり不本意ですが。

やはり復号化するというのが、結構面倒ですね。

今回も、暗号化した文字列内に商を保持しておけば総当りしなくも良かったわけですが、それをどこに含ませるのか。
いざ自分で作ってみると考えなくてはいけないことが多そうです。