PayPayは2020年8月より「JPQR」の申込受付を開始しました。この「JPQR」は、お店が複数のキャッシュレス決済サービスを1つのQRコードで導入することができるものです。本記事では、PayPayがJPQRにまつわる技術的な課題に対し、どのように解決していったかに焦点を当てていきます。
※JPQRのビジネスに関する情報や、コードに格納されているデータや利用方法については、公式仕様書をご覧ください。
課題
JPQRに対応するにあたり、いくつかの技術的な課題を解決する必要がありました。ひとつが、JPQRコードのパース(構文解析)でした。そしてこれは、さらに2つの課題解決が必要です。
- コードをデータオブジェクトに変換する
- コードの検証
これまでにもお伝えしていますが、PayPayでは主にJavaなどのJVM言語を使用しているため、ここではJavaを使用したソリューションを説明しますが、ほとんどの言語で同じことが可能です。
JPQRをTLVとして解析
JPQRは、TLVデータ構造の形式を持っています。TLVは、Type-Length-Valueの略です。複数のレコードを含む長い文字列であり、各レコードに2つのメタフィールドがあります。ひとつめにはIDかレコードタイプ、ふたつめには値の長さが含まれます。メタフィールドの長さは、TLVレコードごとに事前に定義されるものですが、JPQRの場合は「2」です。
TLVフィールドの値は、それ自体を新たなTLVにすることができるので、階層データを表すことに使えます。JPQRはこの手法を活用し、例えば以下に示す薄い色でJPQRの識別番号(以下、タイプ26)を表します。

