mikanmarusanのブログ

テクノロジーとかダイビングとか

[Ruby] シンボルとハッシュ

TL;DR

  • シンボルの特徴
    • 同じ内容のシンボルはかならず同一のオブジェクト
    • ソースコード上では文字列のように見え、内部では整数として扱われる
  • キーがシンボルの場合、 {a => b}{a: b} と書き換えることができる

概要

単に自分が Ruby の newbie なのかもしれないが、Railsでシンボルを使ってハッシュを表現するときにどのように記述すべきか迷うことがある。Hashのキーに文字列を使うかシンボルを使うかとか、「=>」(ハッシュロケット、ファットアローなどともいう)を使うか使わないかなどである。

 {"key" => "value"} 
 {:key => "value"} 
 {:key => :value}
 {key:  "value"}
 {key: :value}

がどう違うかを説明できるようになって平昌オリンピックを迎えたい。
以下、特に指定がなければ Ruby 2.5.0(2017/12/28現在 最新版)での話とする。

シンボルとは「名前という概念を表すオブジェクト」

シンボル(Symbol)は正しくは Symbolクラスのオブジェクト。シンボルオブジェクトは、:symbol のようなリテラルで表現することができる。

マニュアルを要約すると、

  • ソースコード上では文字列のように見え、内部では整数として扱われる、両者を仲立ちするような存在
    • Rubyの内部実装では、メソッド名や変数名、定数名、クラス名など の名前を 整数 で管理していて、その整数をRubyのコード上で文字列のように表現したもの
  • 同じ内容のシンボルはかならず同一のオブジェクトになる

ということである。一言で言うと「名前という概念を表す整数オブジェクト」というところか。

同じ内容のシンボルはかならず同一のオブジェクト(同一のobject_id)

マニュアルによれば、「同じ内容のシンボルはかならず同一のオブジェクト」 ということである。これを実際に確認してみる。文字列の場合と比較するため、同じ内容のシンボルの場合と同じ内容の文字列の場合でそれぞれ比較する。

sym1 = :symbol
sym2 = :symbol

str1 = "string"
str2 = "string"

p sym1.object_id
p sym2.object_id

p str1.object_id
p str2.object_id
[実行結果]
# 同じ内容のシンボルは同一のオブジェクトになる(=object_idが同一)
384488
384488
# 同じ内容の文字列は異なるオブジェクトになる(=object_idが異なる)
70167279378560
70167279378540

この結果から、シンボルは、プログラム上では文字列にように表現しているが、内部では整数(object_id)として扱われていると言える。ここがマニュアルでいう ソース上では文字列のように見え、内部では整数として扱われる、両者を仲立ちするような存在 につながる。だからメモリ効率もよいし、(整数比較となるので)比較が高速になるということなのだろう。

シンボルのシンタックスシュガー

シンボルオブジェクトは、以下のようなリテラルでも取得することができる。以下は全て :symbol というシンボルになり全て同じ値になる(記法の違いのことをシンタックスシュガーと呼ぶ)。

p :symbol
p :"symbol"
p "symbol".to_sym
p %s(symbol) # %記法
[実行結果]
:symbol
:symbol
:symbol
:symbol

シンボルを使うとよいケース

「同じ内容のシンボルはかならず同一のオブジェクト」 という特徴を利用すると、複数あるとよろしくないものを作るときに向いている。例えば、ハッシュのキーとしてシンボルを使うことでキーに一意性を持たせることができる。

また、ソースコード上では文字列のように見え、内部では整数として扱われる」 という特徴を利用すると、C言語の列挙型(enum)のように、プログラムからは名前でアクセスする整数リスト(値は別に何であっても構わない)の使い方が想像できる。

Railsでは、ActiveRecord::Enum を使って列挙型を表現することができる。

class Membership < ActiveRecord::Base
  enum gender: [:female, :male, :other, :rather_not_say]
end

ハッシュアローを使うとき、使わないとき

マニュアルのHashクラスを見るとこう書かれている。

{a => b, ... } # aはキー、bは値となる
{s: b , ... } # { :s => b, ... } と同じ。キーがシンボルの場合の省略した書き方

ということは、下記の2つは記法の違いなので、ハッシュとしては同一となる。

 {:key => "value"} 
 {key:  "value"}

また、下記も記法の違いで同一である。

 {:key => :value}
 {key:  :value} 

念のため下記のサンプルコードで確かめてみる。

h1 = {"key" => "value"}
h2 = {:key => "value"}
h3 = {:key => :value}
h4 = {key:  "value"}
h5 = {key: :value}

p h1 === h2 # false
p h1 === h3 # false
p h1 === h4 # false
p h1 === h5 # false
p h2 === h3 # false
p h2 === h4 # true
p h2 === h5 # false
p h3 === h4 # false
p h3 === h5 # true
p h4 === h5 # false

{:key => "value"}{key: "value"} 、そして {:key => :value}{key: :value} が同じであることが確認できた。

まとめると、キーがシンボルの場合に限り、=> (ハッシュロケット)記法以外に、 : 記法(シンボルを表す前コロンは書かず、キーと値の間にコロンを置く)で表現できる。いいかえると、キーがシンボルの場合 {a => b}{a: b} と書き換えることができる ということだ。

Hashのキーがシンボルでも文字列でもアクセスできる

rubyのハッシュは、下記のサンプルの通り、キーがシンボルか文字列かを判別する。

hash = {key: "value"}
hash[:key]  #=> "value"
hash["key"] #=> nil

hash = {"key" => "value"}
hash[:key]  #=> nil
hash["key"] #=> "value"

だけど、Rails の Controller において リクエスト情報(クエリパラメータやPOSTで送られてくるデータ)が格納される params については、シンボルと文字列のどちらを指定しても同じ値が取得できる。

class ExamplesController < ApplicationController
  def index
    p params[:action]  # => 'index'
    p params['action'] # => 'index'
  end
end

