読者です 読者をやめる 読者になる 読者になる

まったり技術ブログ

Technology is power.

【knock】JSON Web Token(JWT)を使ってみる【セキュリティ編】

f:id:motikan2010:20170421183524p:plain

前回の続きです。
JSON Web Token(JWT)を使ってみる【実装編】 - まったり技術ブログ
今回はJWTのセキュリティにふれてみます。

JWTは危険なのか

認証成功時に発行されるJSONは軽く見たかんじ、少し長い乱数のセッションIDのように見えますがそうではありません。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTI4MTY4MzYsInN1YiI6NX0.EzBo2BZatWc-80HAfioQYbL1gPH90tf9YV00yAnHBr8

規則性があり、下記の記事のように危険がふくまれているそうです。

auth0.com

christina04.hatenablog.com

発行されるJSONはこのような形式になっています。

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
eyJleHAiOjE0OTI4MTY4MzYsInN1YiI6NX0
EzBo2BZatWc-80HAfioQYbL1gPH90tf9YV00yAnHBr8

が発行された場合に、まずは「.」で区切る。
個々の値をBase64でデコードします。これだけです。

base64デコード
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 {“typ”:“JWT”,“alg”:“HS256”}
eyJleHAiOjE0OTI4NDY3MTUsInN1YiI6NX0 {“exp”:1492816836,“sub”:5}
EzBo2BZatWc-80HAfioQYbL1gPH90tf9YV00yAnHBr8 (署名バイナリデータ)

ユーザによって値が改ざんされる危険性

ここで重要なのが「“sub”:5」の"5"という数値がユーザidということです。
Webアプリケーション側では、この数値でユーザを識別されており、この値を他ユーザの値に改ざんして送信することによって、なりすましを行うことが可能となっています。
本来は「EzBo2BZa・・・」の値が検証トークンとなっており、値が改ざんされたことを検出できるが、algの指定にnoneが用いられた時に検証されないとのことです。
つまり、「{“typ”:“JWT”,“alg”:“none”} 」の場合に、なりすましが行われてしまう
そのことがJWTのセキュリティ上の懸念となっている。

ここではknockのみに焦点を当てて、改ざんが検出されるか、またはされないかの確認をしていきます。

knockはどうなのか

試しに「"sub":5」を「sub":6」に改ざんしてリクエストを送信してみます。

アルゴリズムを「HS256」

まずは、knockのデフォルトアルゴリズムで確認します。

ユーザid:5で認証

$ curl -X "POST" "http://nuconuco.com:3000/user_token" \
-H "Content-Type: application/json" \
-d '{"auth": {"email": "user1@example.com", "password": "passwd1"}}'