JPQRには追加のTLVである2つのフィールドしかなく、2つは常に同じであるため、階層TLVをパースする一般的なソリューションだとやや過剰となります。代わりに、JPQRをパースした後でこれらのフィールドに対してTLVパースを再度実行するだけで十分です。
このオープンソースのTLVライブラリに、JavaでTLVをパースする方法が例示されています。文字列操作によるカスタム実装も簡単に作成できます。
JPQRのCRC認証
最後のキー(ID 63)には、JPQRを認証するために使用するCRCチェックサムが含まれています。CRCはCyclic Redundancy Check(巡回冗長検査)の略で、データブロックの検証に使われる一般的な手法です。通信や保管にも使われています。人気はあるものの、標準化されていないため、多数のCRCアルゴリズムが現在出回っています。Ross N.WilliamsのA Painless Guide to CRC Error Detection Algorithmsという記事を見ると、CRCについてわかりやすく説明されています。
検証チェックサムを実行するにあたって、データチャンク全体から小さなデータ結果を出すことができれば、どんな計算方式でも構いません。一例として、バイトの合計のモジュロの計算があります。ただし、Sumのような演算の問題点は、検証されたデータが少し変わると、チェックサムも少しだけ変わるという点です。本来、データ中に小さなエラーがあった場合、チェックサムとデータの両方にエラーがあるかもしれないので、チェックサムに大きく影響してほしいわけです。ふたつの小さい誤変更がある場合、両方ともに変更が小さければ(例えばそれぞれ1ビットの場合)互いに打ち消し合う可能性が高くなります。チェックサムに大きな変更を加えることで、エラーチェックの耐障害性を高めることができます。
このためCRCは特定のビット系列に対し、polinomと呼ばれるXOR演算を使用します。この操作はバイナリ分割を単純化したものなので、これに則して評価されます。左側のビットから開始し、一番左のビットが1であるたびにxorを実行し、0でスキップします。計算中、中間結果はレジスタと呼ばれます。
11010011101100 000 <--- input right padded by 3 bits, one bit less then the poly
1011 <--- poly used as divisor
1100 <--- intermediate result, also called the register
1011
1110
1011
1011
1011
1101 <--- note that the divisor moves over to align with the next 1 in the dividend (since quotient for that step was zero)
1011 (in other words, it doesn't necessarily move one bit per iteration)
1101
1011
1100
1011
1110
1011
101 0
101 1
-----------------
100 <--- remainder (3 bits). Division algorithm stops here as dividend is equal to zero. This will be the resulting CRC
このアルゴリズムの実装は非常に単純ですが、さらに効率よくやる方法もあります。ひとつは、ビットごとに行うのではなく、シーケンスのチェックサムを事前に計算して使用することです。例えば、各バイトに対してチェックサムを事前に計算するとキャッシュサイズは255になりますが、必要とされるステップは8分の1に削減できます。キャッシュサイズが大きいほど高速になりますが、キャッシュサイズは指数関数的に増加します。
ほとんどのCRCアルゴリズムには、次の特性があります。
- 多項式のサイズ:結果のCRCチェックサムは1ビット小さくなります。チェックサムが大きいほど堅牢ですが、より多くのストレージ容量を必要とします。すべての場合において、バイトの倍数と2の倍数を持つことには意味があります。例えば、ほとんどのCRCアルゴリズムは、8ビット、16ビット、32ビットなどです。
- 多項式のタイプ:適切な多項式を選択するために様々な検証が盛んに行われていますが、多項式自体もCRCの種類を非常に多くしています。
- CRCの初期値:通常は全て0または1ビットです。
- データビットの順序:このフラグは、各バイトのビットの順序を逆にするかどうかを指定します。これは、逆の順序でデータを送信するハードウェアのデータパッケージを検証する場合に便利です。
- レジスタビットの順序:このフラグは、レジスタ(計算されたCRC)のビットを、最後のXORステージの前に反転するかどうかを指定します(次のパラメータを参照)。
- 最終のXOR:特定のビット順序に対して最後にXORを実行するアルゴリズムもあります。これが完全に0ビットの場合、CRCは変更されません。
標準化されたアルゴリズムの参考に、こちらに16ビットCRCを計算するための22個のアルゴリズムが提示されています。
JPQRは、CRC-16/CCITT-FALSEまたはCRC-16/IBM-3740で標準化されたアルゴリズムを使用します。
完全なパラメータリストは次のとおりです(ほとんどの実装で使用される変数の名称がカッコに入っています)。
多項式のサイズ(width) | 16 |
多項式(poly)のタイプ | 0x1021 |
レジスタ(init)の初期値 | 0xfff |
データビットの順序(refin) | false |
レジスタビット順序(refout) | false |
最終XOR(xorout) | 0x0000 |
JPQRのCRCを検証するには最初にコードを2つの部分、検証されるデータと提供されるCRCとに分割する必要があります。CRCは、レコードIDおよびサイズフィールドを持つTLVレコードです。CRCのメタフィールドは、検証が必要なデータの一部です。Java CRCは、上述のRoss N.Williamsの記事に基づくJava実装です。JPQRを検証するコードは、次のように表示されます。
public class JpqrService {
private final CRC crcCalculator;
public JpqrService() {
crcCalculator = new CRC(CRC.Parameters.CCITT);
}
public boolean verifyCrc(String jpqr) {
String dataToVerify = jpqr.substring(0, jpqr.length() - 4);
String providedCrc = jpqr.substring(jpqr.length() - 4);
long crc = crcCalculator.calculateCRC(dataToVerify.getBytes());
String actualCrc = StringUtils.leftPad(Long.toHexString(crc).toUpperCase(), 4, "0");
return providedCrc.equals(actualCrc);
}
}
ライブラリは、CCITTという名前を使用してCRC-16/CCITT-FALSEを実装しますが、正確な名前ではありません(CCITTはCCITT-TRUEまたはKERMITのエイリアスです)。違いは初期パラメータで、JPQRの場合は0x1111にする必要があります。CRCオブジェクトを保存して再利用することで、キャッシングおよび最適化されたパフォーマンスにアクセスできます。上記の例では、CRCオブジェクトは簡単にするためにコントローラ内でインスタンス化されますが、テスト容易性を高めるためには、ファクトリクラスまたはdependency injectionによって行うのが最適です。
まとめ
JPQRのパースはやや困難ですが、作業を単純化するのに役立つ多くのオープンソースツールがあります。他のプログラミング言語用でも、似たツールが提供されています。
PayPayでは、TLVデータ構造の解析とCRCのチェックの両方のためのカスタム実装を作成し、第3者ライブラリとライセンスへの依存を減らし、柔軟なパラメータ化を排除することでパフォーマンスを向上させました。QRコード解析は、毎日何百万回も使用されるPayPayの中核的な機能であるため、パフォーマンスを少しでも向上させることが重要です。