swiftでQRコードジェネレータを作ってみた

だいぶ前だけど、swiftでQRコードジェネレータを作ってみた

bitbucket.org

今回はSwiftとRxSwiftを使ってみようということで、
またしてもMac上で動くQRコードジェネレータを作ってみた。

QRコードのエンジン部分にはQRCoderを使った。

QRCoder on CocoaPods.org

 

f:id:cnaos:20190126112303g:plain

動的にQRコードが変わる様子

 

テキストボックスと、スライダーの両方を監視して、
どちらかに変更があったらQRコード画像を生成し直すっていう処理が比較的少ないコード量で書ける。

ただ、RxSwiftの参考書などを見ずにやってたから、このくらいだったら大丈夫だけど、もっと複雑なことをやろうとしたら大変そうだなって感触。

あとMacOS用のRxSwiftはRxCocoaというサブセットで、
サポートされているGUI部品がiOS用のものとだいぶ違うらしい。
たとえば、MacOSだとテーブルはNSTableViewを使うんだけど、
このあたりのサポートがなかったりする。

 

 

griffonでQRコードジェネレータを作ってみた

macQRコードを生成するツールZebraがあって、
便利に使ってたんだけど、URLを変更するときにいちいち別の画面を開かないといけないのが面倒だったんで、QRコードジェネレータを作ってみた。
Zebraでもショートカットキーをつかえば簡単にできるけどね。

バイナリはここから
https://bitbucket.org/cnaos/quickqrgen/downloads/qucickQrGen-0.1-jar.zip

ソースコードはここに。
https://bitbucket.org/cnaos/quickqrgen/src


参考にしたのはこのあたり。

QRコードジェネレータ部分はgoogleのzxingを使いました。
GitHub - zxing/zxing: ZXing ("Zebra Crossing") barcode scanning library for Java, Android


結構作るのに手間取ったけど、
groovyのswing builderの使い方とか、
Javaのswingのあたりの基礎力が足りないんだろうな。

jerseyのサロゲートペアに関するバグ

現象

POSTするテキストなどにU+10000以上のUTF-8文字(サロゲートペアに変換される文字)が含まれると、OAuthのSHA-1署名が不正なものになる。

まずは、OAuthの署名をやってるメソッドのコード*1です。

OAuthSignature.java

