SHOYAN BLOG

I am a pragmatic programmer.

base64エンコードのアルゴリズムをRubyで実装する

Base64とは英数字、記号を用いてマルチバイト文字やバイナリデータ(画像など)を扱うためのエンコード方式です。
具体的にはA–Z, a–z, 0–9 までの62文字と、記号2つ (+,/)、さらにパディング(余った部分を詰める)のための記号として = が用いられます。
7ビットの文字コードしか扱うことができない電子メールにおいてよく利用されています。

変換アルゴリズム

変換アルゴリズムは以下となります。

  1. 元データを6ビットずつに分割する(6ビットに満たない部分は0を追加して6ビットにする)。
  2. 各6ビットの値を変換表を使って4文字ずつに変換する(4文字に満たない部分は=記号を使って4文字にする)。

変換例

1. 元データ

文字列: “ABCDEFG”
2進数に変換する: “0100 0001 0100 0010 0100 0011 0100 0100 0100 0101 0100 0110 0100 0111”

rubyでのサンプルコード

1
"ABCDEFG".unpack("B*").pop.scan(/.{1,4}/).join(" ")

2. 6ビットずつに分割

“010000 010100 001001 000011 010001 000100 010101 000110 010001 11”

1
"ABCDEFG".unpack("B*").pop.scan(/.{1,6}/).join(" ")

3. 2ビット余るので、4ビット分0を追加して6ビットにする

“010000 010100 001001 000011 010001 000100 010101 000110 010001 110000”

1
list = "ABCDEFG".unpack("B*").pop.scan(/.{1,6}/).join(" ").split.map { |s| sprintf("%-06s", s).gsub(" ", "0")}.join(" ")

4. 変換表により、4文字ずつ変換

“QUJD”, “REVG”, “Rw”

1
2
3
4
5
6
7
# 変換表を作成する
keys = (0..63).map {|m| sprintf("%06s", m.to_s(2)).gsub(" ", "0")}
values = [('A'..'Z'), ('a'..'z'), ('0'..'9'), ['+', '/']].map { |a| a.to_a }.flatten
base64_table = Hash[[keys, values].transpose]

base64_list = list.map {|a| base64_table[a]}.join.scan(/.{1,4}/)
=> ["QUJD", "REVG", "Rw"]

5. 2文字余るので、2文字分 = 記号を追加して4文字にする

1
base64_list.map {|s| sprintf("%-4s", s).gsub(" ", "=")}

6. Base64文字列

“QUJDREVGRw==”

1
2
base64_str.scan(/.{1,4}/).map {|s| sprintf("%-4s", s).gsub(" ", "=")}.join
=> "QUJDREVGRw=="

簡易的なbase64_decodeメソッド

今までのロジックをメソッドにまとめて簡易的なbase64_decodeメソッドを作成しました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Base64
  def self.base64_encode(str)
    # 変換表を作成する
    keys = (0..63).map {|m| sprintf("%06s", m.to_s(2)).gsub(" ", "0")}
    values = [('A'..'Z'), ('a'..'z'), ('0'..'9'), ['+', '/']].map { |a| a.to_a }.flatten
    base64_table = Hash[[keys, values].transpose]

    binary = str.unpack("B*").pop.scan(/.{1,6}/).join(" ").split.map { |s| sprintf("%-06s", s).gsub(" ", "0") }
    base64_list = binary.map {|a| base64_table[a]}.join.scan(/.{1,4}/)
    base64_list.map {|s| sprintf("%-4s", s).gsub(" ", "=")}.join
  end
end

p Base64.base64_encode("ABCDEFG")
=> "QUJDREVGRw=="

RubyのBase64ライブラリでencodeした値と比べてみましょう。

1
2
3
require 'base64'
Base64.encode64("ABCDEFG")
=> "QUJDREVGRw==\n"

Rubyのencode64は最後に改行が入るようですが、encodeされた値は同じですね!

参考リンク

先週月曜日の日付を取得するアルゴリズム

先週の月曜日を求めるアルゴリズム

  • 先週(7日前)が月曜日である場合はその日付を返す
  • 月曜日でない場合
    • 月曜日より前であれば日付を1日たす
    • 月曜日より後であれば日付を1日ひく

Rubyで実装

