Nim and RSA(7)

『海外出張』(ダイヤモンド社)なんて本を読んだ。副題に、成功の鍵はホテルにあり! なんて 付いていた。リタイアしたオイラーには、もう不要なはずなのに。

100歩ゆずったとしても、現役時代、ホテルにこだわる事なんてしなかったなあ。寝れれば、 そして安全であれば、それでOKって主義でしたから。

本書はそれを否定してる。ビジネスマンこそ、ホテルにこだわるべき。どんな核の所に宿泊 してるかが、ステータスになるからと。りっぱな身なりをするのと一緒。そして、ホテルの 機能を十分に引き出してこそ、ビジネスを成功に導く秘訣とも。

ビジネスセンターで、通訳を雇ったり、印刷したり、フィットネス・クラブでリフレッシュ したり出来るのは、安いBBのホテルでは出来ませんよと。それって、トレンディードラマに 出て来る、きむたくさんみたいだな。

付録で、便利なホテルでの会話集が付いていた。

The Computer froze, and I can't even reboot.

パソコンがフリーズして、再起動も出来ません。出来るビジネスマンがこんな事で、おろおろ するかね? そんなん、うむを言わせず、電源断だろうに。言われたホテルの人は、どう対処 するんだろう? オタクを設備してるのだろうか?

オイラーが一流ホテルに泊まって、嬉しかったのは、北京の外資系ホテルだったかなあ。 暇を持て余して、案内を見てたら、レンタサイクル有りますって出てた事。いくらチャリの 国と言っても、市中で貸し自転車屋を探すのは、それだけでくたびれてしまいそう。

さっそく、チャリと地図を手に、市中へ繰り出しましたよ。目的は買い物。出発前に、こんな物を 買いたいんだけど、どこら辺に行けばいいって、聞いておいた。

広大な天安門広場を眺めながら、古びた町並みに行きましたねぇ。中国4000年の歴史のある 所っぽい所で、買い物が出来て、Gooooooodでしたよ。

あと、りっぱなホテルではないけど、居心地が良かったのは、ドイツの片田舎のBBなホテル。 ベッドと朝食提供のホテルね。あそこの朝食が口に合ってたたなあ。

丸パンをナイフでスライスして、よりどりみどりのハムを挟んで食べるやつ。パンの種類も ハムの種類も豊富なものだから、いろいろな組み合わせにトライ。飽きる事なかった。

そのホテルにレストランが併設。村の社交場みたいになってて、いろいろな人と知り合いに なれた。むこうからしたら、変なやつが居るぞ。どこから来たんか聞いてみよー。 何、ヤーパンから来たって。ゲイシャは居るんか? どんな着物を着てるんか? 遠い国だったんすね。

RSAを使う

前回、tccとMD5の組み合わせが不調だったので、種を駄洒落で作ってみたい。 語呂合わせ とか、16進数なら、 HexspeakAからFだけで構成される英単語 から、見繕ってくればいいだろう。あなたの知性が試されますよ。毎度、893(やくざ)や、 abadcaff(まずいコーヒー)じゃつまらないですから。

人に頼らずに自分で調べてみれ

[sakae@fb10 /usr/share/dict]$ grep -E '^[a-fA-F][a-f][a-f]+$' words | wc
      96      96     493

4文字以上だと65語になった。これだけあれば十分かな。

ともあれGMPとの合わせ技で、結構使えるものになった。キャメルケースを平気で読める、あの言語族の 人は不満がないだろう。

var M = "I Like Nim Python Lisp Scheme Clojure Ruby 4649"
var a, c : mpz
echo "\n", M
var m = init_mpz(M, 62)
echo "m: ", m
mpz_powm(c, m, e, n)
echo "c: ", c
mpz_powm(a, c, d, n)
echo "a: ", a
discard mpz_out_str(stdout, 62, a)
echo " "

実行してみると、

I Like Nim Python Lisp Scheme Clojure Ruby 4649
m: 2368801101545695871182130052235878863748308303555837537712476700143769
c: 111573458711461238479456273468498663050240486042737182473673056859119978325
a: 2368801101545695871182130052235878863748308303555837537712476700143769
ILikeNimPythonLispSchemeClojureRuby4649

ここで、一つ注意すべき事が有る。まずは、実例を