これは、前回のエントリとど同様、Railsのコア拡張(ActiveSupport::CoreExtenstions::Hash)に含まれる、Hash#with_indifferent_access効果である。
中身としては比較的単純で、キーがシンボルだったら文字列に変換するようにしてあるだけだ。

参考までに、キーがシンボルでも文字列でもアクセスできる、Hashクラスを継承したHash2クラスの疑似コードを書く。

# Hash2クラス
class Hash2 < Hash
  def convert_key(key)
    key.kind_of?(Symbol) ? key.to_s : key
  end

  def [](key)
    super(convert_key(key))
  end

  def initialize(constructor = {})
    if constructor.respond_to?(:to_hash)
      super()
      update(constructor)

      hash = constructor.to_hash
      self.default = hash.default if hash.default
      self.default_proc = hash.default_proc if hash.default_proc
    else
      super(constructor)
    end
  end
end
# Hash2を使ったテスト
h = Hash2.new("key" => "value")
p h["key"]  # => 'value'
p h[:key] # => 'value'

TL;DR(再掲)

  • シンボルの特徴
    • 同じ内容のシンボルはかならず同一のオブジェクト
    • ソースコード上では文字列のように見え、内部では整数として扱われる
  • キーがシンボルの場合、 {a => b}{a: b} と書き換えることができる
  • 2017年も終わり、良いお年を!

ハッシュスライス(Hash#slice)

2017/12/25にRuby2.5.0がリリースされた。 Ruby2.5.0 から 待望の Hash#slice が 標準メソッドになったのを記念してエントリを書いている。

ハッシュスライス

ハッシュスライスとは、指定したキーに合致するキーと要素からなる、新しいハッシュを作ることである。

Rails用のライブラリを提供しているので、Railsでアプリケーションをサンプルでいくつか書くのだけれども、ハッシュから特定のキーだけを持つハッシュを作ることがある。このような場合にはハッシュスライスが便利。例えば Rails の Controller において リクエスト情報(クエリパラメータやPOSTで送られてくるデータ)が格納される params について使いたいキーのみ残して引き回したいときなどだ。

Railsではこれが簡単にできる。
Railsのコア拡張(ActiveSupport::CoreExtenstions::Hash::Slice)のおかげで

{ a: 1, b: 2, c: 3, d: 4 }.slice(:a, :b)
# => {:a=>1, :c=>3}

な風に描ける、便利すぎて泣ける。
Railsは本当にエンジニアをダメにしてくれる素晴らしいメソッドを用意してくれているとよく思う。
これを 標準Ruby で実行すると、Ruby2.5.0以前であれば、アレ動かない?となる。

ちなみに irb上記メソッドを実行してみればわかるが、NoMethodErrorで怒られる。

% irb          
irb(main):001:0> { a: 1, b: 2, c: 3, d: 4 }.slice(:a, :b)
NoMethodError: undefined method `slice' for {:a=>1, :b=>2, :c=>3, :d=>4}:Hash
    from (irb):1
    from /usr/bin/irb:12:in `<main>'

じゃRuby2.5.0以前はどのようにやればいいかというと、 selectメソッドを組み合わせて下記のように記述することが(少なくとも自分は)多かった。ただし直感的じゃないので1週間後ぐらいにリファクタリングする自分に自信が持てない、つらみ。

sliced = { a: 1, b: 2, c: 3, d: 4 }.select { |k,_| [:a, :c].include? k }
# => {:a=>1, :c=>3}

ということでRuby2.5.0でのHash::sliceを試してみる。

Ruby 2.5.0 の インストール

念のためインストール手順の記載。rbenvで楽勝。

# rubyのバージョンを確認する、古い
$ ruby -v
ruby 2.3.1p112 (2016-04-26) [x86_64-linux-gnu]
# rbenvでインストール可能なバージョンを調べる、2.5.0は大丈夫そう
$ rbenv install --list | grep 2.5.0
  2.5.0-dev
  2.5.0-preview1
  2.5.0-rc1
  2.5.0
  rbx-2.5.0
# ruby 2.5.0のインストール
$ rbenv install -v 2.5.0
# インストールできたかを確認し、ruby 2.5.0 に切り替える。globalが怖い場合はlocalでも可
$ rbenv versions
* system (set by /home/ubuntu/.rbenv/version)
  2.5.0
$ rbenv global 2.5.0
$ rbenv versions
  system
* 2.5.0 (set by /home/ubuntu/.rbenv/version)
# rubyのバージョン確認をする、できた
$ ruby -v
ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux]

Hash#sliceをためしてみる

Hash#sliceでシンプルに記載できるようになる。

# 対象のハッシュ
hash = {foo: 1, bar: 2, baz: 3}

# :fooと:bazのみを取り出したい
sliced = hash.slice(:foo, :baz)
pp sliced
# [実行結果]
{:foo=>1, :baz=>3}

これで JSON とか YAML のデータを読み込んでハッシュで保持しているときに、必要なキーのデータだけにするとかが簡単に出来そうで素敵。

(番外編)perl のハッシュスライス

perl には以前からハッシュスライスは存在していて、特定のキーの値をリスト形式で取得できるハッシュスライス @hash{... }; が有名であるが、キーと値のペアではないので、 RubyHash#slice とは結果が異なる。

my %hash = (foo => 1, bar => 2, baz => 3);
my($foo, $baz) = @h{qw/foo baz/};

perl 5.20 からは 新たなスライス表記として、キーと値のペアを戻す「%hash{…}」というスライス表記が加わっているので、RubyHash#slice と同じ結果となる。

#!/usr/bin/perl

use strict;
use warnings;

my %hash = (foo => 1, bar => 2, baz => 3);

# キーと値のペアを戻すハッシュスライス%hash{…}
my %sliced =  %hash{('foo', 'baz')};

perlでもrubyでもハッシュスライスができて万歳。

まとめ

Ruby 2.5.0 からの Hash#slice はイイ。

PHP7.2 から導入された パスワードハッシュ関数のハッシュアルゴリズム Argon2 を使ってみた

