RSA暗号で「ふっかつのじゅもん」を作る(3)-完結
Pythonを使って高速素数判定をしてみる - Pashango’s Blog
RSA暗号で「ふっかつのじゅもん」を作る(1) - Pashango’s Blog
RSA暗号で「ふっかつのじゅもん」を作る(2) -RSA暗号鍵の生成
当初はサクッと終わらせるつもりだったのに、意外と長くなってしまいました。
ドラクエ1の「ふっかつのじゅもん」の仕様
まず、ドラクエ1の「ふっかつのじゅもん」の仕様を調べてみました。
- 使われる文字は64種類のひらがなと数字
- じゅもんの長さは20文字まで
- 以下の情報を含む
- なまえ(0〜63までの数値×4)
- 魔法の鍵(0〜7までの数値)
- 薬草(0〜7までの数値)
- ぶき(0〜7までの数値)
- ぼうぐ(0〜7までの数値)
- たて(0〜3までの数値)
- アイテム(0〜15までの数値×8)
- フラグ(0〜1までの数値×5)
- 経験値(0〜65535までの数値)
- ゴールド(0〜65535までの数値)
呪文の長さは20文字ですので、情報量は6bit×20文字=120bitとなります。
そのうち、最上位bitはRSA暗号化時に失われる可能性がありますので、データ保持として使えるbit数は119bitとなります。
また、上記の情報を保存するのに必要なbit数は107bitですので、119-107=12bitの余裕bitがあります。
暗号鍵を求める(eとdを求める)
前回、暗号鍵の元となる素数p,qを求めましたね。
私もすっかり忘れていましたが、乗数であるeとdを求める関数を作りましょう。
前回のgenerate_pq()と一緒のファイルに書き足してください。
def generate_ed(p,q): """与えられた素数ペアからe,dを生成""" if p*q < 5: raise ValueError,"p*q must be large than 4." l = lcm(p-1,q-1) e = random.randint(5,l-1) | 1 while True: e = e+2 if e+2 <= l-1 else 5 if gcd(e,l) == 1: break d = gcd2(e,l)[0] if d < 0: d += l return (e,d)
さっそく、鍵を生成してみましょう。
ふっかつのじゅもんに必要なbit数は120bitですので、以下のように求めます。
p,q = generate_pq(120) e,d = generate_ed(p,q) n = p*q # p = 96568316136941074427 # q = 7372545182284901 # n = 711954273896770180320412909759326727 # e = 26578325672224134549282406558845001 # d = 34518827246730708105140436249657601
pとqはn,e,dが求まれば不要ですので、廃棄してください。
「公開鍵」がeとn、「秘密鍵」がdとnの数値ペアとなります。
勇者の情報を生成する
暗号化する勇者の情報を生成するクラスを作成しました。
このクラスはパラメータの乱数生成に加え、パラメータ<->107bit数値の相互変換機能を持っています。
以下、コードですが特に面白味もないコードなので、サラッと読み飛ばしてください。
#coding:utf8 import random class DQParams(object): NAMES_CHARS = u"*あいうえおかきくけこさしすせそたちつてと"\ u"なにぬねのはひふへほまみむめもやゆよ"\ u"らりるれろわをんっゃゅょ ー"\ u"0123456789S" PARAM_INFO = [ {'name':u"なまえ",'max':len(NAMES_CHARS),'len':4}, {'name':u"魔法の鍵",'max':8,'len':1}, {'name':u"薬草",'max':8,'len':1}, {'name':u"ぶき",'max':8,'len':1}, {'name':u"ぼうぐ",'max':8,'len':1}, {'name':u"たて",'max':4,'len':1}, {'name':u"アイテム",'max':16,'len':8}, {'name':u"フラグ1",'max':2,'len':5}, {'name':u"経験値",'max':65535+1,'len':1}, {'name':u"ゴールド",'max':65535+1,'len':1} ] def __init__(self, val=None): self.params = {} for x in self.PARAM_INFO: self.params[x['name']] = [0] * x['len'] if val == None: val = random.randint(0,1<<120) dec = lambda val,max:(val/max,val%max) for x in reversed(self.PARAM_INFO): for y in range(x['len']): val,self.params[x['name']][y] = dec(val, x['max']) self.params[x['name']].reverse() def name2val(self, name): return [self.NAMES_CHARS.index(x) for x in name] def val2name(self, val): return ''.join([self.NAMES_CHARS[x] for x in val]) def generate_value(self): val = 0 for x in self.PARAM_INFO: for y in self.params[x['name']]: val = val*x['max'] + y return val def output(self): f = self.PARAM_INFO[0] print f['name'],u":",self.val2name(self.params[f['name']]) for x in self.PARAM_INFO[1:]: print x['name'],u":",self.params[x['name']]
それでは、さっそく勇者を生成してみましょう。
obj = DQParams() obj.output() #なまえ : かんてつ #魔法の鍵 : [3L] #薬草 : [0L] #ぶき : [2L] #ぼうぐ : [1L] #たて : [3L] #アイテム : [4L, 12L, 12L, 1L, 4L, 0L, 0L, 14L] #フラグ1 : [1L, 1L, 0L, 1L, 1L] #経験値 : [46633L] #ゴールド : [6040L] val = obj.generate_value() #val => 17045993125123514293179659130776
おお、よくきたな、ゆうしゃかんてつよ、
今後は、生成された勇者「かんてつ」のパラメータを使います。
チェックサムとランダム値
今回の仕様では、必要情報量107bitー最大情報量119bitで差し引き12bitの余裕bitがあります。
この12bitに「チェックサム」と「ランダム値」を埋め込みます。
「チェックサム」は、この数値が正当あるかのチェックに使われ、適当に入力された呪文をはじく効果があります。
「ランダム値」は、データ値に乱数を埋め込むことにより、同じパラメータを暗号化しても毎回呪文が変わるようになり、暗号解析がしにくくなります。
今回の例では12bitの余裕bitのうち、チェックサムに9bit、ランダム値に3bitを割り当てます。
以下、出力された数値にチェックサムとランダム値を埋め込むコードです。
def check_sum(val,base=1<<9): csum = 0 while val > 0: csum += val%(base-1) csum %= base val /= base csum = (base-1)-csum return csum csum = check_sum(val) #csum => 114 val = (val << 9) + csum val = (val << 3) + random.randint(0,7) #val => 69820387840505914544863883799659408
なお、チェックサムは((1<<9)-1)から引いた値になっていますが、これはvalが0にならないようにするためです。
valが0ですと暗号化した結果も0になってしまい、暗号になっていません。
チェックサムのビットを反転する事により、valに0が入る事を防いでいます。
「ふっかつのじゅもん」を生成
ここまで長い道のりでしたが、ようやく「ふっかつのじゅもん」を生成します。
val = 69820387840505914544863883799659408 n = 711954273896770180320412909759326727 e = 26578325672224134549282406558845001 d = 34518827246730708105140436249657601 val = pow(val,e,n) #val => 692554535384224777977608304993455189 PASS_CHARS = u"あいうえおかきくけこさしすせそたちつてと"\ u"なにぬねのはひふへほまみむめもやゆよわ"\ u"らりるれろ"\ u"がぎぐげござじずぜぞだぢづでどばびぶべぼ" s = [] while True: s.append(PASS_CHARS[val%64]) val /= 64 if val <= 0: break print "".join(s) # にざぜくねだひかづそぎけあぬへさときぬめ
勇者「かんてつ」のふっかつのじゅもんは「にざぜくねだひかづそぎけあぬへさときぬめ」となりました。
さっそく、復活できるかチェックしてみましょう。
n = 711954273896770180320412909759326727 d = 34518827246730708105140436249657601 val = 692554535384224777977608304993455189 val = pow(val,d,n) csum = (val>>3) & ((1<<9)-1) val >>= 12 csum2 = check_sum(val,1<<9) if csum != csum2: print u"ふっかつのじゅもんがちがいます" else: obj = DQParams(val) obj.output() #なまえ : かんてつ #魔法の鍵 : [3L] #薬草 : [0L] #ぶき : [2L] #ぼうぐ : [1L] #たて : [3L] #アイテム : [4L, 12L, 12L, 1L, 4L, 0L, 0L, 14L] #フラグ1 : [1L, 1L, 0L, 1L, 1L] #経験値 : [46633L] #ゴールド : [6040L]
ゆうしゃかんてつよ、よくぞもどってきた
RSA暗号化 vs 旧来の暗号化
RSA暗号化による暗号化と、旧来の暗号化の違いはなんでしょうか?
まずは、保存可能な情報量の違いから。
旧来の暗号化はまるまる120bitに情報を詰め込めます、RSA暗号化は最上位ビットが不定になりますので119bitしか使えません。
余った情報はチェックサムに使われますので、RSA暗号化の方がチェックサムが1bit少なくなります、これはどういう事かと言いますと、適当に打ち込んだ呪文が通ってしまう確率が旧来暗号化に比べて2倍高いという事です。
一方、RSA暗号のメリットは、やはり暗号化と複合化に使われる鍵が2つにわかれている点でしょう。
旧来暗号化の弱点は、暗号化と複合化が必ずセット提供されてしまう点です。
「暗号化の手順=復号化の逆手順」であり、そのまた逆も然りです。
リバースエンジニアリングにより、暗号化の手順が解析されてしまうと、自動的に復号化の手順も解析されてしまうのです。
一方、RSA暗号では、暗号化と復号化のどちらか一方のみの提供が出来ます。
暗号化はサーバーで行い、クライアントは復号化しかできないという扱いも可能です。
(RSA暗号でも、クライアントに暗号鍵と復号鍵を埋め込んだらリバースエンジニアリングには無力ですよ、念のため)
うんうん、なんか使えそうなんだけど、意外と使いどころが難しい技術ですね。
でも、工夫次第ですごい面白い事もできそうです。