seed: 3d747b1c55b23a
p: 3744634287414685577791
q: 3522120437025564111443
n: 13189052952889924215703582329132453969762413
e: 65537
d: 6967938255349813632669625256699315913816833

I Like Nim Python Lisp Scheme Clojure Ruby 4649
m: 2368801101545695871182130052235878863748308303555837537712476700143769
c: 7765033156190690279623766764713074631171388
a: 4175684821740848355722850210294213291052596
Os5lAM65FecC7gfEB41oFgpg

暗号キーの一部である n と、平文を数値に変換した m の間には、m<n という関係が 無いと、正しく暗号化出来ないという制限が有るんだ。 素数夜曲の本でも、強く警告してたな。それは何故か?

暗号化するには、キーをe 及びnとすると、m^e mod n の計算を行っているから (^は、冪乗記号)。 結果は必ず、n 以下に丸められてしまう。m>=nだと、情報欠落になってしまうんだ。

日本語もRSA暗号出来るようにする

素数夜曲に出て来るRSAでは、平文をASCII文字列と仮定している。ASCIIってアメリカ生まれの 文字コードね。それがそのままISOになったのだけど、国際標準規格にアメリカって名前は 無いだろうってんで、ISOなんたらって規格番号に変身してるんだ。昔アメリカは強かったけど、 国名を冠するのは国際社会に受け入れられなかったのね。

そういう例がまだ他にも有ったぞ。地球時間を表す、GMT。これもイギリスのグリニッジ地方の ローカル時間。これって、イギリスの栄光をそのまま現していて癪にさわるぞって、どこかの国が 言い出すに決まってる。それを回避するため、UTCなんて名前になってる。政治ですなあ。

最近では、赤い国が言い出した世界銀行もどきのやつ。AIIBとか称して、これからの注目は アジアですよって強調してる。シルクロードを新たに作って、沿線住民とデストネーションを 豊かにしましょってやつ。北陸新幹線が開通して豊かになるのは、デストネーションだけですよ。 でも、そういう事は言わない約束です。

話が逸れた。ASCIIコードは、0から127までに文字を割り振った体系。数字、英文字、各種 記号がもれなく網羅されててもそれだけの数で済んじゃう、国の人の体系です。 この中には、印刷出来ない、いわゆる制御文字も含まれている。

暗号の最終目的地は、人間が読み書き出来る文字なんで、制御文字はイラネって考えると、 イラネって文字と取り除いちゃえって発想が出てくる。上手に取り除けば、100個に満たない 数値で、文字を表せるぞ。そうなれば、いわゆる100進法が適用出来て便利。

コード表をじっと眺めていたら、ASCIIコードから27を引く事を思い付いた。SHIFT-JISならぬ SHIFT-ASCIIの発明ですな。その取り繕いをSchemeのコードで実現してたぞ。面白い。

ひるがえって、日本語の平文を暗号にするのはどうよ?いっぱい文字があり過ぎて、小手先の 手段では、簡単にいかないな。ええい、コードを書いちゃえ。

その前に実行結果をば。

[sakae@fedora gmp]$ ./rsa
seed: 23237b1c55b23a46499696893be
n: 240523885418313615330947131716914414586821803528652295824687766884361857089
size: 31
e: 65537
d: 90305182181104182657632865755625007012182979313106412327496075053220922785
こんにちわ世界
Keyを逆に使う

実行例に出て来るsizeってのは、この文字数までなら(但し、ASCII文字ね)暗号化出来ますって目安です。 こちらがコード。

## RSA using GMP

import gmp
import gmp/utils
import strutils

let e: mpz = 65537
var p, q, n, phi, d : mpz
var seed = "23237b1c55b23a46499696893be"
echo "seed: ", seed

mpz_nextprime(p, init_mpz("caff" & seed, 16))
mpz_nextprime(q, init_mpz("abad" & seed, 16))
mpz_mul(n, p, q)
#echo "p: ", p
#echo "q: ", q
echo "n: ", n
echo "size: ", mpz_sizeinbase(n, 16) div 2

mpz_sub(p, p, 1)
mpz_sub(q, q, 1)
mpz_mul(phi, p, q)
mpz_gcdext(p, d, q, e, phi)
if d < 0:
    mpz_add(d, d, phi)
echo "e: ", e
echo "d: ", d