概要

2017/11/30にPHP7.2がリリースされた。
PHP5.5.0 から導入されたパスワードハッシュ関数を使ってみた の更新から3年。PHP7.2でハッシュアルゴリズム Argon2 が追加されたので調べてみた。

TL;DR

Argon2

Argon2は、2015年の Password Hashing Competition のWinnerになったパスワードハッシュアルゴリズムで、クリエイティブ・コモンズのCC0 と Apache 2.0 のデュアルライセンスで公開 されている。

で、論文がこれ。 Argon2: the memory-hard function for password hashing and other applications

実際には Argon2dとArgon2iという2つのアルゴリズムで構成されているが、password_* のアルゴリズムとして採用されているのはArgon2iの方。Argon2iはサイドチャネル攻撃(side-channel timing attacks)耐性があり、Argon2dはGPUを使った攻撃に対する耐性がある。

ハッシュアルゴリズムが使うメモリコスト(メモリ使用量)と時間コスト(回数)と並列度(ハッシュ化時に利用するCPUスレッドの数)をパラメータとして設定し、サーバーサイドアプリケーションの負荷に応じてハッシュ強度を設定できるアルゴリズムのようだ。そのパラメータの設定値は定数として定義されていて

# Argon2で定義されている定数を覗いてみる
<?php
        # メモリコスト(メモリ使用量; 単位 KiB)
        echo PASSWORD_ARGON2_DEFAULT_MEMORY_COST . PHP_EOL;
        # 時間コスト(回数)
        echo PASSWORD_ARGON2_DEFAULT_TIME_COST   . PHP_EOL;
        # 並列度(CPUスレッド数)
        echo PASSWORD_ARGON2_DEFAULT_THREADS     . PHP_EOL;
# 実行結果
1024
2
2

な感じ。

php.netのマニュアル によれば、メモリコストと時間コストについて下記のように書かれているが、

  • memory_cost (integer) - Argon2 ハッシュの計算に用いるメモリの最大値 (バイト数) を設定します。 デフォルトは PASSWORD_ARGON2_DEFAULT_MEMORY_COSTです。

  • time_cost (integer) - Argon2 ハッシュの計算にかける時間の最大値を設定します。 デフォルトは PASSWORD_ARGON2_DEFAULT_TIME_COSTです。

PHP7.2が実際にrequireしているライブラリ のコードを紐解くと、メモリコストの単位はKiB(キビバイト)で、時間コストは(時間という概念ではなく)イテレーション数ということなので、マニュアルは少し誤植があるのかもしれない。

API一覧

APIについては password_* が公開されたPHP5.5から大きく変更されていない。 したがってArgon2の利用方法と注意点だけをかいつまんで説明することにする。

password_hash

まずはハッシュ生成関数。

string password_hash ( string $password , integer $algo [, array $options ] )

第1引数 $password は生パスワード、第2引数 $algo はハッシュアルゴリズム定数、第3引数は $options アルゴリズムがサポートするオプションを入力する。ハッシュアルゴリズムをArgon2をするだけであれば、$algoに PASSWORD_ARGON2I を指定するだけでよい。

<?php
    $raw_passwd = '3kanmarusan_Passw0rd';

    $hashed_passwd = password_hash($raw_passwd, PASSWORD_ARGON2I);
# 生成されたハッシュ
$argon2i$v=19$m=1024,t=2,p=2$SEJ2M2VNL3B5MXZNN0tDYg$44a3kX0OxiFzoy4bCtkHnAyy7/TVkvIVU7kdyj8ewCg

$argon2iで始まっているので、Argon2っぽい。$m=1024,t=2,p=2がハッシュ化時に使用しているパラメータ。これはPHPで定義されたデフォルト値そのものである(前述)。パラメータは連想配列で指定することができる。

<?php
    $hashed_passwd = password_hash($raw_passwd, PASSWORD_ARGON2I, ['memory_cost' => 4096, 'time_cost' => 8, 'threads' => 4]);
# 生成されたハッシュ
$argon2i$v=19$m=4096,t=8,p=4$ejcvNmtJL3J1R2kwQWRkVA$Z1QBd6MShZmK+hIGGXfOQx1tq1djPjt8yJC1RDh//6Y

生成されたパスワードハッシュにも $m=4096,t=8,p=4 とあるので指定通りになっている。

ちなみに、ハッシュアルゴリズムのデフォルト(PASSWORD_DEFAULT)に変更があるか調べたところ、PHP7.2の段階では変更がないようだ。

# PASSWORD_DEFAULTはまだbcryptということを調べる
<?php
  echo "[constants]" . PHP_EOL;
  echo " PASSWORD_BCYRPT: "  . PASSWORD_BCRYPT  . PHP_EOL;
  echo " PASSWORD_ARGON2I: "  . PASSWORD_ARGON2I  . PHP_EOL;
  echo " PASSWORD_DEFAULT: " . PASSWORD_DEFAULT . PHP_EOL;
# 実行結果
[constants]
 PASSWORD_BCYRPT: 1
 PASSWORD_ARGON2I: 2
 PASSWORD_DEFAULT: 1

PHP RFCのプロセスによれば、PHP7.4でハッシュアルゴリズムのデフォルトをArgon2に変更しようとする提案があったようが、今回のRFCでは取り下げられたようだ。

[Resolved] Inclusion on 7.4
Per discussion on the internals mailing list during an initial vote, this RFC no longer proposes changes to PASSWORD_DEFAULT in 7.4.

password_get_info

先ほどの説明の通り、password_hashは、アルゴリズムやコスト、ソルトといった情報もハッシュに含めて返す。password_get_infoは、password_hashで生成した有効なハッシュを第1引数$hashで渡すと、ハッシュに関する情報の配列を返すメソッド。bcryptの時とインタフェースに変更はないが、戻り値の options が少し変わる。

array password_get_info ( string $hash )