{"jwt":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTI4NDc5OTAsInN1YiI6NX0.L_inYpObtUsQE_lEP_Kk2FNgP8888ppMICykuGa7AVQ"}%
Base64デコード
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 {“typ”:“JWT”,“alg”:“HS256”}
eyJleHAiOjE0OTI4NDc5OTAsInN1YiI6NX0 {“exp”:1492847990,“sub”:5}

「"sub":6」に改ざんして送信

Base64デコード
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 {“typ”:“JWT”,“alg”:“HS256”}
eyJleHAiOjE0OTI4NDc5OTAsInN1YiI6Nn0= {“exp”:1492847990,“sub”:6}
$ curl -X "GET" "http://example.jp:3000/private-posts" -v \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTI4NDc5OTAsInN1YiI6Nn0=.L_inYpObtUsQE_lEP_Kk2FNgP8888ppMICykuGa7AVQ" \
-H "Content-Type: application/json"


HTTP/1.1 401 Unauthorized
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: text/html
Cache-Control: no-cache
X-Request-Id: dc65a24d-e7c6-4cd7-abb2-0eaa27680f0a
X-Runtime: 0.003603
Transfer-Encoding: chunked

改ざんが検知されて、「HTTP/1.1 401 Unauthorized」となっている。

アルゴリズムを「none」

algにnoneを指定すれば、署名であるトークンが発行されずに改ざんができるのかを確認します。

設定ファイルの編集

$ vim config/initializers/knock.rb

# config.token_signature_algorithm = 'HS256'
config.token_signature_algorithm = 'none' # 32行付近に追記

これで「{typ: “JWT”, alg: “none”}」となってくれるはずです。

ユーザid:5で認証

$ curl -X "POST" "http://nuconuco.com:3000/user_token" \
-H "Content-Type: application/json" \
-d '{"auth": {"email": "user1@example.com", "password": "passwd1"}}'

{"jwt":"eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJleHAiOjE0OTI4NDg2MzUsInN1YiI6NX0."}%
Base64デコード
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0 {“typ”:“JWT”,“alg”:“none”}
eyJleHAiOjE0OTI4NDg2MzUsInN1YiI6NX0 {“exp”:1492848635,“sub”:5}

予想通り「{“typ”:“JWT”,“alg”:“none”}」になり、認証トークンも発行されていないことが分かります 。 この状態で「"sub":5」を改ざんするとなりすましが可能であるか確認してみます。

「"sub":6」に改ざんして送信

Base64デコード
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0 {“typ”:“JWT”,“alg”:“none”}
eyJleHAiOjE0OTI4NDg2MzUsInN1YiI6Nn0= {“exp”:1492848635,“sub”:6}
$ curl -X "GET" "http://example.jp:3000/private-posts" \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJleHAiOjE0OTI4NDg2MzUsInN1YiI6Nn0=." \
-H "Content-Type: application/json"

HTTP/1.1 401 Unauthorized
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: text/html
Cache-Control: no-cache
X-Request-Id: 55fda114-d9c3-435f-aa49-22816180bd97
X-Runtime: 0.004454
Transfer-Encoding: chunked

結果は「HTTP/1.1 401 Unauthorized」
なんと「"alg":“none"」を指定しているがエラーになった。
knockでは「"alg”:“none"」に指定しても、認証トークンが検証されることが確認できた。
でも何故なのか。


knockのソースを見て原因を調べてみる。

なぜknockでは「"alg":“none"」で検証が行われたのか

JWTの実装を確認してみる。

ruby-jwtの仕様を確認

knockは内部でruby-jwtを利用しているので、まずはruby-jwtのREADMEを見てみます。
github.com

を見てみると、

decoded_token = JWT.decode token, nil, false

JSONのでコード時つまりトークンの検証時に、第3引数に検証有無を指定する必要があるらしく、上記のようにfalseが指定されていると検証が行われません。

knockの実装を確認

knock/auth_token.rb at 7fb00e36b8a1db188d2258eb28dbc56441385302 · nsarno/knock · GitHub

# 10行付近
@payload, _ = JWT.decode token, decode_key, true, options.merge(verify_options)

knockだとtrueが指定されており、検証が必須となっている。 そのため「"alg":“none"」となっていても検証が行われていたわけです。

knockでトークン検証なしにしてみる

ソースコードを少し変更してみて、トークンの検証が行われないようにしてみます。
試しに第3引数を"false"に変更して、動作を確認してみる。

$ vim vendor/bundler/ruby/2.3.0/gems/knock-2.1.1/app/model/knock/auth_token.rb

@payload, _ = JWT.decode token, decode_key, false, options.merge(verify_options) # 変更

再度改ざんしたリクエストを送信してみる。認証者をレスポンスで返すようにする。

$ vim app/controllers/private_posts_controller.rb

class PrivatePostsController < ApplicationController
  include JSONAPI::ActsAsResourceController
  before_action :authenticate_user

  def index
    render :json => current_user
  end

end
curl -X "GET" "http://example.jp:3000/private-posts" -v \
-H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJleHAiOjE0OTI4NDg2MzUsInN1YiI6Nn0=." \
-H "Content-Type: application/json"

HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/json; charset=utf-8
ETag: W/"f7e6dd29a636a3d858fab2dd8c67e57d"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: e4fdf9a4-49e7-4bff-be60-ae8351dc4483
X-Runtime: 0.004916
Transfer-Encoding: chunked

{"id":6,"password_digest":"$2a$10$2IL4VyZ2m2ojfjrWpMxiDOTL6Ctu43cm2o6423z7xCET71HtZVSRC","name":"User2","email":"user2@example.com","created_at":"2017-04-20T18:46:10.245Z","updated_at":"2017-04-20T18:46:10.245Z"}%

改ざんしたユーザidの情報を取得できていることが確認できる。

結論

knockの場合だと、変にknock自体のソースコードを変更しない限り、トークンの検証は必ず行われると考えられます。
今回は必然的に改ざんの検出が行われるとなりましたが、他のJWTライブラリではどうなのか。設定次第では検証を行わせないようなライブラリがあるのかなどを探していきます。

【knock】JSON Web Token(JWT)を使ってみる【実装編】

f:id:motikan2010:20170421183808p:plain

github.com

JSON Web Tokenの説明は下記の記事を参照。 qiita.com

事前準備

新規アプリを生成

$ rails new railsJWT --api

Gemfileに追記

必要なライブラリをGemfileに追記します。

$ vim Gemfile

# 下記を追記
gem "faker"
gem "bcrypt"
gem "jsonapi-resources"
gem "knock"

$ bundle install

GitHub - cerebris/jsonapi-resources: A resource-focused Rails library for developing JSON API compliant servers.

モデルの作成

Postモデル

「タイトル」「内容」「公開/非公開の指定」のカラムを保持したPostモデルを作成します。

$ rails g model Post title:string body:text type:string
$ touch app/models/private_post.rb
$ touch app/models/public_post.rb
$ vim app/models/private_post.rb

class PrivatePost < Post
end

$ vim app/models/public_post.rb

class PublicPost < Post
end
$ vim app/models/post.rb

class Post < ApplicationRecord
  validates :body, presence: true
  validates :title, presence: true
  validates :type, presence: true

  POST_TYPES = %w(PublicPost PrivatePost)
  validates :type, :inclusion => { :in => POST_TYPES }
end

Userモデル

次に「パスワード」「名前」「メールアドレス」のカラムを保持したUserモデルを作成します。

$ rails g model user password_digest:string name:string email:string
$ vim app/models/user.rb

class User < ActiveRecord::Base
  has_secure_password

  validates :name, presence: true
  validates :email, presence: true
end
$ rails db:migrate

テストデータの追加

$ vim db/seeds.rb

Post.destroy_all
User.destroy_all

# ユーザを作成
User.create!({
  name: 'User1',
  email: 'user1@example.com',
  password: 'passwd1',
  password_confirmation: 'passwd1'
})

User.create!({
  name: 'User2',
  email: 'user2@example.com',
  password: 'passwd2',
  password_confirmation: 'passwd2'
})

3.times do
  # 公開記事を作成
  PublicPost.create!(
    title: Faker::Lorem.sentence,
    body: Faker::Lorem.paragraphs.join(' ')
  )

  # 非公開記事を作成
  PrivatePost.create!(
    title: Faker::Lorem.sentence,
    body: Faker::Lorem.paragraphs.join(' ')
  )
end
$ rails db:seed

コントローラの作成

PublicPostsコントローラ

$ vim app/controllers/application_controller.rb

class ApplicationController < ActionController::API
  include Knock::Authenticable # 追記
end
$ rails g controller PublicPosts
$ vim vim app/controllers/public_posts_controller.rb

class PublicPostsController < ApplicationController
  include JSONAPI::ActsAsResourceController # 追記
end
$ rails generate jsonapi:resource public_posts
$ vim app/resources/public_post_resource.rb

class PublicPostResource < JSONAPI::Resource
  immutable
  attributes :title, :body
end

ルーティング設定

$ vim config/routes.rb

jsonapi_resources :public_posts # 追記

PrivatePostsコントローラ

$ rails generate knock:install
$ rails generate knock:token_controller user

before_action :authenticate_user」を追記することによって、認証が必要なコントローラにすることができます。

$ rails g controller PrivatePosts
$ vim app/controllers/private_posts_controller.rb

class PrivatePostsController < ApplicationController
  include JSONAPI::ActsAsResourceController # 追記
  before_action :authenticate_user # 追記
end
$ rails generate jsonapi:resource private_posts
$ vim app/resources/private_post_resource.rb

class PrivatePostResource < JSONAPI::Resource
  immutable
  attributes :title, :body
end

ルーティング設定

$ vim config/routes.rb

jsonapi_resources :private_posts

動作確認

リクエスト

“/public-posts"にアクセス

認証を行わずにアクセスすることが可能です。

$ curl -X "GET" "http://example.jp:3000/public-posts"

HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/vnd.api+json
ETag: W/"ae93de1833f5e081219472e78b408c0a"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: a0b4df38-9374-4801-97c3-714599305b00
X-Runtime: 0.010128
Transfer-Encoding: chunked

{"data":[{"id":"1","type":"public-posts","links":{"self":"http://example.jp:3000/public-posts/1"},"attributes":{"title":"Necessitatibus et sit alias.","body":"Numquam...(中略)..."}}]}%

“/private-posts"にアクセス

レスポンスで「HTTP/1.1 401 Unauthorized」と返ってきており、認証が必要ということが分かります。

$ curl -X "GET" "http://example.jp:3000/private-posts"

HTTP/1.1 401 Unauthorized
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: text/html
Cache-Control: no-cache
X-Request-Id: 7cde37bd-abdd-421f-a6dc-5667e8cce0d0
X-Runtime: 0.002747
Transfer-Encoding: chunked

認証を行う

“/private-posts"に対してアクセスを行うためには、認証後に発行されるトークンをリクエストに含める必要があります。

トークンを取得する認証リクエスト

$ curl -X "POST" "http://nuconuco.com:3000/user_token" \
> -H "Content-Type: application/json" \
> -d '{"auth": {"email": "user1@example.com", "password": "passwd1"}}'

{"jwt":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTI4MTY4MzYsInN1YiI6NX0.EzBo2BZatWc-80HAfioQYbL1gPH90tf9YV00yAnHBr8"}%

JSON形式で返ってきている「eyJ0eXAiOiJKV1QiL・・・」が認証トークンです。

トークンを使用してアクセス

Authorizationヘッダの値に取得したトークンを指定します。

$ curl -X "GET" "http://example.jp:3000/private-posts" \
> -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTI4MTY4MzYsInN1YiI6NX0.EzBo2BZatWc-80HAfioQYbL1gPH90tf9YV00yAnHBr8" \
> -H "Content-Type: application/json"

GET /private-posts HTTP/1.1
Host: example.jp:3000
User-Agent: curl/7.43.0
Accept: */*
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0OTI4MTY4MzYsInN1YiI6NX0.EzBo2BZatWc-80HAfioQYbL1gPH90tf9YV00yAnHBr8
Content-Type: application/json

HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Type: application/vnd.api+json
ETag: W/"df8eaf13cb9cd4dd8f47df9f4ec65bb3"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 5050c781-bbfb-43a6-a7f7-2b887682d3a0
X-Runtime: 0.022478
Transfer-Encoding: chunked

{"data":[{"id":"2","type":"private-posts","links":{"self":"http://example.jp:3000/private-posts/2"},"attributes":{"title":"Qui voluptas nemo tenetur.","body":"Nemo...(中略)..."}}]}%

正常に"/private-posts"にアクセスすることができています。

これでJSON Web Tokenの実装が完了となります。

ボタンを重ねない『クリックジャッキング』の話

f:id:motikan2010:20170407185617g:plain
Trap Site

2017年度に入りましたが、昔ながらのクリックジャッキングの話
『クリックジャッキング』ってなんぞやという方は下の記事が大変解りやすいかと。
blog.tokumaru.org

クリックジャッキングを成立させることは難しいのか

クリックジャッキングのことで同期の方と話した時に、おとりとなるボタンを押下させることがそもそも難しいということで、リスクは低いと言っていた。
f:id:motikan2010:20170407190439p:plain
(参考:http://www.ipa.go.jp/files/000026479.pdf)
確かに、いろんなサイトでのクリックジャッキングの説明を見てみるとおとりのボタンやリンクに『ここをクリック』だったり『おすすめ情報!!』になっており、見るからに怪しく、騙されてクリックする人なんていなさそう。
f:id:motikan2010:20170407185323p:plain

自然にボタンがページに紛れ込んでいても特定の箇所をクリックさせることがそもそも難しい気がする・・・。
f:id:motikan2010:20170407185509p:plain
そう考えてみると、攻撃を成立させることは難しそう。

どこをクリックしてもターゲットとなるボタンを押下させる

常にマウスカーソルにターゲットとなるサイト(iframe内)が来るようにJavaScriptで制御してみる。 http://biboroku.watanabehiroki.net/markup/javascript/jquery-sample-2

あとは、ターゲットとなるサイト内のボタンが左上に来るように調整する。 http://blog12345.seesaa.net/article/281787492.html

ターゲットとなるiframeは下記のように記述
<div id="target" style="width:180px;height:50px;margin:0px;opacity:0.5;overflow:hidden;">
  <iframe width="300px" height="650px" scrolling="no" frameborder="0"
    style="margin:-350px 0 0 -25px;overflow:hidden;" src="./target.html"></iframe>
</div>
divがマウスカーソルに追跡するように記述
$('html').mousemove(function(e){
    $('#target').css({
      top:e.pageY-25,
      left:e.pageX-50
      });
  });

デモサイト

Trap Site

透過50%です。透過100%にしたらまず気づかれることはなさそう。
f:id:motikan2010:20170407185617g:plain
これでどこをクリックしてもiframe内のボタンをクリックしたことになります。

【Go言語】パスワードをハッシュ化(bcrypt)

f:id:motikan2010:20170213221701p:plain
・bcrypt.GenerateFromPassword
・bcrypt.CompareHashAndPassword
を使ってみる話です。

Go言語を使ったWeb開発で認証機能を実装したくて調べてみたらこんなリポジトリが見つかった。

github.com

よく見てみるとパスワード格納はライブラリに含まれておらず、サンプルコードでも無残にパスワードが平文で格納されているではありませんか・・・。

Go言語でのパスワードハッシュに関して調べてみると下記の記事が見つかった。

hachibeechan.hateblo.jp

ハッシュ化のライブラリには主に2つ用意されているらしい。
・scrypt
・bcrypt

本記事では、bcryptライブラリを使ってハッシュ化する方法を紹介します。

bcrypt - GoDoc

パスワードのハッシュ化

hash, err := bcrypt.GenerateFromPassword([]byte("パスワード"), bcrypt.DefaultCost)

// Byteで返されるので文字列に変換して表示
fmt.Println(string(hash))

// 毎回値の異なるハッシュ値が取得できる(ちなみに"password"のハッシュ値)
// $2a$10$iuJaubQvGTawiwa6UFa08uvOGwFaa25Wz29llEKEFHyPT3w262Qw6
// $2a$10$HFZ4bmj98bEePKO3gNsbZO3XsgXORvjFhexZV6HADm46/CuaE6M/m
// $2a$10$BSzyPPKOOs0YwC1h6UoD2eNFAyWYVfS.hmZQuQLLTRyC/Z.z3fzsy

パスワード文字列とハッシュ値を比較

認証部分は下記のように記述します。

err = bcrypt.CompareHashAndPassword([]byte("ハッシュ値"), []byte("パスワード"))
// 一致している場合はerrにnilが返されます。一致していない場合はエラーが返されます。

コードにまとめるとこのような感じ

下記のコードでは「Success」が表示されます。

package main

import (
    "fmt"

    "golang.org/x/crypto/bcrypt"
)

func main() {
    storePass := "password"
    loadPass := "password"

    hash, err := bcrypt.GenerateFromPassword([]byte(storePass), bcrypt.DefaultCost)
    if err != nil {
        return
    }
    hash_str := string(hash)

    err = bcrypt.CompareHashAndPassword([]byte(hash_str), []byte(loadPass))
    if err != nil {
        fmt.Println("Failure")
    } else {
        fmt.Println("Success")
    }

}

認証のデモ

今回はパスワード認証機能が動作確認をしたいだけなので、データベースなどは用意せずにログインIDとパスワードを格納することができるUser構造体を使って動作確認を行う。

package main

import (
    "fmt"
    "time"

    "golang.org/x/crypto/bcrypt"
)

type User struct {
    LoginId  string
    Password string
}

type Users []*User

var users Users

func register(login_id, pass string) {
    /*
      bcrypt.MinCost = 4
      bcrypt.MaxCost = 31
      bcrypt.DefaultCost = 10
   */
    hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
    if err != nil {
        return
    }
    users = append(users, &User{LoginId: login_id, Password: string(hash)})
}

func login(login_id, password string) {
    var hash_str = ""
    start := time.Now()
    for _, user := range users {
        if login_id == user.LoginId {
            hash_str = user.Password
            break
        }
    }
    err := bcrypt.CompareHashAndPassword([]byte(hash_str), []byte(password))
    end := time.Now()
    fmt.Printf("%fs\t", (end.Sub(start)).Seconds())
    if err != nil {
        fmt.Print("Failure")
    } else {
        fmt.Print("Success")
    }
    fmt.Printf("\t%s/%s\n", login_id, password)
}

func main() {
    users = Users{}
  // 登録
    register("user1", "password1")
    register("user2", "password2")
    register("user3", "password3")
    register("user4", "password4")
    register("user5", "password5")
  fmt.Println()

  // 認証
    login("user1", "password1")
    login("user2", "password2")
    login("user3", "password3")
    login("user4", "password4")
    login("user5", "password5")
    login("user6", "password1")
    login("user1", "")
    login("user3", "password1")
    login("user3", "password2")
    login("user3", "password3")
    login("user3", "password4")

}
出力
0.106827s    Success user1 / password1
0.096496s   Success user2 / password2
0.099798s   Success user3 / password3
0.099758s   Success user4 / password4
0.100966s   Success user5 / password5
0.000001s   Failure user6 / password1
0.125239s   Failure user1 /
0.097884s   Failure user3 / password1
0.097816s   Failure user3 / password2
0.097296s   Success user3 / password3
0.096882s   Failure user3 / password4

処理時間を見てみると、ログインIDに「user6」を指定した時だけ処理時間が異様に短いことが分かる。
存在しないユーザであり「CompareHashAndPassword」の処理がとてつもなく短くなるからである。
レスポンス時間からユーザの有無を知られるなんて気分が悪いので、このように"hash_str"を初期化してみる。

var hash_str = "$2a$10$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

こうすれば常に「CompareHashAndPassword」の処理時間が同等になる。

0.097851s Success user1 / password1
0.095720s  Success user2 / password2
0.097851s  Success user3 / password3
0.097147s  Success user4 / password4
0.104292s  Success user5 / password5
0.099853s  Failure user6 / password1
0.098755s  Failure user1 /
0.094102s  Failure user3 / password1
0.093256s  Failure user3 / password2
0.100131s  Success user3 / password3
0.097537s  Failure user3 / password4

これでレスポンス時間からユーザの有無を知られることはなくなるかと思う。

Webフレームワーク『Gin』を使ってみる

f:id:motikan2010:20170211223100p:plain
Go言語でWebアプリ開発をしていみたいと思っていましたので、調べてみたらいろいろあるらしい。

概観からGoのWebFrameworkを選ぶ(2016/02) - Qiita その中で速度が速く、人気もある『Gin』を手始めにさわってみることにします。

github.com

作成するものは「SQLiteを使ったTODOリストアプリ」です。
こちらの記事を参考にして作成しました。

Go言語製WAF GinでWebアプリを作ってみる【準備編】 | eureka tech blog

作成順序としては
 ビュー → コントローラ → モデル
です。

下記のコマンドでGinをインストールすることができます。

$ go get gopkg.in/gin-gonic/gin.v1

HTMLテンプレートを呼び出す

まずは「/」にアクセスしたら、「index.tmpl」を呼び出し、出力するだけのコードを書いていきます。

$ vim main.go
package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func main() {
    router := gin.Default()
    router.LoadHTMLGlob("views/*")

    router.GET("/", func(c *gin.Context) {
        c.HTML(http.StatusOK, "index.tmpl", gin.H{
                    "title": "Hello Gin!",
                })
    })

    router.Run(":8080")
}

HTMLテンプレート作成

main.goで

gin.H{
    "title": "Hello Gin!",
}

と定義しているので、テンプレート側で「{{ .title }}」と記述してレンダリングさせることができます。

$ mkdir views
$ vim views/index.tmpl
<!DOCTYPE html>
<html>
    <head>
        <title>{{ .title }}</title>
    </head>
    <body>
        <h3>{{ .title }}</h3>
    </body>
</html>

ここまでできたら起動させてアクセスしてみます。

動作確認

$ go run main.go

http://127.0.0.1:8080」にアクセスしてみます。
f:id:motikan2010:20170211215514p:plain

コントローラを作る

ビューとモデル(DB)を橋渡しをするコントローラを作成していきます。

タスク一覧を渡すコントローラ作成

今はタスク一覧はDBから持ってくるのではなく、コントローラ内でをタスク配列として用意し、
そのタスク一覧をテンプレート側に渡す処理を書いていきます。

$ mkdir controllers
$ vim controllers/task.go
package task

import "strconv"

// idとテキストを保持する構造体
type Task struct {
    ID   int
    Text string
}

func NewTask() Task {
    return Task{}
}

// タスク構造体一覧を返す
func (c Task) GetAll() interface{} {

    // テストデータとして5つタスクを作成
    tasks := make([]*Task, 5)
    for i := 1; i <= 5; i++ {
        tasks[i-1] = &Task{ID: i, Text: "Task Text " + strconv.Itoa(i)}
    }

    return tasks
}

コントローラの呼び出し

$ vim main.go
//・・・

router.GET("/", func(c *gin.Context) {
    controller := task.NewTask()
    tasks := controller.GetAll()

    c.HTML(http.StatusOK, "index.tmpl", gin.H{
        "title": "TODO List",
        "tasks": tasks,    // 追記 テンプレートにタスクを渡す
    })
})

//・・・

テンプレートを修正

タスク一覧をリストとして表示するforを記述していきます。

$ vim views/index.tmpl
//・・・

<body>
<h3>{{ .title }}</h3>
    <ul>
    {{ range $index, $task := .tasks }}
        <li>{{ $task.ID }}: {{ $task.Text }} </li>
    {{ end }}
    </ul>
</body>

//・・・

f:id:motikan2010:20170211215616p:plain

モデルを作成して、DBにタスクを保存する

タスク内容をDB内に保存できるようにします。
今回DBはSQLiteを使っていきます。

Modelを作成

タスクを登録するためにCreate関数を用意しています。
文字列の引数を受け取り、その文字列をDBに挿入するような動作を行います。

$ mkdir models
$ vim models/task.go
package task

import (
    "github.com/jinzhu/gorm"
    _ "github.com/mattn/go-sqlite3"
)

var db *gorm.DB

func init() {
    var err error

    db, err = gorm.Open("sqlite3", "task.db")

    db.DropTableIfExists(&Task{})
    db.CreateTable(&Task{})

    if err != nil {
        panic(err)
    }
}

type Task struct {
    ID   int    `gorm:"primary_key"`
    Text string `gorm:"size:140"`
}

type Tasks []Task

type TaskRepository struct {
}

func NewTaskRepository() TaskRepository {
    return TaskRepository{}
}

// データベースに一行登録する
func (m TaskRepository) Create(text string) {
    var task = Task{Text: text}
    db.NewRecord(task)
    db.Create(&task)
    db.Save(&task)
}

コントローラ − Create関数を追加

こちらでもCreate関数を定義しています。
モデル内に定義されているCreate関数に対して、ユーザが送信したタスク文字列を渡しています。

$ vim controllers/task.go
import (
    "strconv"

    task "../models"
)

//・・・

func (c Task) Create(text string) {
    repo := task.NewTaskRepository()
    repo.Create(text)
}

POSTデータの受け取り

「text := c.PostForm(“text”)」でユーザが送信したパラメータを取得することができます。

$ vim main.go
func main() {

    //・・・

    router.POST("/", func(c *gin.Context) {
        text := c.PostForm("text")
        ctrl := task.NewTask()
        ctrl.Create(text)

        c.Redirect(http.StatusMovedPermanently, "/")
    })

登録フォームの作成

アプリケーションにタスク文字列を送信するためにフォームを追加します。

$ vim views/index.tmpl
//・・・

<ul>
<form action="/" method="post">
  <input type="text" name="text"></input>
  <input type="submit" value="送信">
</form>
{{ range $index, $task := .tasks }}
    <li>{{ $task.ID }}: {{ $task.Text }} </li>
{{ end }}
</ul>

//・・・

タスクを登録

$ go run main.go

f:id:motikan2010:20170211215637p:plain

登録されたタスクの確認

$ sqlite3 task.db

sqlite> .tables
tasks

sqlite> select * from tasks;
1|Test task

データベースに登録したタスクを出力

モデル − GetAll関数を追加

GetAll関数はDBに登録されているタスクを全て返します。

$ vim models/task.go
func (m TaskRepository) GetAll() Tasks {
    var tasks = Tasks{}
    db.Find(&tasks)

    return tasks
}

コントローラ − GetAll関数を修正

$ vim controllers/task.go
func (c Task) GetAll() interface{} {
    repo := task.NewTaskRepository()
    tasks := repo.GetAll()

    return tasks
}

f:id:motikan2010:20170211215706p:plain

idを指定してタスクを取得

モデル - GetByID関数を追加

GetByID関数はタスクIDを引数として受け取り、該当するタスクを返します。

$ vim models/task.go
func (m TaskRepository) GetByID(id int) Tasks {
    var tasks = Tasks{}
    db.Find(&tasks, id)
    return tasks
}

コントローラ - Get関数を追加

$ vim controllers/task.go
func (c Task) Get(n int) interface{} {
    repo := task.NewTaskRepository()
    tasks := repo.GetByID(n)

    return tasks
}

 Getを呼び出す

func main() {

    //・・・

    router.GET("/:id", func(c *gin.Context) {
            var id, _ = strconv.Atoi(c.Param("id"))
            ctrl := task.NewTask()
            tasks := ctrl.Get(id)

            c.HTML(http.StatusOK, "index.tmpl", gin.H{
                "tasks": tasks,
            })
        })

動作確認

http://127.0.0.1:8080/2」にアクセスすると、idが2のタスクが表示されます。 f:id:motikan2010:20170211215739p:plain

phpbrewでGDライブラリを入れようとしたらエラーになった

結論から言いますとGDライブラリはyumでインストールすることができなかったので、
結局ソースからインストールしました。

“phpbrew install"が失敗した

phpbrewでGDライブラリを入れようとしたら下記のようなエラーが表示された。

$ phpbrew install 5.6.26 +default +gd +openssl=/usr -- --with-libdir=lib64
*WARNING* You're runing phpbrew as root/sudo. Unless you're going to install
system-wide phpbrew or this might cause problems.
===> phpbrew will now build 5.6.26

(省略)

checking for gdSetErrorMethod in -lgd... no

configure: error: Unable to find libgd.(a|so) >= 2.1.0 anywhere under /usr

Please checkout the build log file for more details:
     tail /root/.phpbrew/build/php-5.6.26/build.log

「Unable to find libgd.(a|so) >= 2.1.0 anywhere under」というエラーを見る限り、
libgdのバージョン2.1.0以上が必要らしい。

yumからlibgdをインストール

$ yum install gd
パッケージ gd-2.0.35-11.el6.x86_64 はインストール済みか最新バージョンです
何もしません

デフォルトのyumリポジトリでは、バージョン2.1.0以上を入れられない。

ソースからlibgdをインストール

少し手間だがソースからインストールしてみる。 http://d.hatena.ne.jp/end0tknr/20150313/1426224469 バージョン2.1.1は古いと思われるが、2.1.0の用件は満たしているのでこのバージョンを入れることにする。
ちなみに最新バージョンは、2.2.4です。(2017/2/6現在) https://libgd.github.io/

$ wget https://bitbucket.org/libgd/gd-libgd/downloads/libgd-2.1.1.tar.gz 
$ tar -zxvf libgd-2.1.1.tar.gz 
$ cd libgd-2.1.1
$ ./configure --prefix=/usr/local/gd
$ make
$ make install

libにシンボリックリンク

これでlibgdは"/usr/local/gd/“以下に配置されているので、

$ phpbrew install 5.6.26 +default +openssl=/usr +gd=/usr/local/gd -- --with-libdir=lib

として実行したのだが、今度はopensslがインストールされなかった。"–with-libdir=lib64"と指定しないといけなさそう。 なので

$ cd /usr/local/gd
$ ln -s lib lib64

を実行してlib64という名でlibにアクセスするようにする。

phpbrewでGDライブラリをインストール

$ phpbrew install 5.6.26 +default +openssl=/usr +gd=/usr/local/gd -- --with-libdir=lib64
*WARNING* You're runing phpbrew as root/sudo. Unless you're going to install
system-wide phpbrew or this might cause problems.
===> phpbrew will now build 5.6.26

(省略)

---> Found date.timezone, patching config timezone with Asia/Tokyo
Congratulations! Now you have PHP with 5.6.26 as php-5.6.26

phpbrewでGDライブラリを入れることができた。

Bluetoothプロキシ『btproxy』を使う

f:id:motikan2010:20170204200342p:plain
IoTというワードが流行りつつあり、Bluetoothの通信を見てみたいという欲求が出てきたので、Bluetooth通信のプロキシを探してみました。
このキャプチャ技術を応用したら
Bluetoothバイス開発のデバッグ
Bluetoothバイスに対してのセキュリティ診断 などの場面で使えそう

github.com

f:id:motikan2010:20170204200304p:plain “RaspberryPi3”(以下ラズパイ)にbtproxyをインストールしてみて、
Macbook Proからスマートフォンへのファイル転送をキャプチャします。

今回ラズパイのOSは2016-03-18-raspbian-jessie.imgを使います。

Download Raspbian for Raspberry Pi

では、早速導入していきましょう。

btproxyをRaspberryPi3に導入

BlueZをインストールする前の下準備

Linux上でBluetoothを扱うにはBlueZをインストールする必要があります。
まずはBlueZをインストールするために必要な依存パッケージをインストールします。

必要パッケージのインストール(※失敗)

pi@raspberrypi:~ $ sudo apt-get -y install libusb-dev libdbus-1-dev libglib2.0-dev libudev-dev libical-dev libreadline-dev
(省略)
E: Failed to fetch http://mirrordirector.raspbian.org/raspbian/pool/main/s/systemd/libudev-dev_215-17+deb8u3_armhf.deb  404  Not Found [IP: 5.153.225.207 80]
E: Unable to fetch some archives, maybe run apt-get update or try with --fix-missing?

ラズパイにOSをインストールしただけでは、パッケージリストが古い状態らしい。
下記のコマンドで解決します。

APTのパッケージリストの更新

pi@raspberrypi:~ $ sudo apt-get update

改めて依存パッケージのインストール

再び必要パッケージのインストール

pi@raspberrypi:~ $ sudo apt-get -y install libusb-dev libdbus-1-dev libglib2.0-dev libudev-dev libical-dev libreadline-dev

今度は成功しました。

BlueZ − インストール

BlueZの最新版は5系ですがbtproxyが動作しなかったので、4系のBlueZを使う。
インストール作業は"/usr/src/“ディレクトリで行う。

pi@raspberrypi:~ $ cd /usr/src/
$ sudo wget http://www.kernel.org/pub/linux/bluetooth/bluez-4.101.tar.xz
$ sudo tar xvf bluez-4.101.tar.xz
$ cd bluez-4.101/
$ sudo ./configure --disable-systemd
$ sudo make
$ sudo make install

btproxy − インストール

btproxyはGitHub上で管理されており、インストールを非常に簡単に行えるようになっています。

pi@raspberrypi:/usr/src $ sudo apt-get -y install python2.7-dev
$ sudo git clone https://github.com/conorpp/btproxy.git
$ cd btproxy
$ sudo python setup.py install

btproxyを使う

動作確認

ヘルプが表示されれば正常にインストールされています。

pi@raspberrypi:~ $ sudo btproxy
usage: btproxy [-h] [-a SET_ADDRESS] [-n] [-c] [-i INTERFACE] [-s SCRIPT] [-l]
               [-1 MASTER_NAME] [-2 SLAVE_NAME] [-C] [-v] [-z] [-q]
               [addr_master] [addr_slave]

Bluetooth MiTM Proxy. For analyzing bluetooth connections actively.

positional arguments:
  addr_master           Bluetooth address of target master device
  addr_slave            Bluetooth address of target slave device

optional arguments:
  -h, --help            show this help message and exit
  -a SET_ADDRESS, --set-address SET_ADDRESS
                        Address to set for Bluetooth adaptor (requires -i)
(省略)
  -q, --inquire-again   Inquire the services again, don't reuse saved
                        settings.

通信のキャプチャ

まずはBluetooth機器のMACアドレスをスキャンします。
MacスマートフォンBluetooth機能をONに設定しておきます。
f:id:motikan2010:20170204194936p:plain:w400

まずは、通信を行う同士のデバイスMACアドレスを取得します。
下記のコマンドの流れで、周囲にあるBluetooth機器のMACアドレスをスキャンします。

pi@raspberrypi:~ $ bluetoothctl
[NEW] Controller B8:27:EB:0A:55:1C raspberrypi [default]
[bluetooth]# scan on
Discovery started
[CHG] Controller B8:27:EB:0A:55:1C Discovering: yes
[NEW] Device 6C:76:60:8A:23:21 KCP01K
[NEW] Device B8:E8:56:2E:23:37 XXXXXX の MacBook Pro

[bluetooth]# quit // 終了

2つのBluetooth機器のMACアドレスを取得できたので、2つのデバイスを中継する『btproxy』を起動させます。
$ btproxy <マスターデバイスMACアドレス> <スレーブデバイスMACアドレス>

pi@raspberrypi:/usr/src/btproxy $ sudo btproxy B8:E8:56:2E:23:37 6C:76:60:8A:23:21
Running proxy on master  B8:E8:56:2E:23:37  and slave  6C:76:60:8A:23:21
Using shared adapter
Slave adapter:  hci0
Master adapter:  hci0
Looking up info on slave (6C:76:60:8A:23:21)
Looking up info on master (B8:E8:56:2E:23:37)
Spoofing master name as  KCP01K_btproxy
paired
Spoofing master name as  KCP01K_btproxy
Proxy listening for connections for "None"
Proxy listening for connections for "Headset Gateway"
Proxy listening for connections for "Handsfree Gateway"
Proxy listening for connections for "AV Remote Control Target"
Proxy listening for connections for "Advanced Audio"
Proxy listening for connections for "Android Network Access Point"
Proxy listening for connections for "MAP SMS/MMS"
Proxy listening for connections for "MAP EMAIL"
Proxy listening for connections for "OBEX Phonebook Access Server"
Proxy listening for connections for "OBEX Object Push"
Attempting connections with 10 services on slave
Now you're free to connect to "KCP01K_btproxy" from master device.
Connected to service "OBEX Object Push"

ペアリングの確認ダイアログが表示されるので許可します。
・ノートPC側
f:id:motikan2010:20170409233455p:plain:w300f:id:motikan2010:20170409233634p:plain:w300
スマートフォン
f:id:motikan2010:20170204194939p:plain:w300f:id:motikan2010:20170204194941p:plain:w300

ノートPC側からスマートフォン側へテキストファイルを送ってみます。

下記のファイルを送ってみます。
TEST.txt

Hello btproxy!!

f:id:motikan2010:20170204194954p:plain:w400
送信と同時にbtproxyによってキャプチャされた内容が表示されます。

Accepted connection from  ('B8:E8:56:2E:23:37', 12)
<<  '\x80\x00\x07\x10\x00\x1f@'
>>  '\xa0\x00\x0c\x10\x00\xff\xfe\xcb\x00\x00\x00\x01'
<<  '\x82\x00/\x01\x00\x15\x00T\x00E\x00X\x00T\x00.\x00t\x00x\x00t\x00\x00\xc3\x00\x00\x00\x0fI\x00\x12Hello btproxy!!'
>>  '\xa0\x00\x0b\xcb\x00\x00\x00\x01I\x00\x03'
<<  '\x81\x00\x03'
>>  '\xa0\x00\x08\xcb\x00\x00\x00\x01'
(104, 'Connection reset by peer') socket slave reconnecting...
Reconnecting...
(104, 'Connection reset by peer') socket master reconnecting...

「Hello btproxy!!」という文字が表示されておりキャプチャができていることを確認することができます。
“/libbtproxy/replace.py"ファイルを編集することによって、通信内容を改ざんすることも可能です。