パスワード生成器 (初級プログラミング)
プログラミング::beginners
序
初級プログラミングの記事への要望が非常に高い。
それに応えずにいるのは、いくつか理由がある。
最大の理由は労力に見合わない、ということだ。 初級プログラミングの要望は、実は 要望が多いのに、実際に書くと読まれない 。
あと、初級プログラマが本当に有効な知識を説明しようとしても、やっぱり読まれない。 初級プログラミングの話は、非常に予定調和の要求が高くて、他のサイトや本に書いてあることとまるで同じようなことを書かないと読まれない。 それは最高に意味がないので、書く意義を見いだせない。
また、Chienomiはどちらかというとエッジなものを扱う考え方であるためあまりそぐわないということもあるのだが、 Mimir Yokohamaの記事で書こうとすると、コンテンツを学習として有効なコンテンツ群の一部として書く必要があるため執筆コストが非常に高く、 それでいてあまり読まれない上にプログラミング記事はプロモーション効果も薄いので非常に書きにくい。
ただ今回一応ではあるが、beginnersというカテゴリを作ったので、(記事が読まれれば)今後こうした初級者向け記事は増えていくかもしれない。
ただし、Mimir Yokohamaでは「すべての固有概念は一度は説明される」が、Chienomi的には一般的な用語の説明などはあまりしない。 意味を正しく理解していなくても、文脈から読み取ることが可能であるはずだからだ。
概要
ウェブサービスやログインなど様々な場面で実に多くのパスワードが要求される。
もちろん、 同じパスワードを使い回すなどということは極めて脆弱な行いでやってはならない し、毎回十分なエントロピーをもったパスワードを作るのは骨が折れる。 Linuxではパスワードを生成するプログラムはいくつかあるが、比較的大きなプログラムの一部だったり、エントロピーに不満があったり、なんらかの不満があったので作ってみた。
ここで紹介するコードは、一度作ったあと、この記事のために説明的に書き直したものだ。
See into code.
Code
#!/usr/bin/ruby
# -*- mode: ruby; coding: utf-8 -*-
= ARGV.shift&.to_i || 64
pwlength = 64 if pwlength < 1
pwlength
= ARGV.shift&.each_char
converter_symbols
= converter_symbols ? ("a" .. "z").to_a + ("A" .. "Z").to_a + ("0" .. "9").to_a + converter_symbols : ('!' .. '~').to_a
converter
= 255 / converter.length
convloops = converter.length * convloops
convlength
File.open("/dev/urandom", "r") do |f|
.times do |i|
pwlength= f.readbyte
c redo if c >= convlength
print(converter[c % converter.length])
end
end
解説
shebang
#!/usr/bin/ruby
Shebang行である。
今回は初回なので、ここも説明しておこう。
Unixシステムにおいては「実行可能なテキストファイル」を実行するとき、その一行目を読んでそのテキストファイルを解釈させるプログラムを決定する。
これはシステム自体が持っている機能であり、シェルが持っている機能ではないが、シェルもそのような機能を持っている可能性がある。 シェルがその機能を持っている場合は、実行した場合に「システムに対してそのプログラムを実行する」という振る舞いではなく、「引数としてそのファイルを渡す」という振る舞いになるから、システムのその機能に頼ることはない。 実はperl(1)が同様の機能を持っており、perl(1)はPerlでないshebangを持つスクリプトを起動できる。
この場合、shebangは#!/usr/bin/ruby
であるから、これが~/bin/mkpwd.rb
というファイルだとすれば
/usr/bin/ruby ~/bin/mkpwd.rb
という形式で起動されることを意味する。このため、shebang行には引数やオプションを含めることができる。
shabang行がない場合、システムはデフォルトのシェルに対する引数としてそのテキストファイルを渡す。
もちろん、これは「実行可能なテキストファイルを実行した場合」の振る舞いであり、このテキストファイルを引数として渡した場合には関係ない。 また、その場合にはそもそもこのテキストファイルが実行可能である(パーミッション的にexecutable flagを持っている)必要がない。
なお、この仕組みであるから起動された処理系はshebang行を含むテキストファイルをソースにすることがある。
多くのスクリプト処理系では#
はコメントであるから、コメントとして読み飛ばすことができる。が、中には#
がコメントになっていない処理系もある。
例えばLispとか…と言いたいところだが、clisp
もsbcl
もちゃんとshebangは読み飛ばすようになっている。Luaも同様である。
理由は、もちろんそうでないと困るから。
ruby マジックコメント
# -*- mode: ruby; coding: utf-8 -*-
Shebang行につづいてRubyはマジックコメントを書くことができる。 これは、スクリプトエンコーディングを指定することができる。
# coding: utf-8
と書けばスクリプトエンコーディングがUTF-8で書かれていることを明示できる。 これが必要なのはRuby 1.9だけであり、Ruby 2.0からはUTF-8がデフォルトだが、明示することでいくつかのトラブルが回避できる可能性が示されている。
# -*- mode: ruby; coding: utf-8 -*-
```は
というのはEmacsの形式で、Emacsは`var: value`という形式で変数を指定できる。[(公式ドキュメント翻訳版)](https://ayatakesi.github.io/emacs/25.1/Specifying-File-Variables.html){rel="external"}
つまり、Emacsの変数として書いておきながらRubyはそれをうまく解釈する、ということである。
もちろんそれは`coding: utf-8`という文字列を含むから、と考えられるが、Rubyはvim形式で次のように書かれていたとしても
```ruby
# vim:set fileencoding=utf-8:
正しく認識する。これは、vimに対してUTF-8のファイルエンコーディングを明示するものだが、これを正しく解釈する。 なお、Vim自身Emacs形式で書かれているものもある程度解釈するようである。
ここでスクリプトエンコーディングを明示するのはそれほど重要なことではないが、文字の順序が影響するコードなので、ASCII非互換の文字集合だと解釈されると困る。
だから、ここでは別にus-ascii
でも構わない。
パスワードの長さ
= ARGV.shift&.to_i || 64
pwlength = 64 if pwlength < 1 pwlength
ARGV
はコマンドライン引数オブジェクトである。ARGV.shift
で引数オブジェクトの先頭の値を削除し、このメソッドの返り値としてその値を返す。
ARGV
はArray
インスタンスであり、shift
というメソッドはArray
に属している。
Array#shift
は配列の長さが0である(つまりからっぽである)場合、nil
を返す。
nil
はRuby独特の言葉で1、一般的にはnull
とかundefined
と呼ばれることが多い。
つまり、ARGV.shift
は潜在的には(コマンドライン引数を与えられていなければ)nil
を返す。後続するメソッドが値が存在することを前提に進めるとエラーになる可能性がある。
&.
というのはRubyの「ぼっち演算子」である。これはRuby
2.3から登場したものである。
このぼっち演算子は後続のメソッドを、レシーバ(この場合ARGV.shift
の結果)がnil
でない場合だけ起動する。
レシーバがnil
だった場合、メソッドは起動されることなくnil
を返す。
つまり、引数があればそれをto_i
によって整数化した結果がpwlength
となるが、引数がなければnil
になるので論理ORによって右辺64
が代入される。
次行では1
以下である場合、つまり、引数が0
だったり-1
だったり、hogehoge
だったり(この場合.to_i
すると0
になる)する場合にデフォルト値にリセットしている。
この記述は説明的なもので、実は
そもそもぼっち演算子が必要な状況ではない 。
なぜならば、nil
が所属するNilClass
には#to_i
メソッドが存在し、なおかつ0
を返すからだ。だから
= ARGV.shift.to_i
pwlength = 64 if pwlength < 1 pwlength
で事足りる。私の好みとしてはこちらのほうがいい。
ただし、to_i
が0以下を返すような引数を許さないのであれば扱いが違うため、
= ARGV.shift&.to_i || 64
pwlength if pwlength < 1
abort "You must set password length as $1."
end
のように書くことになるからぼっち演算子が必要になる。
だが、今回の場合は「任意性のある2つの引数」をとるため、そのようなことはないはずだ。
記号集合の定義
= ARGV.shift&.each_char converter_symbols
converter_symbols
変数はパスワードで使用される記号を定義している。Array
インスタンスである。
ここで再びARGV.shift
が登場している。既に一度shift
しているから2番目の引数だ。
結果は、nil
かString
インスタンスである。
String#each_char
はブロックを与えるかどうかで挙動が異なる。
ブロックを与えた場合、ブロック変数に対して1文字ずつ(1バイトではなく、エンコーディングで定義された1文字ずつである)代入しながらループする。
ブロクを与えなかった場合、Enumerator
インスタンスが返る。
Enumerator
クラスはイテレータクラスである。イテレータクラスは「反復可能なコレクション」のクラスである。
Ruby 1.8では標準ライブラリだったが、Ruby
1.9から組み込みになり、扱いも少し変わった。
例えば
[1, 2, 3].each do |i|
puts i
end
というのは配列に対するイテレーションであり、each
メソッドがイテレータということになるのだが、配列は配列であって繰り返しのためだけに存在しているわけではない。
これを
= [1, 2, 3].each enum
とすると、enum
はEnumrator
インスタンスとなり、繰り返しのための存在に変わる。
Enumrator
クラスにする大きな理由は外部イテレータにすることである。例えば
= enum.next arg1
のようにして1要素ずつ取り出して処理できる。 要素の1番目だけ扱いが違う、というような時に
= lines.each
enum = enum.next
title while i = enum.next
#...
end
みたいなことができる。
もうひとつ、有用な用法が、「反復可能な要素を配列にできる」ことである。これは、Enumerator#to_a
という配列化のメソッドがあり、反復される要素を配列にしたものになるからだ。
元要素が配列の場合は全く意味がないが、今回の場合文字列であるため、「1文字ずつ分離して配列にする」という簡単かつ速い方法になる。
ちなみに、他の方法としては
= ARGV.shift.split(/(.)/) converter_symbols
というやり方もあるが、こっちのほうがわかりにくいだろう。
候補文字配列
さて、ぼっち演算子を使用しているため、第2引数がなかった場合はeach_char
されることなくnil
を返す。
= converter_symbols ? ("a" .. "z").to_a + ("A" .. "Z").to_a + ("0" .. "9").to_a + converter_symbols : ('!' .. '~').to_a converter
で三項演算子を使って、nil
かどうかによって処理が分かれるようになっている。('!' .. '/')
というのはRange
リテラルだ。
Range
はそのまま、「範囲」を表すものであり、範囲に含まれているかどうかという判定ができたり、イテレータを利用可能であったりする。
この範囲はsucc
というメソッドを使って作られるようになっており、左の要素をsucc
していって、右の要素になったら終了だ。
String#succ
は文字集合的に「次の文字」を返すようになっている。正確には、アルファベットはアルファベットで「次」を返すし、「数値の文字列」の場合は数値的な「次」を返すが。
だが、実際には
("A" .. "~")
も("A" .. "ZZ")
も正しく動作するので、もうちょっと複雑かつ器用にこなしてくれる。
そして、!
はASCIIにおける最初の記号、~
は最後の記号である。
このあたりはシステムと文字エンコーディングに関する知識が必要になる。
そして、ASCIIを知っていれば
('!' .. '~')
が「すべてのASCII可視文字」になることがわかるはずだ。この間にはアルファベットや数字が全て含まれている。
Range#to_a
はrange.each.to_a
的な結果を返す。だから、('!' .. '~').to_a
ですべてのASCII可視文字からなる配列が得られる。
左辺の場合はその方法でアルファベット、数字の文字列を作って指定された記号に連結している。
実はこれは書き方はかなり様々あるのだが、逆に「使用禁止する記号を指定する」形式だと
= ('!' .. '~').to_a ^ (ARGV.shift&.each_char || []).to_a converter
という方法でまとめて1行で書けたりする。 配列を集合として扱う方法があるのは比較的珍しい。
余剰の計算
= 255 / converter.length
convloops = converter.length * convloops convlength
これらは生成時に無駄が起きたり偏りが発生しないように調整するためのものだ。
1バイトの最大値は0xff
であり、255
である。
例えば候補文字が50あるとすると、
[converter % 50] byte
で50以上の値は出なくなる(%
で商の余りであり、50以上は50で割れるので余りが50以上になることはない)のだが、これだと先頭の6つ(0から数えるから)だけ出る確率が上がる。
かといって、
next if 50 <= byte
とかやると50
から255
までの随分大きな範囲がスキップされることになってしまい、「ものすごくサイコロを振り直すランダム」みたいになる。
まず1行目で/
によって割っているが、RubyにおいてInterger#/
は余りを捨てる。
だから、255 / 50
は5
になる。
そして、50 * 5
によって得られる250
は正確にループ可能な最大数であり、配列としての最大インデックスは249
である(0から始まるから)。
だから、250
以上を割った余りは先頭から中途半端なところで終わるということがわかるので、
next if 250 <= byte
が最小の「振り直し」ということになる。
これを変数化したものである。
urandom
File.open("/dev/urandom", "r") do |f|
#...
end
/dev/urandom
はUnixシステムにある疑似乱数生成器である。
暗号論的擬似乱数生成器であり、ちゃんと安全なエントロピーを持っている。/dev/random
は真乱数生成器だが、パスワードに使える長さの乱数を得るのはものすごく大変なのでここでは/dev/urandom
で十分。
/dev/urandom
はLinux発祥だが、FreeBSD, OpenBSD, NetBSD,
DragonflyBSD, Solaris, Mac OS
Xといったデスクトップで一般的に使われるUnixシステムには備わっているので問題ないはずだ。
Windowsにはないが、そもそもWindowsには暗号論的擬似乱数生成器となるファイルというものがない。
このファイルから読み出すと暗号論的擬似乱数生成器によるランダムな内容が返ってくる。
これまたシステムに関する知識が必要な部分である。
出力
.times do |i|
pwlength= f.readbyte
c redo if c >= convlength
print(converter[c % converter.length])
end
pwlength.times
でパスワードの長さ分だけ繰り返す。
= f.readbyte c
はurandomファイルから1バイトを読み出す。IO#readbyte
は1バイトを読むのだが、返り値はその1バイトを数値で表したInteger
である。
redo if c >= convlength
先に出てきた通り、途中までで終わってしまう余りが出る部分の値である場合はやり直す。ここでnext
でループを繰り返してしまうと文字数が減ってしまうのでredo
でこのループをやり直す。
ループをやり直しても新たに1バイトを読むようになっている(f.readbyte
をもう一度通過する)から問題ない。
print(converter[c % converter.length])
これも先に出てきた通りで、c
がちゃんとすべての文字で公平な分配になる数であることが前行で保証されているので、配列の長さで割った余りを求めれば配列の添字として使える数だけが返る。
c
は/dev/urandom
で得たランダムな値であるから結果的には候補文字のうちランダムな1文字が出力される。
なお、
.times do |i|
pwlength= Random.rand convlength
c print(converter[c % converter.length])
end
と書けば/dev/urandom
を使わない(Windowsでも動作する)実装に変更できるが、
Random#rand
が使う乱数生成器はメルセンヌ・ツイスタであり、暗号論的擬似乱数生成器ではない
ので、エントロピーが下がる。
(暗号鍵としては十分でないが、パスワードとして十分かどうかは別問題)。
おまけ Perlで実装
Perlだといささかクセが強いコードになる。 その理由は、Perlの独特な部分である「スカラー型とリスト型の特性」をものすごく活用することになるためだ。
例えば、リスト変数をスカラーコンテキストで評価すると「リストの長さ」になるし、リストにはスカラー値しか格納できないので多次元配列にはならず、そのため「リストのリスト」は自動的に平坦なリストになったりする。
#!/usr/bin/perl
my $plen = int(shift);
$plen = 64 if $plen lt 1;
my @csym = split(//, shift);
my @converter;
if (0 < @csym) {
@converter = (("a" .. "z"), ("A" .. "Z"), (0 .. 9), @csym);
else {
} @converter = map { chr $_ } (33 .. 126);
}my $cloop = int(255 / @converter);
my $clen = @converter * $cloop;
open(FH, "<", "/dev/urandom") or die $!;
for (my $outlen = 0;$outlen < $plen; $outlen++) {
my $c;
read(FH, $c, 1);
$c = ord($c);
redo if $c >= $clen;
print $converter[($c % @converter)];
}
おまけ2 Luaで実装
Luaはショートハンドが少なくて、機能的にも少ないので、(私のLua力が低いのもあるにせよ) すごく素直な、というか、回りくどい、というかそういう書き方になる。
JavaScreipt同様にいわゆるHashとArrayが同一型(table)なのがLuaの特徴的なところだが、それ以上に配列としてtableを使うと1
originであるというのが混乱を招く。
あと、ライブラリに属する関数を呼ぶときは.
で、インスタンスメソッド的なものは:
になっているのも慣になる。れが必要。
#!/usr/bin/lua
pwlength = tonumber(arg[1])
converter_symbols = arg[2]
if not(pwlength) or pwlength < 1
then
pwlength = 64
end
converter={}
if converter_symbols
then
for c in converter_symbols:gmatch(".") do
table.insert(converter, c)
end
for asc=48,57 do
table.insert(converter, string.char(asc))
end
for asc=65,90 do
table.insert(converter, string.char(asc))
end
for asc=97,122 do
table.insert(converter, string.char(asc))
end
else
for asc=33,126 do
table.insert(converter, string.char(asc))
end
end
convloops = math.floor(255 / #converter)
convlength = #converter * convloops
outlength = 0
urandom = io.open("/dev/urandom", "r")
while outlength < pwlength
do
c = urandom:read(1)
c = c:byte()
if not(c >= convlength)
then
io.write(converter[(c % #converter) + 1])
outlength = outlength + 1
end
end
続編
この記事には続編がある。
おまけの宣伝
Mimir Yokohamaではプログラミングの学習に適したサービスを提供している。
家庭教師サービスは(条件は限定されるが)こうした内容をより踏み込んだものや、体系化された知識を授業として受講可能だ。
また、開発サービスに”Educational Code”オプションがあり、そのオプションはオーダーメイドのソフトウェアに、この記事のような解説がコメントとして入ったコードを入手することができる。
もちろん、これらの組み合わせも可能だ。