GrailsのSpring Security Core Pluginでパスワードのソルトとストレッチング

パスワードをハッシュで保存して置くのは当たり前ですが、レインボーテーブル使用した総当たり探索の対策として、ソルトとストレッチングを組み合わせ、より安全にパスワードを保存するのが一般的になってきました。

GrailsSpring Security Core PluginはデフォルトではSHA-256のハッシュでパスワードを保存しますが、ソルトやストレッチングは使用されていません。しかし、そこは認証のデパートSpring Security、ソルトとストレッチングのサポートが組み込まれています。

まずはソルトのサポートから見て行きましょう。

usernameをソルトとする

usernameをソルトとして扱うにはConfig.groovyに以下の設定を追加します。

grails.plugins.springsecurity.dao.reflectionSaltSourceProperty = 'username'

これで認証時に、usernameをソルトとしてパスワードに付与しハッシュを計算するようになります。ただし、この設定はあくまで認証時の設定であり、ユーザのパスワードを保存する時にソルトが付与されるわけではありません。

もし、s2-quickstartコマンドを使って生成したユーザドメインクラスを使用している場合は、このドメインクラスに手を入れます。デフォルトでは以下のようにパスワードがエンコードされています。

protected void encodePassword() {
    password = springSecurityService.encodePassword(password)
}

これを以下のように変更します。

protected void encodePassword() {
    password = springSecurityService.encodePassword(password, username)
}

この様にspringSecurityService#encodePasswordの第2引数にソルトとしてusernameを指定します。

これでパスワード保存時にusernameをソルトとして付与してハッシュが計算されます。

ソルト専用のプロパティを使う

徳丸先生の資料によるとソルトには一定の長さが必要です。usernameとpasswordの組み合わせで一定以上の長さが確保できない場合は、一定の長さをもつソルト専用のプロパティが欲しくなります。

以下のようにユーザドメインクラスを変更します。

import org.apache.commons.lang.RandomStringUtils

class User {

    ...

    String salt

    String getSalt() {
        // 新規作成時にsaltがない場合にsaltを生成
        if (!salt) {
            // ランダムである必要ないが一定の長さをもつ文字列として
            // ランダムが扱いやすいのでRandomStringUtilsを使用
            salt = RandomStringUtils.randomAlphanumeric(20) // ランダムな20文字
        }
        return salt
    }

    static constraints = {
        ...
        salt blank: false
    }

    static mapping = {
        ...
        // 安全のためinsert以外でsaltが更新されないようにする
        salt updateable: false
    }

    protected void encodePassword() {
        // usernameをsaltに変更
        password = springSecurityService.encodePassword(password, salt)
    }
}

合わせてConfig.groovyreflectionSaltSourcePropertyも変更しましょう。

grails.plugins.springsecurity.dao.reflectionSaltSourceProperty = 'salt'

これで設定完了!と言いたいところですが、まだいくつかの準備が必要です。

上記のreflectionSaltSourcePropertyはSpringSecurityのUserDetailsを実装したクラスのプロパティを指定しています。GrailsのSpring Security Core PluginのデフォルトではUserDetailsの実装クラスにGrailsUserが使われます。 しかし、GrailsUserにはsaltというプロパティは存在しないため、新たにクラスを用意する必要があります。

また、このUserDetailsインスタンスUserDetailsServiceの実装クラスで生成されます。GrailsのSpring Security Core Pluginでは、これがGormUserDetailsServiceというクラスになります。

つまり独自のUserDetailsServiceを作成し、saltというプロパティを持つ独自のUserDetailsを生成する必要があります。

クラスはsrc/groovyなどに作成すると良いでしょう。

まずはUserDetailsの実装クラスを以下のよう作成します。

import org.codehaus.groovy.grails.plugins.springsecurity.GrailsUser
import org.springframework.security.core.userdetails.UserDetails

class MyUser implements UserDetails {

    @Delegate
    GrailsUser grailsUser

    String salt
}

次に上記のMyUserクラスを生成するUserDetailsServiceを作ります。既存のGormUserDetailsServiceを継承して作るがカンタンです。

import org.codehaus.groovy.grails.plugins.springsecurity.GormUserDetailsService
import org.springframework.security.core.GrantedAuthority
import org.springframework.security.core.userdetails.UserDetails

class MyUserDetailsService extends GormUserDetailsService {

    @Override
    protected UserDetails createUserDetails(user, Collection<GrantedAuthority> authorities) {
        new MyUser(grailsUser: super.createUserDetails(user, authorities), salt: user.salt)
    }
}

groovyの@Delegateを使用したお手軽実装にしてみました。

あとはこのサービスをSpringのビーンとして登録します。resource.groovyに以下のように追加してください。

beans = {
    ...
    userDetailsService(MyUserDetailsService) {
        grailsApplication = ref('grailsApplication')
    }
}

これでソルト専用のプロパティを使うことができます。

ストレッチング

GrailsのSpring Security Core PluginのデフォルトではpasswordEncoderorg.springframework.security.authentication.encoding.MessageDigestPasswordEncoderが使われています。

このクラスはストレッチングをサポートしています。 しかし、プラグインの設定ではgrails.plugins.springsecurity.password.algorithmというハッシュアルゴリズムの設定があるだけで、ストレッチング回数の設定はありません(デフォルトでは1回です)。

自分で新たにビーンを定義することもできますが、ここはGrailsが持つ、設定ファイルからSpringビーンのプロパティを上書きする仕組みを活用します。

Config.groovyに以下の設定を追加します。

beans.passwordEncoder.iterations = 1000

これで1000回ストレッチングします。

テスト

ソルトとストレッチングが機能しているか不安になるのでテストを書いてみましょう。

def "ソルトとストレッチングの確認"() {
    setup:
    def password = "password"
    def user = new User(username: "test", password: password, enabled: true)

    and: "パスワードにランダム文字列をソルトとして付与"
    def digest = "$password{$user.salt}".getBytes("UTF-8")

    and: "1000回ストレッチング"
    1000.times { digest = MessageDigest.getInstance("SHA-256").digest(digest) }
    def hashedPassword = new String(Hex.encode(digest))

    when: "新規Userを保存"
    user.save()

    then: "ソルトとストレッチングを使用したハッシュになっていること"
    user.password == hashedPassword
}

うまく動いているようです。

おわりに

ということでソルトとストレッチングで安全にパスワードを保存しましょう。 プラグインのリファレンスは以下を詳しく見ておくとよいです。

http://grails-plugins.github.io/grails-spring-security-core/docs/manual/guide/12%20Password%20and%20Account%20Protection.html

なお、この記事の内容は自己責任でご利用ください。ではでは。

この記事はGrails2.2.1、Spring Security Core Plugin1.2.7.3をもとに記述しています。