1
2
3
4
5
6
7
8
9
def last_monday(date = Date.today - 7)
  return date if date.monday?
  if date.wday < 1
    date += 1
  else
    date -= 1
  end
  last_monday(date)
end

先週の金曜日を求める場合

1
2
3
4
5
6
7
8
9
def last_friday(date = Date.today - 7)
  return date if date.friday?
  if date.wday < 5
    date += 1
  else
    date -= 1
  end
  last_friday(date)
end

次の月曜日の日付を求めるアルゴリズム

  • 明日が月曜日かどうか
    • 月曜日であればその日を返す
    • 月曜日でなければ1日たす

Rubyで実装

1
2
3
4
def next_monday(date = Date.today + 1)
  return date if date.monday?
  next_monday(date + 1)
end

前回の月曜日の日付を求めるアルゴリズム

  • 昨日が月曜日かどうか
    • 月曜日であればその日を返す
    • 月曜日でなければ1日ひく

Rubyで実装

1
2
3
4
def prev_monday(date = Date.today - 1)
  return date if date.monday?
  prev_monday(date - 1)
end

実行結果

1
2
3
4
5
6
7
8
9
10
puts Date.today.strftime("%Y-%m-%d (%a)")
=> 2016-06-09 (Thu)
puts prev_monday.strftime("%Y-%m-%d (%a)")
=> 2016-06-06 (Mon)
puts next_monday.strftime("%Y-%m-%d (%a)")
=> 2016-06-13 (Mon)
puts last_monday.strftime("%Y-%m-%d (%a)")
=> 2016-05-30 (Mon)
puts last_friday.strftime("%Y-%m-%d (%a)")
=> 2016-06-03 (Fri)

ActionMailerの添付ファイルをRspecでテストする

ActionMailerで添付ファイルを送るようにしたのですが、そのテストをするときの情報があまりなかったのでまとめました。

以下のようにテストしました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
RSpec.describe AppMailer, type: :mailer do

  before(:all) do
    Archive::Zip.archive(
      "tmp/app.zip",
      "README.md")
  end

  after(:all) do
    FileUtils.rm "tmp/app.zip"
  end

  let(:mail) do
    AppMailer.send_email(zip_path)
  end

  it 'assings the attachment file' do
    attachment = mail.attachments[0]
    expect(attachment).to be_a_kind_of(Mail::Part)
    expect(attachment.content_type).to be_start_with('application/zip')
    expect(attachment.filename).to eq "app.zip"
  end
end

まず、before(:all)でテストに使うzipファイルを作成します。
zipファイルはarchive-zipを使って作成しています。
アーカイブするファイルは何でもよいですが、この記事ではREADME.mdとしています。
テストで作成したzipファイルはafter(:all)で消しています。
mail.attachments[0]に添付ファイルが入っているので、その種類やファイル名を確認しています。

参考リンク

TCPWrappersとは何なのかを調べてみた

sshのハンドシェイクでこけてしまい、その原因がわからずハマりました。
原因としては、TCPWrappersで拒否されていました。
TCPWrappersとはなんだろうということで調べてみました。

TCPWrappersとは

TCPWrapperとは、ネットワークのアクセス制御をする機能です。
デフォルトでインストールされています。

Linuxの基本的なアクセス制御の仕組み

Access Control to Network Services

アクセス制御の仕組みとして、FirewallとTCP Wrappersがあります。
Firewallはiptablesで制御します。
なぜ2つの仕組みが存在するのかはわかりません(誰か教えてください)。

クライアント→Firewall→TCPWrappers→サービスという流れで処理を行います。
TCPWrappersは接続を許可するかどうかのファイルを参照し、許可リストにあればサービスに処理を受け渡します。

処理のログはsyslog daemon (syslogd)によってクライアントと接続先サービスの情報を /var/log/secure または /var/log/messages に書き込まれます。

TCPWrappersのアドバンテージ

TCPWrappersを使うことで、以下のメリットがあるとのこと。

  • 接続したクライアントとそのサービスがログに残る
  • 様々なプロトコルを集約して管理できる

##TCPWrappersの設定ファイル

以下の2つのファイルがあります。

  • /etc/hosts.allow
  • /etc/hosts.deny