先ほどArgon2で生成したハッシュ $argon2i$v=19$m=4096,t=8,p=4$ejcvNmtJL3J1R2kwQWRkVA$Z1QBd6MShZmK+hIGGXfOQx1tq1djPjt8yJC1RDh//6Y について確認してみる。

<?php
        # password_hash で生成した有効なハッシュ(Argon2) 
        $hash = '$argon2i$v=19$m=4096,t=8,p=4$ejcvNmtJL3J1R2kwQWRkVA$Z1QBd6MShZmK+hIGGXfOQx1tq1djPjt8yJC1RDh//6Y';
        $info = password_get_info($hash);
        print_r($info);
Array
(
    [algo] => 2
    [algoName] => argon2i
    [options] => Array
        (
            [memory_cost] => 4096
            [time_cost] => 8
            [threads] => 4
        )
)

algo の値が、PASSWORD_ARGON2I(=2) を指しており、options にハッシュ化時に使用したパラメータが表示されていることがわかる。

password_verify

ここは変更なし

password_needs_rehash を使ってパスワードハッシュアルゴリズムをアップデートする

パスワードをハッシュで保存しているときに、パスワードハッシュの強度(アルゴリズムやコスト、ソルトやストレッチングの回数など)を変更したい場合がある。PHP7.1まではアルゴリズムが bcrypt のみだったのでコストやソルトの変更しかできなかったが、PHP7.2からはアルゴリズムが追加されたのでハッシュアルゴリズムが変更できるか試してみる。

なお、ハッシュで保存している=生パスワードを知り得ないので、ハッシュ強度の変更をオフラインで実行することはできない。したがって、ユーザーの認証が成功した時に(生のパスワードが分かっているので)オンラインでハッシュの変更(rehash)をする。

下記は、bcryptアルゴリズムでハッシュ化されて保存されているパスワードハッシュに対して、認証成功後にArgon2アルゴリズムで再ハッシュする疑似コードである。

<?php
        $raw_passwd = '3kanmarusan_Passw0rd'; // ユーザーの入力
        $hashed_passwd = '$2y$10$fAO/bg1Ti9.yBM3wC3FyJeIfrIql9dVFx/dhTDZO.FjSSjylRRCLK'; // DBなどから引っ張ってきたもの(これはbcrypyでハッシュ化されている)

        // まず認証しようず!
        if(password_verify($raw_passwd, $hashed_passwd)) {
                // パスワード認証成功。
                if(password_needs_rehash($hashed_passwd, PASSWORD_ARGON2I)) {
                        // rehashが必要
                        $new_hashed_passwd = password_hash($raw_passwd, PASSWORD_ARGON2I);

                        // (新しいハッシュをDBに格納)
                        // ...
                }
        } else {
        // パスワード認証失敗
     }

TL;DR(再掲)

PHP5.5.0 から導入されたパスワードハッシュ関数を使ってみた

概要

パスワードのハッシュ
パスワードハッシュ API は crypt() を手軽に使えるようにしたラッパーで、 パスワードの作成や管理を安全な方法で行えます。

crypt() を利用すればパスワードハッシュの生成は可能なんだけど、経験上煩雑なコードになりやすい気がして(自分がPHPを書くのが下手なだけかもしれないけど)ちょっと勉強してみた。

環境構築

vagrantUbuntu(raring)を用意し、php5.5.7をLaunchpadのPersonal Package Archiveでインストール。

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 13.04
Release:    13.04
Codename:   raring

$ php -v
PHP 5.5.7-1+sury.org~raring+1 (cli) (built: Dec 12 2013 21:50:03) 

API一覧

まずは一覧から。4つのメソッドが公開されているので順に説明していく。

  • password_hash : パスワードハッシュを作る
  • password_get_info : 指定したハッシュに関する情報を返す
  • password_verify : パスワードがハッシュにマッチするかどうかを調べる
  • password_needs_rehash : 指定したハッシュがオプションにマッチするかどうかを調べる

password_hash

まずは hash 生成関数、最初にしてメイン。

string password_hash ( string $password , integer $algo [, array $options ] )

第1引数 $password は生パスワード、第2引数 $algo はハッシュアルゴリズム定数、第3引数は $options アルゴリズムがサポートするオプションを入力する。PHP5.5では PASSWORD_DEFALUT と PASSWORD_BCRYPT が同じ値を指しており、共にBCRYPTが利用される模様。本当かな?と思って調べてみる。

# PASSWORD_DEFALUT と PASSWORD_BCRYPTの定数を覗いてみる
<?php
  echo "[constants]" . PHP_EOL;
  echo " PASSWORD_BCYRPT: "  . PASSWORD_BCRYPT  . PHP_EOL;
  echo " PASSWORD_DEFAULT: " . PASSWORD_DEFAULT . PHP_EOL;
# 実行結果
[constants]
 PASSWORD_BCYRPT: 1
 PASSWORD_DEFAULT: 1

ということで、2つのアルゴリズムを利用して(同じだけど)生のパスワードのハッシュを作ってみる。

<?php
    $raw_passwd = '3kanmarusan_Passw0rd';

    echo 'PASSWORD_DEFAULT' . PHP_EOL;
    $hashed_passwd = password_hash($raw_passwd,
                                   PASSWORD_DEFAULT);
    echo ' hash:' . $hashed_passwd . PHP_EOL;

    echo 'PASSWORD_BCRYPT(cost12)' . PHP_EOL;
    $hashed_passwd = password_hash($raw_passwd,
                                   PASSWORD_BCRYPT,
                                   ['cost' => 12]); // コストも変えてみる
    echo ' hash:' . $hashed_passwd . PHP_EOL;
# 実行結果
PASSWORD_DEFAULT
 hash:$2y$10$fAO/bg1Ti9.yBM3wC3FyJeIfrIql9dVFx/dhTDZO.FjSSjylRRCLK
PASSWORD_BCRYPT(cost12)
 hash:$2y$12$OciCNOfWegjXIgyNIoO/ZOPLt6wPLWcnYTYFwmAGoN6.2dG/4itZi