78  public class OAuthSignature {

90 public static String generate(OAuthRequest request,
91 OAuthParameters params, OAuthSecrets secrets) throws OAuthSignatureException {
92 return getSignatureMethod(params).sign(elements(request, params), secrets);
93 }

105 public static void sign(OAuthRequest request,
106 OAuthParameters params, OAuthSecrets secrets) throws OAuthSignatureException {
107 params = (OAuthParameters)params.clone(); // don't modify caller's parameters
108 params.setSignature(generate(request, params, secrets));
109 params.writeRequest(request);
110 }

122 public static boolean verify(OAuthRequest request,
123 OAuthParameters params, OAuthSecrets secrets) throws OAuthSignatureException {
124 return getSignatureMethod(params).verify(elements(request, params), secrets, params.getSignature());
125 }

jerseyのOauthモジュールで署名を扱ってるのはこのOAuthSignatureクラスなんですが、sign(), verify()メソッドから呼ばれているelementsメソッドで、URIとかリクエストの中身を正規化しています。

230  private static String elements(OAuthRequest request,
231 OAuthParameters params) throws OAuthSignatureException {
232 // HTTP request method
233 StringBuilder buf = new StringBuilder(request.getRequestMethod().toUpperCase());
235 // request URL, see section 3.4.1.2 http://tools.ietf.org/html/draft-hammer-oauth-10#section-3.4.1.2
236 buf.append('&').append(UriComponent.encode(constructRequestURL(request).toASCIIString(),
237 UriComponent.Type.UNRESERVED));
239 // normalized request parameters, see section 3.4.1.3.2 http://tools.ietf.org/html/draft-hammer-oauth-10#section-3.4.1.3.2
240 buf.append('&').append(UriComponent.encode(normalizeParameters(request, params),
241 UriComponent.Type.UNRESERVED));
243 return buf.toString();
244 }

このあたりには問題がなくて、UriComponent.encode()メソッドが問題。

UriComponent.java

252  public static String encode(String s, Type t, boolean template) {
253 return _encode(s, t, template, false);
254 }

274  private static String _encode(String s, Type t, boolean template, boolean contextualEncode) {
275 final boolean[] table = [t.ordinal()];
277 StringBuilder sb = null;
278 for (int i = 0; i < s.length(); i++) {
279 final char c = s.charAt(i);
280 if (c < 0x80 && table[c]) {
281 if (sb != null) sb.append(c);
282 } else {
283 if (template && (c == '{' || c == '}')) {
284 if (sb != null) sb.append(c);
285 continue;
286 } else if (contextualEncode) {
287 if (c == '%' && i + 2 < s.length()) {
288 if (isHexCharacter(s.charAt(i + 1)) &&
289 isHexCharacter(s.charAt(i + 2))) {
290 if (sb != null)
291 sb.append('%').append(s.charAt(i + 1)).append(s.charAt(i + 2));
292 i += 2;
293 continue;
294 }
295 }
296 }
298 if (sb == null) {
299 sb = new StringBuilder();
300 sb.append(s.substring(0, i));
301 }
303 if (c < 0x80) {
304 if (c == ' ' && (t == .)) {
305 sb.append('+');
306 } else {
307 appendPercentEncodedOctet(sb, c);
308 }
309 } else {
310 appendUTF8EncodedCharacter(sb, c);
311 }
312 }
313 }

こいつがU+10000のUnicode文字つまり、java内でサロゲートペアに変換される文字を考慮してないんですな。

まあ、中身が正しいかどうかはわからんけど、サロゲートペアが含まれる場合は、以下のようにStringのlength()をそのまま使うと、U+10000以上のコードポイントの1文字が2文字に分割されてるので、バグります。


for (int i = 0; i < s.length(); i++) {

実際にgroovyで確認

ではそれを実地で確認してみましょう。javaだとjersey持ってきたりコンパイルするのがたるいので、goovyのコードです。groovyConsoleから実行するのが楽です。

@Grapes(
    @Grab(group='com.sun.jersey', module='jersey-core', version='1.14')
)
import java.net.URLEncoder;

def text="\ud83c\udc00\ud83c\udc01\ud83c\udc02"

println "text="+text
println "----"

def encoded = com.sun.jersey.api.uri.UriComponent.encode(text, com.sun.jersey.api.uri.UriComponent.Type.UNRESERVED)
println "UriComponent.encode="+encoded

def utf8andurlencoded = URLEncoder.encode(text, "UTF-8");
println "utf8andurlencoded="+utf8andurlencoded;

なにをやってるかというと、unicodeの U+1F000, U+1F001, U+1F002の3文字(unicodeで)をサロゲートペア化したテキストを用意して、JerseyのUriComponent.encodeとjava標準のURLEncodeでそれぞれUTF-8エンコード&URLエンコードした結果を表示しています。

で、結果はこれ。

text=🀀🀁🀂
      • -
UriComponent.encode=%3F%3F%3F%3F%3F%3F utf8andurlencoded=%F0%9F%80%80%F0%9F%80%81%F0%9F%80%82

jerseyの使ってるクラスのほうがエンコード失敗してますね。

というわけで、jerseyのOAuth署名処理にはコードポイントでU+10000以上の文字が含まれる場合に、リクエストパラメタの正規化のためのURLエンコードが正しく行われない問題があり、結果としてOAuth署名が正しく行われないのでした。

jersey2のほうでもまだ修正されてないみたいね。
jersey/UriComponent.java at master · jersey/jersey · GitHub

jersey2で確認するときはGrapeのところ次のように書き換えます。

@Grapes(
     @Grab(group='org.glassfish.jersey', module='project', version='2.0-m08-1')
)

*1:grepcodeの奴が見やすいので、そっちを貼ったけど、状況はjerseyの1.14でも同じ。

田中公平先生の解説一覧

NHKの熱中夜話あるいは、熱中スタジアム田中公平先生が出ている回は見逃せないと思い、過去の番組のページを集めてみた。
BS熱中夜話ヒーローソングナイト第1夜誕生編
BS熱中夜話ヒーローソングナイト第2夜進化編

BS熱中夜話アニメソングナイト第1夜水木一郎&堀江美都子編
BS熱中夜話アニメソングナイト第2夜新世代作曲家編

熱中スタジアム特撮ソング第1夜ウルトラマン&仮面ライダーソング
熱中スタジアム特撮ソング第2夜戦隊&メタルヒーローソング

年に一回はこんな感じで出てたらしい。
八月にはhttp://www.nhk.or.jp/n-stadium/anison/index.htmlが放送されるらしいし、楽しみ。

junitで例外のテストをもっと簡単にしたい

最近のJunitでは例外のチェックが前よりは簡単になったけど、JDaveの匿名クラスを使った記述方法と比べていまいちだなと感じていた。

例外のチェック方法の比較

JUnit4での例その1:@Testのexceptionを使う場合。

JUnit Cookbook」より

@Test(expected= IndexOutOfBoundsException.class)
public void empty() { 
    new ArrayList<Object>().get(0); 
}
JUnit4での例その2:@Ruleを使う場合。

http://kentbeck.github.com/junit/javadoc/latest/org/junit/rules/ExpectedException.html」より。

// These tests all pass.
 public static class HasExpectedException {
        @Rule
        public ExpectedException thrown= new ExpectedException();
 
        @Test
        public void throwsNothing() {
    // no exception expected, none thrown: passes.
        }
 
        @Test
        public void throwsNullPointerException() {
                thrown.expect(NullPointerException.class);
                throw new NullPointerException();
        }
 
        @Test
        public void throwsNullPointerExceptionWithMessage() {
                thrown.expect(NullPointerException.class);
                thrown.expectMessage("happened?");
                thrown.expectMessage(startsWith("What"));
                throw new NullPointerException("What happened?");
        }
 }

あとは「JUnit4.7 の新機能 Rules とは〜その2 - A Memorandum」が参考になる。

JDaveでの例

JDave-examples」から必要な部分を抜粋。

import jdave.Block;
import jdave.Specification;
import jdave.junit4.JDaveRunner;

@RunWith(JDaveRunner.class)
public class StackSpec extends Specification<Stack<?>> {
    public class FullStack {
        private Stack<Integer> stack;

        public Stack<Integer> create() {
            stack = new Stack<Integer>(10);
            for (int i = 0; i < 10; i++) {
                stack.push(i);
            }
            return stack;
        }

        public void complainsOnPush() {
            specify(new Block() {
                public void run() throws Throwable {
                    stack.push(100);
                }
            }, should.raise(StackOverflowException.class));
        }       
    }
}

好みの問題かもしれないけど、

  • Blockを実装した匿名クラスを使う事で例外が起きるであろう箇所を指定できる。
  • そのすぐ近くに投げられるであろう例外と例外メッセージを記述できる。

という部分がとても気に入っている。

ただ、以下のような違いもあって、Junitから乗り換えるのが難しいと感じる部分もある。

  • JDaveRunnerでうごかさないといけない
  • Specificationクラス内にテストのサブクラスを記述しないといけい
  • createメソッドが必要

だったらJUnitのassertThatで同じような感じで記述できればいいかなと思って、以下のような感じで記述するためのカスタムMatcherを作ってみた。

assertThat( new Block(){
  public void run() throws Throwable {
                    stack.push(100);
                }
}, should.raise(StackOverflowException.class));

設計と実装

hamcrest の Matcher を独自拡張 - java.util.Date 版 closeTo - 倖せの迷う森」の独自のDate用Matcherを作成した例を参考にして、実装してみる。

TypeSafeMatcherについて

この例ではorg.hamcrest.TypeSafeMatcherというクラスを使っていて、これはhamcrest1.2以降で導入されている。
で、hamcrest-core1.2を使おうとしたら、mavenのセントラルレポジトリにないではないか。「Google Code Archive - Long-term storage for Google Code Project Hosting.」を見てみると、mavenレポジトリのアップロードに苦労しているっぽい。仕方がないので、ローカルにインストールして使う。

jdaveの例外記述バリエーション
  • 例外メッセージチェックの有無。
  • 例外クラスを厳密にチェックするか派生クラスを許可するか。

の組み合わせで、以下の4つが存在する。

  1. should.raise(Class expected)
  2. should.raise(Class expectedType, String expectedMessage)
  3. should.raiseExactly(Class expected)
  4. should.raiseExactly(Class expected,String expectedMessage)
assertThatメソッドのactualとBlockの相性

ひとまずJDaveのBlockインターフェースをそのまま使ってみたのだが、assertThatメソッドと第一引数として渡しているBlockの相性が悪い。

assertThatの第一引数actualに渡したオブジェクトは、toString()メソッドで実際の値をかえさなければいけないが、インターフェースを実装した匿名クラスだと、toStringで匿名クラス名しか表示できない。
かといって、toString()メソッドをそれぞれ実装するのは記述量がふえてしまって良くない。
ということでBlockを抽象クラスに変更して、Matcherでの評価時に発生した例外を保存出来るようにし、toString()メソッドでその例外の情報を返すようにした。

実際のコード

ひとまずbitbucketに置いてみた。
http://bitbucket.org/cnaos/hamcrest-exception-matcher/

主要なクラスの解説

Blockクラス

jdaveのBlockインターフェースとほぼ一緒。前述の理由によりassertThat()に適合するように修正している。

public abstract class Block {
    /** ブロックの実行時に発生した例外 */
    private String actualException="No Exception was thrown";

    /**
     * Evaluate this block.
     *
     * @throws Throwable if an exception is thrown during block evaluation.
     */
    public abstract void run() throws Throwable;

    /**
     * 発生した例外を設定します。
     * @param actualException
     */
    public void setActualException(String actualException) {
        this.actualException = actualException;
    }

    @Override
    public String toString() {
        return this.actualException;
    }
}
RaiseMatcherクラス

Block用のカスタムMatcher。
要となる部分は、matchesSafelyメソッドで、この部分で渡されたBlockを実行して、投げられた例外をチェックしている。

public class RaiseMatcher extends TypeSafeMatcher<Block> {

    /** 例外の期待値 */
    private final ExpectedException<? extends Throwable> expectation;

    /**
     * 
     * @param aExpectation 例外の期待値
     */
    public RaiseMatcher(ExpectedException aExpectation) {
        this.expectation = aExpectation;
    }

    @Factory
    public static <E extends Throwable> Matcher<Block> raise(final Class<E> expected) {
        return new RaiseMatcher(new ExpectedException<E>(expected));
    }

    @Factory
    public static <E extends Throwable> Matcher<Block> raise(final Class<E> expected, final String expectedMessage) {
        return new RaiseMatcher(new ExpectedExceptionWithMessage<E>(expected, expectedMessage));
    }

    @Factory
    public static <E extends Throwable> Matcher<Block> raiseExactly(final Class<E> expected) {
        return new RaiseMatcher(new ExactExpectedException<E>(expected));
    }

    @Factory
    public static <E extends Throwable> Matcher<Block> raiseExactly(final Class<E> expected, final String expectedMessage) {
        return new RaiseMatcher(new ExactExpectedExceptionWithMessage<E>(expected, expectedMessage));
    }

    /**
     *
     * @see jdave.Specification#specify(Block,ExpectedException)
     * @param block テストで実行するブロック
     * @return 指定したテスト条件を満たした場合はtrue
     */
    @Override
    protected boolean matchesSafely(final Block block) {
        try {
            block.run();
        } catch (final Throwable t) {
            block.setActualException(expectation.createDescription(t));
            if (!expectation.matches(t)) {
                return false;
            }
            return true;
        }
        return false;
    }

    /**
     * 投げられる事を期待している例外クラス名や例外メッセージを返す
     * @param description 期待する条件の説明を返す
     */
    @Override
    public void describeTo(Description description) {
        description.appendText("throw ");
        description.appendValue(expectation);
    }
}
ExpectedExceptionクラス

Blockから投げられる例外に関する期待条件クラス。
jdaveのExpectedExceptionをhamcrestMatcherに合うように改変した。

public class ExpectedException<T> {
    protected final Class<? extends T> expected;

    public ExpectedException(Class<? extends T> expected) {
        if(expected == null){
            throw new NullPointerException();
        }
        this.expected = expected;
    }

    public boolean matches(Throwable t) {
        return matchesType(t.getClass());
    }

    public String createDescription(Throwable t){
        return t.getClass().getName();
    }

    protected boolean matchesType(Class<? extends Throwable> actual) {
        return expected.isAssignableFrom(actual);
    }

    @Override
    public String toString() {
        return expected.getName();
    }
}

TwitterのOAuth認証を使ったサービスを開発する際の注意(その2)

前回のエントリで、対策が具体的でなかったのは、SHA-1などのハッシュ関数を使っても問題ないか自信が持てなかったためです。すみません。とりあえず自分で納得できる対策が調べられたと思うので書いておきます。

twitterIDをそのままSHA-1などの暗号学的ハッシュ関数にかけただけでは弱いので避けるということは知っていましたが、単なる固定文字列のsaltを付け加えるだけだと、常に同じ値が生成されるのでこれでいいのか自信が持てませんでした。

で、調べてみたら自動ログイン用のクッキーの値に必要な特性は、セッションIDの特性によくにていて、値の生成は「ユーザIDや時刻等の情報と擬似乱数とを混ぜ合わせた文字列に対してハッシュ関数を使用する」で問題なさそうだということが分かりました。
また、管理方法もに似ていて、「ログインごとに認証用のキーとなる値を新しい値に更新しなければいけない」ということも分かりました。

以下の情報を参考にしました

「解答:まちがった自動ログイン処理」で参考にした箇所

セキュリティのベストプラクティスとしてはクッキーにユーザに関連するいかなる情報(暗号化された情報を含む)も保存すべきではありません。クッキーにユーザに関連した情報(ユーザID、パスワード、メール、氏名、etc)を保存しなければならないシステムである場合、設計を見直し、ユーザ情報をクッキーに保存しなくても良い設計にしなければなりません。クッキーに保存しても良い値は、表示設定などのユーザ情報と関係の無い情報、セッションIDなどの予測不可能なランダムな値のみです。

ユーザIDくらいは大丈夫かと思ってたけど、そうだったのか。

【まとめ】

  • 自動ログイン機能は基本的にセキュリティ上のリスクを増加させるので安全性が重要なサービスでは実装しない。
  • 自動ログインの実装にはセッション管理と同様にランダムなクッキーの値を使用する。
  • 自動ログイン用のクッキーはログインの度に新しい値に更新する。(古い鍵の削除も忘れない)
  • ログイン時に自動ログインオプションが無効かつ自動ログイン用のクッキーが設定されている場合は自動ログインテーブルの該当レコードと自動ログインクッキーも削除する。
  • 自動ログイン用のクッキーは自動ログイン処理を行う場合にのみ必要なので自動ログイン専用のディレクトリを設定し、そのディレクトリのURLがリクエストされた場合にのみ送信する。(XSSリスクの低減)
  • ログアウトした場合、自動ログインクッキーが設定されている場合、自動ログインテーブルの該当レコードと自動ログインクッキーも削除する。
  • 実際の運用では古い自動ログイン用のレコードが溜まるので定期的に不要なレコードを削除する。

「安全なウェブサイトの作り方」で参考にした箇所

1.4 セッション管理の不備」が参考になった。
以下は項目名だけ抜粋

1) セッション ID を推測が困難なものにする
2) セッション ID を URL パラメータに格納しないようにする
3) HTTPS 通信で利用する Cookie には secure 属性を加える
4-1) ログイン成功後に、新しくセッションを開始するようにする
または
4-2) ログイン成功後に、既存のセッションIDとは別に秘密情報を発行し、ページの遷移毎にその値を確認する