proc encode*(msg: string, e: mpz, n: mpz) : mpz =
    var s = ""
    var i = msg.high
    while i >= 0:
        s.add(toHex(ord(msg[i]), 2))
        dec(i)
    var m = init_mpz(s, 16)
    if n <= m:
        quit("Msg too long", 1)
    var c : mpz
    mpz_powm(c, m, e, n)
    return c

proc decode*(c: mpz, d: mpz, n: mpz) : string =
    result = ""
    var a, h : mpz
    mpz_powm(a, c, d, n)
    while 0 < a:
        mpz_mod(h, a, 256)
        result.add(chr(parseInt($h)))
        mpz_tdiv_q(a, a, 256)

when isMainModule:
    var M = "こんにちわ世界"
    var c =  encode(M, e, n)
    echo decode(c, d, n)

    M = "Keyを逆に使う"
    c =  encode(M, d, n)
    echo decode(c, e, n)

平文と公開鍵を受け取って、暗号数値に変換するencodeと、暗号数値と秘密鍵を受け取って、 平文に復元するdecodeって関数を定義したよ。 when以下は、その用例。キーは、逆に使っても大丈夫。いわゆる認証にどうぞってやつだ。

まずはencodeの方。文字列はbyteの連なり。byteってのは0から255まで表せるASCIIの拡張ね。 これを、ord関数を使って数値に変換。それを2文字分の16進数にtoHexを使って変換。そいつを、溜め込んでいく。 この時、byteのスキャンを逆にやってるのは、decodeする時の都合です。

こうして、16進文字列に変換出来たら、後は、それをGMPに備わっている関数でGMP用の 数値に変換、暗号化すればよい。数値に変換した時、キーの大きさより大きかったら、諦めてもらう。

decodeの方は、ます暗号数値を復号。それを文字列に戻していく。256で割れば、1byte分を 取り出せる。それはGMP風の出力なんでparseIntで普通のIntに直し、それをchrで文字と言うか byteに直している。

下位の桁から文字に確定して行くので、逆転した文字列になるはずなんだけど、encodeで 逆にscanしてたから、辻褄が合って、上位から確定してる事になる。

後は、256で割った商を取り出して、商が0になるまで上記を繰り返す。この時、商を求める 関数に最初、mpz_cdiv_q を使っていたんだ。そしたら、永久ループさ。 いつまで経っても、商が1になるんだ。いわば、23/256の商が1になるんだ。 慌てて、GMPのマニュアルを見ましたよ。

cdiv rounds q up towards +infinity, and r will have the opposite sign to d. The c stands for “ceil”.
fdiv rounds q down towards -infinity, and r will have the same sign as d. The f stands for “floor”.
tdiv rounds q towards zero, and r will have the same sign as n. The t stands for “truncate”. 

割り算には商の丸め方によって3種あるとな。cdivは、大きい方に向かって丸めちゃうんで、 結果が1になっちゃうと判明。tdivを使って事なきを得ましたよ。あっ、事が有ったから こういう事を知ったんじゃん。日本語の使い方を間違ってますな。 詳細な事は、 5.6 Division Functions を参照。いろいろ有って、目がチカチカするぞ。

で、今ふと思ったんだけど、tdivを使ったりmodを使ったり随分とGMPに頼っているな。 GMPの数値を16進数へと変換出来れば、後はnimの世界だけで事が済むぞ。直交演算はきっとあるはず。 こういう時は、gmp/utils.nimの中を眺めるに限る。

お目当ての関数は、$さ。

proc `$`*(a: mpz_t, base: cint = 10): string =
  result = newString(mpz_sizeinbase(a, base) + 1)
  return $mpz_get_str(result,base,a)

何気なく使うやつに、省略時のデフォルトが設定されてた。

let x: mpz = 1024
echo x $ 16
echo ($(x))

実行結果は

400
1024

ふーむ、文字数が奇数になる場合がある事に注意せいとな。

日本語って大変だ

とまあ、無事に日本語も暗号・復号出来るようになったけど、 日本海海戦 で打たれた有名な電文、 「敵艦見ユトノ警報ニ接シ 連合艦隊ハ直チニ出動 コレヲ撃滅セントス、本日天気晴朗ナレドモ波高シ」 は、そんなの長すぎって言われて、暗号文にならない。これは困った。日本負けちゃう、一大事ですよ。 景気の良いトラトラトラ ぐらいは暗号に出来るかな? って、これを暗号にしたって意味ねーじゃん。