$2yで始まっているので、共にBCRYPTっぽい。その次の$10とか$12がコストを表すので PASSWORD_DEFAULT はcost10のBCRYPTということになる。

ちなみに、パスワードハッシュ生成方法そのものは、phpソースコードgithub.com からダウンロードして確認することができる。300MB超えのためワイヤレス環境だと厳しい。

git clone https://github.com/php/php-src.git
cd git-src
git checkout php-5.5.7
cd ext/standard
vim password.c

少々ソースを読んでみると、デフォルトのアルゴリズムとなる PASSWORD_BCRYPT は、

  • ハッシュ文字列の最初の7バイトでアルゴリズムとコストを表す
  • /dev/urandom を利用して乱数ビット列を作り、Base64 に変換した最初の22バイトをソルトデータとする。
  • BLOWFISH アルゴリズムを利用して、ソルト+ストレッチング内蔵の60バイトのパスワードハッシュを生成。
  • crypt内部の挙動としては、実際は最初の7バイトとソルトデータの22バイトを合わせた29文字をソルトとしてハッシュ計算する模様(この辺りよくわかっていない)

というロジックになっているようだ(ざっくり)。

PASSWORD_DEFAULT と PASSWORD_BCRYPT の使い分け

password_hash には下記のことが書かれている。

  • PASSWORD_DEFAULT - bcrypt アルゴリズムを使います (PHP 5.5.0 の時点でのデフォルトです)。 新しくてより強力なアルゴリズムPHP に追加されれば、 この定数もそれにあわせて変わっていきます。 そのため、これを指定したときの結果の長さは、変わる可能性があります。 したがって、結果をデータベースに格納するときにはカラム幅を 60 文字以上にできるようなカラムを使うことをお勧めします (255 文字くらいが適切でしょう)
  • PASSWORD_BCRYPT - CRYPT_BLOWFISH アルゴリズムを使ってハッシュを作ります。これは標準の crypt() 互換のハッシュで、識別子 "$2y$" を使った場合の結果を作ります。 その結果は、常に 60 文字の文字列になります。失敗した場合に FALSE を返します。

こう書かれると、メンテナンスコストも考えると PASSWORD_DEFAULT を選ぶのが怖い気もするけれども、マニュアルの下の方にアルゴリズムの更新/追加/デフォルト化のリリースルールが書かれている。

注意: この関数がサポートするアルゴリズムの更新 (あるいはデフォルトのアルゴリズムの変更) は、必ず次の手順にのっとって行われます。

  • 新しく追加されたアルゴリズムがデフォルトになるまでには、 少なくとも一度は PHP のフルリリースを経ること。 つまり、たとえば、新しいアルゴリズムが 5.5.5 で追加されたとすると、 そのアルゴリズムがデフォルトになるのは早くても 5.7 以降ということになります (5.6 は、最初のフルリリースだからです)。 しかし、もし別のアルゴリズムが 5.6.0 で追加されたとすると、 そのアルゴリズムも 5.7.0 の時点でデフォルトになる資格を得ます。
  • デフォルトを変更できるのはフルリリース (5.6.0 や 6.0.0 など) のときだけで、リビジョンリリースでは変更できない。 唯一の例外は、現在のデフォルトにセキュリティ上の致命的な欠陥が発覚した場合の緊急リリースです。

先週(2014/1/23)、PHP5.6.0alpha1がリリースされたけれども、

  • パスワードハッシュAPIには大きな変更が無さそうなのでデフォルトが変更されるのはもう少し時間がかかりそう
  • このまま行けばPHP5.6ではアルゴリズム変更は無いので、少なくとも次のフルリリースとなる(であろう)PHP5.7を経て、PHP5.8まで変更はないと期待できる
  • パスワードハッシュAPIアルゴリズム更新ルールが明確
  • (後述しますが)パスワードハッシュそのものにアルゴリズムやコスト、ソルトなどが含まれており、異なるハッシュアルゴリズムが混在していもパスワードの検証は出来る。

を考えると、PASSWORD_DEFAULT でもあまり問題にならないのではないかと思っている(個人の見解です)。

とはいえ、アルゴリズムはどんどん強固なものに更新していく必要もあり、それをサポートする後述の機能(password_needs_rehash)も提供されていることを考慮すれば、少なくともハッシュをデータベース等のストレージに格納する場合は、カラム長にゆとりを持たせておくことは大事ですね(玉虫色の回答)。

BCRYPTのハッシュコスト

PASSWORD_BCRYPT のハッシュコストのデフォルトは10となっているが、先ほど12の場合を例示した。もちろんコストが高いほど強固になるけれども、ハードウェアにそれ相応の負荷をかけることになる。php.netには、ハッシュ計算にかかる時間から逆算して適切なコストを推定するプログラムが例示されていたうのでそれを紹介する。

# password_hash() で、適切なコストを探す例
<?php
$timeTarget = 0.2; 

$cost = 9;
do {
    $cost++;
    $start = microtime(true);
    password_hash("test", PASSWORD_BCRYPT, ["cost" => $cost]);
    $end = microtime(true);
} while (($end - $start) < $timeTarget);

echo "Appropriate Cost Found: " . $cost . "\n";

1.7GHz Core i5/4GBのMBAubuntu-13.04 を仮想環境で動かした場合は、ハッシュ計算を0.2秒ぐらいにすると、適切なコストは 12 ということになった。もちろんこの辺りは環境によって調整すべき。

password_get_info

先ほどの説明の通り、password_hashは、アルゴリズムやコスト、ソルトといった情報もハッシュに含めて返す。password_get_infoは、password_hashで生成した有効なハッシュを第1引数$hashで渡すと、ハッシュに関する情報の配列を返すメソッド。

array password_get_info ( string $hash )

先ほどBCRYPTのcost12で生成したハッシュ $2y$12$OciCNOfWegjXIgyNIoO/ZOPLt6wPLWcnYTYFwmAGoN6.2dG/4itZi について確認してみる。