IPA セキュア・プログラミング講座:Webアプリケーション編」で参考にした箇所

自動ログイン用のキーの特性として、セッションIDと同じ特性が必要らしいので、「第4章セッション対策セッション乗っ取り:#2 セッションID強度を高める」が参考になった。

(1) 公共に知られていそうなIDは使用しない
例えば、会員番号やユーザID等、公共に知られていそうな値では容易に推測が可能であるので、使用すべきではない。セッションIDは、人間が覚えておく必要は無いので、見た目が無意味な値であっても、まったく問題はない。

(2) ランダムなIDを使用する
00001、00002...等の規則性や連続性のある値は、推測が容易であるのでランダムなIDを使用する。例えば、ユーザIDや時刻等の情報と擬似乱数とを混ぜ合わせた文字列に対してハッシュ関数を使用することで、ランダムなIDが生成されるようになる。
ただし、擬似乱数生成関数やハッシュ関数を自前で作成することは避けるべきである。

(3) 桁数と文字種を多くする
桁数が短く、文字種の少ない場合、ブルートフォースアタックで破られてしまうおそれがある。これを防ぐためには、使用する文字種、桁数をなるべく多くし、総当りするパターンを増やすことである。例えば、英数小文字大文字で20桁の値を考えた場合、組み合わせとしては「(26+26+10)^20」通りとなる。