TCPWrappersはクライアントからのリクエストを受けると、以下の処理を行います。

  • /etc/hosts.allow を参照します。クライアントがリストに存在した場合、接続を許可します。リストに存在しない場合は次のステップに進みます。
  • /etc/hosts.denyを参照します。クライアントがリストに存在した場合、接続を拒否します。リストにない場合は接続を許可します。

注意点

  • /etc/hosts.allowから先に評価され、そこでリストに一致した場合/etc/hosts.denyは評価されません。/etc/hosts.allow/etc/hosts.denyに同じクライアントを指定した場合は/etc/hosts.allowが優先されます。
  • ファイルの上から評価されていき、一致した時点で評価を打ち切ります。順番が重要です。
  • TCPWrappersはキャッシュを持ちません。ですので、設定ファイルを書き換えたら即反映されます。デーモンをリスタートする必要はありません。
  • 最後に改行を含めるとエラーメッセージが出るとのことなので、含めないほうがよさそうです。warning: /etc/hosts.allow, line 20: missing newline or line too longというメッセージが /var/log/messages または /var/log/secureに出力されるとのことです。

参考リンク

クロスサイトスクリプティングの脆弱性とその対策

クロスサイトスクリプティングはユーザーの入力を受け付ける動的なアプリケーションに含まれる脆弱性を利用した攻撃です。
英語ではCross Site Scriptingと呼ばれその頭文字をとるとCSSとなりますが、「Cascading Style Sheets」との区別が紛らわしいため一般的にXSSの表記が使われています。

クロスサイトスクリプティングとはどのような脆弱性であるのか

XSSとは動的なWebサイトのプログラムの脆弱性を利用した攻撃です。
この脆弱性を利用することで、攻撃者は悪意のあるJavaScriptコードをサイトに埋め込み、そのコードを実行することができるようになります。

例えば、XSSの脆弱性がある掲示板があったとします。
攻撃者はフィッシングサイトにページを遷移させるJavaScriptコードを投稿しました。
すると、他の利用者がこの掲示板を閲覧したときにそのJavaScriptコードが実行され、フィッシングサイトが表示されるようになります。

この脆弱性を持ったサイトが攻撃されるとどのような被害が起き得るのか

上記の例のようにXSSはWebサイトの利用者を攻撃の対象とします。

例えば、以下のような被害が出る可能性があります。

  • ユーザーのcookie情報を盗み、その情報を利用してそのユーザーでログインする
  • 不正なページへユーザーを誘導する
  • 不正なファイルをダウンロードさせる

Webサイトを直接攻撃することも可能です。
JavaScriptのhtmlを書き換える機能を使って、攻撃対象のサイトの内容を書き換えることができてしまいます。

なぜそのようなセキュリティホールが作り込まれてしまうのか

HTMLには特殊文字が存在します。
具体的には「’”<>&」といった文字です。
これらの特殊文字をHTML上に表示するには適切な方法で変換しなければなりません。
しかし、XSSの脆弱性のあるプログラムはこのような特殊文字を意識せずに作られてしまっています。

どのように対策をすればよいのか

入力時のチェックと出力時のチェックを行います。

入力時のチェック

具体的には、入力時に入力できる文字を制限します。
例えば、郵便番号を入力するフォームの場合は、数字と-しか入力できないようにします。
そうすれば、悪意のあるコード自体が入力できなくなります。
入り口でとめる作戦です。

注意点としては、入力文字のチェックは必ずサーバサイドで行わなければなりません。
PHP等のサーバサイドで実行されるプログラムでチェックする必要があるということです。
クライアントサイド(JavaScript)のチェックでは攻撃者がそのチェック機構自体を無効にすることが可能です。

出力時のチェック

実際には仕様上、入力文字を制限することができないことがあります。
例えばフリーフォームの場合、無闇に入力文字を制限するとユーザーは自由に文章を入力することができなくなってしまいます。

そこで、出力時に文字を無害化(サニタイジング)します。

サニタイジングについては、プログラム言語やフレームワークに専用の機能が用意されています。
その機能を使うことを強く推奨します。

例えばPHPであれば、htmlspecialcharsというhtmlの特殊文字を無害化する関数が用意されています。
http://php.net/manual/ja/function.htmlspecialchars.php

また、フレームワークはデフォルトでサニタイズされた文字が出力される仕組みになっているはずなので、そのようなフレームワークを使ったほうがよいでしょう。

参考リンク