<?php
    # password_hash で生成した有効なハッシュ(BCRYPT/cost12)
    $hash = '$2y$12$OciCNOfWegjXIgyNIoO/ZOPLt6wPLWcnYTYFwmAGoN6.2dG/4itZi';
    $info = password_get_info($hash);
    print_r($info);
Array
(
    [algo] => 1
    [algoName] => bcrypt
    [options] => Array
        (
            [cost] => 12
        )
)

algo の値が、PASSWORD_BCRYPT(1) を指しており、options にコストが格納されていることが分かる。

password_verify

password_verifyは、指定したパスワードハッシュが生のパスワードとマッチするかどうか検証するメソッド。password_get_info でも説明した通り、パスワードハッシュの検証に必要なアルゴリズムやコスト、ソルトの情報もハッシュに含まれているので簡単に検証できる(他のデータソースから用意し・・・といった面倒なことがない)。

boolean password_verify ( string $password , string $hash )

第1引数 $password にユーザーから入力された生パスワードを、第2引数 $hash にストレージ等に格納しておいたパスワードハッシュを指定すると、パスワードハッシュ値アルゴリズムやコストにしたがってパスワードとそのハッシュが一致するかを検証する。

<?php
        $hash = '$2y$12$OciCNOfWegjXIgyNIoO/ZOPLt6wPLWcnYTYFwmAGoN6.2dG/4itZi';

        $correct_password = '3kanmarusan_Passw0rd'; // みかんまるさん
        var_dump(password_verify($correct_password, $hash));

        $incorrect_password = '2kanmarusan_Passw0rd'; // にかんまるさん?
        var_dump(password_verify($incorrect_password, $hash));
# 実行結果
bool(true)
bool(false)

password_hash で生成したパスワードハッシュにはアルゴリズムやコスト、ソルトがそれぞれ含まれているので、異なるハッシュアルゴリズムが混在しても問題なくパスワードの検証ができる。

password_needs_rehash

パスワードをハッシュで保存しているときに、パスワードハッシュの強度(アルゴリズムやコスト、ソルトやストレッチングの回数など)を変更したい場合がある。ハッシュで保存している=生パスワードを知り得ないので、ハッシュ強度の変更をオフラインで実行することはできない。

したがって、ユーザーの認証が成功した時に(ここで生のパスワードが分かるので)オンラインでハッシュの変更(rehash)をする必要が出てくる。これをうまく出来るようにするのが password_needs_rehash メソッド。

boolean password_needs_rehash ( string $hash , string $algo [, string $options ] )

第1引数$hashがチェック対象となるハッシュ。第2引数は$algoはハッシュアルゴリズムで、第3引数$optionsには、第2引数のハッシュアルゴリズムがサポートするオプションを指定する。第1引数の$hashアルゴリズムやオプションが第2引数以降とマッチしなかった場合は再ハッシュが必要ということで true が返却される。言葉にするとよく分からなくなるので書いてみる。同じ事を2度書くとかイケていないけれども、ここは分かりやすさ重視で書いてます。

<?php
    # BCRYPT, cost10(デフォルト)で生成したハッシュ
    $hash = '$2y$10$fAO/bg1Ti9.yBM3wC3FyJeIfrIql9dVFx/dhTDZO.FjSSjylRRCLK';

    if(password_needs_rehash($hash, PASSWORD_BCRYPT, ['cost' => 12])) {
        echo "need rehash" . PHP_EOL;
    } else {
        echo "not need rehash" . PHP_EOL;
    }

    # BCRYPT, cost12で生成したハッシュ
    $hash = '$2y$12$OciCNOfWegjXIgyNIoO/ZOPLt6wPLWcnYTYFwmAGoN6.2dG/4itZi';

    if(password_needs_rehash($hash, PASSWORD_BCRYPT, ['cost' => 12])) {
        echo "need rehash" . PHP_EOL;
    } else {
        echo "not need rehash" . PHP_EOL;
    }
# 実行結果
need rehash
not need rehash

上記の例の場合、はじめのハッシュはcost10のハッシュなのでrehashが必要と判定され、2番目のハッシュはcost12のハッシュなのでrehashは不要だと判定できている。

password_verify でも説明した通り、パスワードハッシュそのものにアルゴリズムやコスト、ソルトなどが含まれており、異なるハッシュアルゴリズムが混在していもパスワードの検証は出来るが、パスワードの強度は常に最新にしていく必要がある。下記の疑似コードで実現できると思う(試してない)。

<?php
    $raw_passwd = '(なにか)'; // ユーザーの入力
    $hash = '(なにか)'; // DBなどから持ってくる

    // まず認証しようず!
    if(password_verify($raw_passwd, $hash)) {
        // パスワード認証成功
        if(password_needs_rehash($hash, PASSWORD_BCRYPT, ['cost' => 12])) {
            // rehashが必要
            $new_hash = password_hash($raw_passwd,
                                      PASSWORD_BCRYPT,
                                      ['cost' => 12]);

            // (新しいハッシュをDBに格納)
        }
    } else {
        // パスワード認証失敗
    }

まとめ

  • PHP5.5から採用されたパスワードハッシュAPIは便利
  • ソルト+ストレッチングを内蔵したパスワードハッシュが作れて再ハッシュも簡単

vagrant + fabric + cuisineで開発環境を自動生成する

はじめに

9/28のDevOps Days Tokyo 2013に参加してきた。

Vagrantをつかってみた というエントリだけでやりっ放しにするのはかっこよくないし、ブログを書くまでがDevOps Daysと幹事の方も言っていたので(意味が違う)、vagrant + fabric + cuisineを使って開発環境を自動生成してみる。

DevOps Days TokyoではChefの話が多かったのだけど、こんなエントリ書いておきながらRubyはまだしっくりきていないし、Pythonのシンプルさが好みなので、Chef-SoloっぽいことができるPythonのライブラリを探してみると fabric というものがあるらしい。
そして、Chefといえば冪等性(何回同じ操作をしても同じ結果になること)だけど、cuisine を組み合わせれば冪等性が少しは担保できるらしい。

