理系学生日記

おまえはいつまで学生気分なのか

Base64 (encode) の PurePerl 実装

きっかけは,はてなダイアリー AtomPub

はてなダイアリー AtomPub が公開されたようです!!

これをちょっと使ってみて悦に浸ってたんですけど,AtomPub を使うときの認証時のパスワードダイジェストは,Base64 でエンコードする必要があります.

ぼくはこのダイジェストを生成時,以下のようにしていた.

use MIME::Base64 qw(encode_base64);
my $digest  = encode_base64( sha1( $nonce . $created . $password ));

ところがこれでは認証が通りません.401 Unauthorized がレスポンスとして返ってきます.ぼくはなんでかなーとうんうんうなっていたんですけど,実装例を見るとこうなっている (ref: はてなフォトライフAtomAPIとは はてなの人気・最新記事を集めました - はてな ).

my $digest = encode_base64(sha1($nonce . $now . $password || ''), '');

実際にこの実装例に従うと,ちゃんと認証をパスしました.

ぼくと実装例の違いは,MIME::Base64::encode_base64 に渡す第二引数だけだ.ここでドキュメントを見ると,MIME::Base64::encode_base64 の第二引数は行区切り文字となっています.

The second argument is the line-ending sequence to use. It is optional and defaults to "\n".

行区切り文字として改行を入れると認証が通らず,行区切りを行わないと認証が通るということになる.

Base64 の仕様

困ったときは RFC なので,MIME を規定している RFC 2045 を見てみました.6.8 に Base64 Content-Transfer-Encoding の記述があります.そこにある行区切りに関する記述がこちら.

The encoded output stream must be represented in lines of no more than 76 characters each.

must だ! 76 文字以上になるときは,行区切りで改行いれないといけないんじゃ…!!

ただ,MIME の Base64 と認証用の Base64 はちがうのかもしれませんし,実際にこちらの Web サービスでも,改行は入りません.まぁとりあえず仕様を見てると,PurePerl で Base64 をしたくなったわけさ! (MIME::Base64 は XS を使ってる).

つくってみた

Perl で初めてビット操作とかしたwww.

package MIME::Base64::Pure;

use warnings;
use strict;
use base qw(Exporter);
use bytes;

our @EXPORT = qw(base64_encode);
our $VERSION = '0.0.1';

my @base64_map = ('A' .. 'Z', 'a' .. 'z', '0' .. '9', '+', '/' );

sub base64_encode{
    my ($str, $eol) = @_;
    $eol = "\n" unless defined( $eol );
    my @chars = map { ord } split //, $str;

    my $ans;
    while( my @group3 = splice @chars, 0, 3 ) {
        no warnings 'uninitialized';

        $ans .= $base64_map[ $group3[0] >> 2 ];
        $ans .= $base64_map[ (($group3[0] & 0x03) << 4 ) | ($group3[1] >> 4) ];
        $ans .= $base64_map[ (($group3[1] & 0x0f) << 2) | $group3[2] >> 6];
        $ans .= $base64_map[ $group3[2] & 0x3f ];

        if   ( @group3 == 2 ) { $ans =~ s/.$/=/   }
        elsif( @group3 == 1 ) { $ans =~ s/..$/==/ }
    }
    $ans =~ s/(.{76})/$1$eol/g;
    $ans;
}

1;

まぁとりあえず,うまくいっている模様.MIME::Base64 についてくる Base64.xs とほとんど同じだから,うまくいくのも当然かもしれませんが!

use Test::More qw(no_plan);

use MIME::Base64::Pure;
use utf8;

is base64_encode( 'abcde' ), 'YWJjZGU=';
is base64_encode( 'xyz' ), 'eHl6';
is base64_encode( 'kiri' ), 'a2lyaQ==';
is base64_encode( 'やばい,超やばい' ), '44KE44Gw44GE77yM6LaF44KE44Gw44GE';
my $ans = <<EOF;
5LqI5oOz44KI44KK44Ga44Gj44Go5pmC6ZaT44GM44GL44GL44Gj44Gm44KE44KK44Gf44GE44GT
44Go44Go44GL5YWo54S244Gn44GN44Gq44GP44Gm5raZ55uu44Gq44KT44Gn44GZ44GR44Gp77yM
44Gd44KM44Gn44KC44G844GP44Gv5LiA55Sf5oe45ZG955Sf44GN44KI44GG44Go5oCd44GE44G+
44GZ
EOF
chomp $ans;
is base64_encode( '予想よりずっと時間がかかってやりたいこととか全然できなくて涙目なんですけど,それでもぼくは一生懸命生きようと思います' ), $ans;