(4) ユニークなセッションIDを発行する
セッションIDは、どのような場合でも、常にユニークな値を発行するようにする。

  • Webアプリケーションを利用する他ユーザに対して発行する場合

同じセッションIDを発行してしまうと「なりすまし」のおそれがある。

  • 同一ユーザに対して発行する場合

セッションの度に同じIDを発行することがないようにする。これは一度セッションIDが盗まれてしまうと、Webアプリケーション内での「なりすまし」が永続的に可能となってしまうからである。

IPAの資料には、セッション設計のガイドラインはあるけど、自動ログインについての記述が無かったので、追加して欲しいと思った。

TwitterのOAuth認証を使ったサービスを開発する際の注意

OAuth を使ってソーシャル・ネットワーキング Web サイトにアクセスする: 第 2 回: OAuth 対応のWeb 版 Twitter クライアントを作成する」のサンプルをいじくっていて気づいたんですが、TwitterOAuth認証を使ったサービスを開発する場合、AccessTokenの管理方法に注意が必要です。
あんまり自信がないので、誤りがあればどんどん指摘してください。

以下の条件が成り立つ場合、サービス内でなりすましができてしまいます。

  1. サービス(Consumer)がtwitterIDとAccessTokenをひもづけて保管している。
  2. サービスがブラウザへ渡すcookieにtwitterIDをそのまま保存している
  3. ブラウザからサービスへtwitterIDを含むCookieが渡された場合、twitterIDをキーとしてサービス内に保存しているAccessTokenを検索し、存在していれば認証済みであるとみなす。
  4. なりすまし対象のアカウントがすでにOAuth認証済みで、サービス内になりすまし対象アカウントのAccessTokenが保存されている。