目的

  • Mac Book Air上に Ubuntu-12.04 LTSの仮想環境が5分で導入できること
    • AWSでもUbuntuを使っているので将来見据えて
  • git clone + vagrant up の2コマンドで完了できること
  • ついでだから Apache + Python (WSGI) でサンプルが動いていると面白い

やってみた

下準備

まずは、Virtual Box/vagrantの確認をしておく。以前、vagrantはruby gemでインストールしていたけれども、いまは Vagrant Downloads からダウンロードすればいい、楽チン。

Virtual Box: 4.2.0
vagrant: 1.3.3

Ubuntu自体は用意されているboxをそのまま利用

$ vagrant box add ubuntu-12.04 http://files.vagrantup.com/precise64.box
$ vagrant box list
ubuntu-12.04

fabricとcuisineのインストール

pipが入ってなければ、 sudo easy_install pip でインストールする。
easy_installだとインストールパッケージ一覧を出すのがしんどいし、pipの方が今風だからこっちにする。

$ sudo pip install fabric
$ vagrant plugin install vagrant-fabric
$ sudo pip install cuisine

Vagrantfile の記載

srcとvagrantディレクトリを作り、vagrantディレクトリの中で vagrant init <box名> を実行する。実行するとVagrantfileができる。

$ mkdir src vagrant (srcは後述のsynced_folderのところで使い道がある)
$ cd vagrant
$ vagrant init ubuntu-12.04

以下のようにVagrantfileを書き換える

# -*- mode: ruby -*-
# vi: set ft=ruby :

VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
    # box名
    config.vm.box = "ubuntu-12.04"
    # ローカル環境でIPが使えるようにする
    config.vm.network :private_network, ip: "192.168.33.10"
    # synced_folder (親ホストのVagrantfileから見た時の ../src を 仮想環境の /src に割り当てる
    config.vm.synced_folder "../src", "/src", create: true, owner: 'vagrant', group: 'vagrant', mount_options: ['dmode=777,fmode=666']

    # プロビジョニング(パッケージの追加や設定の反映)
    # provision.py に記載した setup メソッドを実行する
    config.vm.provision :fabric do |fabric|
        fabric.fabfile_path = "./provision.py"
        fabric.tasks = ["setup"]
    end
end
synced_folder

親ホストのディレクトリを仮想ホストのディレクトリを共有する仕組み。
開発は親ホスト上のお好きなエディタやIDEで実施し、仮想サーバで実行することができる。

  config.vm.synced_folder "../src", "/src", create: true, owner: 'vagrant', group: 'vagrant', mount_options: ['dmode=777,fmode=666']

例えば前述のVagrantfileに記載した内容の場合、下記のような共有ディレクトリが生成される。

  • 親ホストの ../src ディレクトリ(Vagrantfileからみて) と 仮想サーバの /src を共有する
  • 仮想サーバに /src が存在しなければ生成する(create: trueのところ)
  • ディレクトリオーナーとグループは vagrant にし、パーミッションは777、生成されるファイルのパーミッションは666になる。

ちなみにVagrantfileが設置されるディレクトリは自動的に synced_folder となり仮想サーバの方では /vagrant というディレクトリで参照できる。
なお、synced_folder については、下記記事が分かりやすかった。

プロビジョニングを記載する

Vagrantfileのコメントにも記載している通り、provision.py にプロビジョニングを記載する。
cuisine の *_ensure あたりを使うと冪等性が担保できるし、file I/Oをうまく使えばかなりの割合で冪等なプロビジョニングができると想像できる(sudoでコマンドを流すのは便利だが冪等性を担保しにくくなる。)

from fabric.api import run
from fabric.api import sudo
from fabric.utils import puts
from fabric.colors import red, green
from fabric.context_managers import *

import cuisine

cuisine.select_package("apt")

def setup():
        _setup_ubuntu()
        _setup_devtools()
        _setup_packages()
        _configuration_apache2()
        _restart_daemons()

# 初期設定系(なんとなく)
def _setup_ubuntu():
        puts(green('Setting Operation System'))
        sudo("cp /usr/share/zoneinfo/Japan /etc/localtime")
        sudo("apt-get update")

# 開発パッケージ系
# cuisine.package_ensure がいい感じ
def _setup_devtools():
        puts(green('Installing Devtools'))
        cuisine.package_ensure('vim')
        cuisine.package_ensure('python-setuptools')

# アプリケーション
def _setup_packages():
        puts(green('Installing Packages'))
        cuisine.package_ensure('libapache2-mod-wsgi')
        cuisine.package_ensure('apache2-mpm-worker')

# Apache2の設定
# 折角なので Apache設定ファイルを file_write で書き込み
# 設定ファイルの有効化とreloadをfabricのsudoで実行してみた
def _configuration_apache2():
        if not cuisine.file_exists('/etc/apache2/sites-enabled/hello'):
                cuisine.file_write(
                                location = '/etc/apache2/sites-available/hello',
                                content  = "WSGIScriptAlias /hello /src/hello.py"
                   "\n"
                   "<Directory /src>\n"
                   "  SetHandler wsgi-script\n"
                   "\n"
                   "  Order deny,allow\n"
                   "  Allow from all\n"
                   "</Directory>\n",
                                mode=None,
                                owner=None,
                                group=None,
                                sudo=True

                )
                sudo('a2ensite hello')
                sudo('/etc/init.d/apache2 reload')

# デーモンの再起動
def _restart_daemons():
        puts(green('Restarting Daemons'))
        cuisine.upstart_ensure('apache2')

../src ディレクトリには、Apache + mod_wsgiHello world が出力されるような hello.py を設置する。
Vagrantfileとprovision.py の設定により、http://192.168.33.10/hello で hello.py が実行される。

def application(environ, start_response):
    status = "200 OK"
    response_headers = [("Content-type", "text/plain")]
    start_response(status, response_headers)
    return ["Hello world! (mod_wsgi)"]