ってな事は置いておいて、日本語の表現をちょいと調べてみる。

import strutils
import unicode

let m = "The日本国"

proc MruneLenAt*(s: string, i: Natural): int =
  ## returns the number of bytes the rune starting at ``s[i]`` takes.
  if ord(s[i]) <=% 127: result = 1
  elif ord(s[i]) shr 5 == 0b110: result = 2
  elif ord(s[i]) shr 4 == 0b1110: result = 3
  elif ord(s[i]) shr 3 == 0b11110: result = 4
  elif ord(s[i]) shr 2 == 0b111110: result = 5
  elif ord(s[i]) shr 1 == 0b1111110: result = 6
  else: result = -1  # almost 0b10xxxxxx

echo runeLen(m)
echo len(m)

for i in 0 .. high(m):
    stdout.write MruneLenAt(m, i), ", "
echo "\n"
for i in 0 .. high(m):
    stdout.write toHex(ord(m[i]), 2), ", "
echo "\n"

世界の文字コード、ユニコードをサポートするのが当たり前になってるな。表現は UTF-8が当然のごとく使われている。

6
12
1, 1, 1, 3, -1, -1, 3, -1, -1, 3, -1, -1,

54, 68, 65, E6, 97, A5, E6, 9C, AC, E5, 9B, BD,

ASCII以外は、多Byteで文字を表現してねってのを、あの人が発明した。そしてその文字表現を ASCII以外って表現出来ないものだから、やんわりとRuneなんて言い方で誤魔化した。

Runeって辞書で引くと、スカンジナビアの古詩、フィンランドの詩歌、ルーン文字、詩歌、神秘的な記号、北欧古代文字 とか出てきて、幻想的だな。決してASCII以外とは、取らないように。米中心の排他主義を 必死で隠そうと、苦労したんだからー。

米国からみると、十羽一束にされちゃった文字だけど、文字数ぐらいは数えられるように してあげようってんで、runeLenとかがサポートされてる。元を質せばバイトの寄せ集め。 バイト列を見ていった時に、どこが文字の始まりかを教えてくれる、runeLenAtがサポート されてたけど、文字を構成する要素の場合も1を返すようになってた。

そこで、ちょいと手を入れて、文字を構成する要素の場合は、-1を返すようにした。 そんなんで、日本語文字は、3Byteで1文字になってるのが分かるな。

RASを安全に

コンピュータの進歩によりRSAの鍵長を大きくする事が推奨されている。昔は1024Bitも 有れば安全とされていたのに、今じゃ2048Bitを推奨しますですって。

鍵長を大きくするには、鍵を求めるに使う乱数を大きくする必要がある。どうするか?

乱数の一種であるπから得るのがよかろう。him-gmpの使用例として、πをn桁求める アプリが付いてくる。ちょいと実験。

[sakae@fedora gmp]$ ./pin 200
31415926535897932384626433832795028841971693993751      :50
05820974944592307816406286208998628034825342117067      :100
98214808651328230664709384460955058223172535940812      :150
84811174502841027019385211055596446229489549303819      :200

上記は200桁まで表示させたけど、お望みならなん桁までも。数値を適当に貰って きて、62進法と看做して、桁数を水増しするのが正しい方法です。ちなみに、 どれぐらいの桁が水増しされるかと言うと、

gosh> (/ (log 62)(log 10))
1.7923916894982537
gosh> (/ (log 10)(log 2))
3.3219280948873626

62進のn桁は、10進数の約1.8倍。10進のn桁は、2進数の3.32倍に相当する。

var seed = "31415926535897932384626433832795028841971693993751"
mpz_nextprime(p, init_mpz("feed" & seed, 62))

2048Bitにしても、256Byte相当。これで日本語Rune文字を表現しようとすると、85文字 しか暗号化できません。なかなか大変ですな。sslとかを噛ませて、暗号方式は共有鍵方式を 使うのが、正しい方法です。

nim vs go

nimを始めた時に、ロゼッタストーンなんてのが有ることを知った。そこに登録されてる コードを2つの言語で同時表示させるサイトが有るのね。 Side-by-side code snippets from Rosetta Code

そこで、nimとgoを並べて表示させてみると、 nim vs go nimは、すっきりさっぱりですなあ。更にすっきりさせようと思ったらHaskellをやっとけって 結論で宜しいでしょうか。