想定されるシナリオ

"サービス"はtwitterAPIを利用するWebアプリケーションです。
"Alice"(通常のユーザ)と"Carol"(なりすまし攻撃者)がいて、AliceがTwitterAPIを利用するサービスにOAuthで許可を与えたとします。

twitterからサービスへAlice用のAccessTokenが送信され、サービスはAliceのtwitterIDとひもづけて保存します。
twitterの認証画面からサービスの画面に戻った時に、サービスからAliceのブラウザにtwitterIDを含むcookieが送信されます。

次にAliceがサービスにアクセスした際に、サービスはcookieからtwitterIDを取得し、そのIDに対応するAccessTokenを取り出します。対応するAccessTokenが存在していれば、サービスはAliceがOAuthで許可を与えているものと見なします。

Carolはサービスにアクセスする際にAliceのスクリーンネームからtwitterIDを割り出し、AliceのtwitterIDを含むcookieを偽造することでサービス内でAliceになりすますことができます。

twitteIdはintの数値であり、これはTwitterAPIを使うことでスクリーンネームから容易に取得できます。ブラウザから確認したいのであれば、Twitter API Viewerのサービスを使ってみてください。

対策

サービス内でAccessTokenを保存する際のキーや、cookieに保存する値として適切なのは、以下の条件を満たすものです。

  • 容易に予測不可能
  • 一意で、ハッシュ値のように衝突しない

最後にひとこと

今後、このような事故がおこった場合、「OAuth認証によってパスワードによる認証が無くなってしまったため、これまで安全と思って送受信したり保存したりしていたIDがセキュリティ上の弱点となってしまった事例」とか言われるんだと思います。
これからOAuth対応でいろいろ手が入るサービスが出てくるでしょうが、気になる人はcookieにtwitteridが入っていないか注意してください。