仮想サーバを起動

vagrant up を実行すれば、無事に仮想環境が立ち上がる。vagran ssh でログインすることができる。

$ vagant up
$ vagrant ssh
or
$ ssh vagrant@192.168.33.10 (pwはvagrant)

Vagrantの1.3.0からは、2回目以降の起動時にはプロビジョニングが実行されないので、2度目以降のupの場合は、下記を実行しなくてはならない。

$ vagant up --provision

親ホストの ../src ディレクトリで開発をすると、仮想サーバの /src 以下が自動的に変更されるので開発がらくちん。 ついでに../srcごとgithubにpushしちゃうとアプリケーション+仮想環境ごと複製できて飯がうまいうまい(ということで後述)

git(github)と連携する

githubに上げる時は、Vagrantfileと同じ場所に生成される .vagrant ディレクトリが追加されないように .gitignoreに記載することをお忘れなく(失敗すると面倒なことに)。

下記の通りに実行すると、仮想サーバを構築して Apache + PythonHello worldが!

$ git clone https://github.com/mikanmarusan/vagrant.git vagrant
$ cd vagrant/ubuntu/vagrant (ディレクトリ構成微妙)
$ vagrant up

まとめ

  • vagrantやっぱり便利
  • fabricとcuisineを使うとchef-soloっぽいのができる(けどシェルの高機能版って感じもする)
  • githubと連携できるとどこにいても同じ環境が作れる。

AWSでEC2を起動したらまずやること

東京リージョンなのにタイムゾーンがずれているので修正

$ date
Mon Aug 26 21:23:11 UTC 2013
$ sudo cp /usr/share/zoneinfo/Japan /etc/localtime 
$ date
Tue Aug 27 06:23:43 JST 2013

初代Let's Note CF-R1のHDDをSSDに換装する

http://ctlg.panasonic.jp/products/images/product/m/5/CF-R1MCAXR_5677.jpg

2003年2月発売の初代Let's Note CF-R1のHDDが廃れてきて(そりゃそうだ)、カラカラという音がひどくなってきていました。Windows XPのサポート期限の2014年4月までなんとか延命させたく、いっそのことSSDに換装をしてみたのでそのまとめです。

(せっかちさん向け)CF-R1のHDD→SSD換装のまとめ

  • まず起動が爆速、最新のMac Book Air並み
  • 体感的にはちょっと早くなったなー程度(CPUがMobile PentiumIII 866MHzと遅いからかな?)
  • CF-R1は元々ファンレスだったので、SSDにしたことで機械駆動部品が無くなり無音PC誕生、おめでとう
  • CF-R1のHDD換装と言えばピンの加工らしいが、億劫な自分はピン加工不要にしたいのでSSDを厳選した。
  • パフォーマンス
    • 左が換装前HDD、右が換装後のSSD。圧勝!

CF-R1のHDDは特殊HDD?

SSDに換装する前にもHDDを交換したいと思っていたのですが、どうも初代Let's Noteで採用されているHDDは省電力設計がされているため少々特殊なようで、先駆者達がかなり無茶されていました。

総じて、「IDE HDDの41ピンと44ピンを曲げる」のがコツらしく、さらにSSDの相性によっては認識されない場合があるので1ピンと42ピンを10KΩのカーボン抵抗でつなぐとかかなりHackされていて正直これはしんどいです。

トランセンドSSDをレッツノートで認識させる

CF-R1のHDDの仕様と対応方法のまとめ

  • 仕様
    • CF-R1のHDDは、東芝製の2.5インチ IDE HDD(自分の場合はMK4020GLSという型番でした)
    • 41ピンがロジック電源(3.3V)/42ピンが駆動用(5V)。※これが特殊
    • 市販のHDDは、41ピンがロジック電源(5V)/42ピンが駆動用(5V)なので、そのままだとロジック側電圧が足りなくて認識できないし、HDD内部で41ピンと42ピンが内部でショートしている場合はマザーボードがお亡くなりになる可能性がある
  • 対応方法
    • 41ピンと42ピンが内部でショートしている場合→41ピンは不要なので抜くか曲げて絶縁する(抜くと後で使い回せないので曲げる人が多い)
    • 41ピンと42ピンが内部でショートしていない場合→41ピンを42ピンとショートさせる
    • 44ピンは使わないので曲げておく

44ピンを曲げるのは、もともとIDEの44ピンはRESERVEDピンのため特段必要がなければ不要ということもあり、CF-R1のコネクタ側でマスクされているため(ピンが刺さらないようになっているので)、曲げておかないと刺さらないからです。ちなみにIDEのピンアサインは、通信用語の基礎知識 が分かりやすいです、これはブクマするべき。

以上の情報から、ピンの加工をせずにCF-R1に最適なSSD

  • 41ピンがハリボテ(ピンがない、もしくはあるけど絶縁されている)
  • 41ピンの内部と42ピンが内部でショートしていて、42ピンの5Vがロジック電源になるもの
  • 44ピンがない

となるのですが、探したらやっぱりあるんですね。
シー・エフ・デー販売 DRAM搭載のIDE接続SSD 64GB CSSD-PMM64WJ2

いざ換装

HDDの中身ををフリーソフトを利用してSSDに丸ごとコピーして(IDE-UDB変換器を利用)、あとはHDDとSSDを交換するだけです。


CF-R1を分解します。手順は、CF-R1 のディスクをSSDに換装する が参考になりました。


ディスクはウレタン?製のカバーに包まれているので取り外します(このカバーは後で使う)。HDDにDC+5V +3.3Vと書いてあるのが見えるでしょうか?


SSDに換装して、カバーを装着します(カバーがないとPCの中でSSDが動いてしまう)


SSDをHDDがあった場所に戻し、組み立て直して終了

まとめ

  • 初代Let's NoteのSSD換装が完了、パフォーマンス良好
  • ただしHDDの仕様が結構特殊なのでSSD選びが需要