Base64でデコード出来ない文字をデコードしようとするとどうなるか?

調べても出てこなかったので、メモ。

何かわかりやすいエラーが出るのかとおもいきや、エラーとわかりやすい挙動はしなかった…orz

答えは『実装による』っぽい。 実際に平文→base64.decode→sha256でhash→base64.encodeの処理をOpensslとJavaの両方で試したら挙動に違いがった。

例えばOpenSSLだとこうなる

$ echo "aaaa" | openssl base64 -d | openssl sha -sha256 -binary | openssl base64
# u6nbXovptodruQ0AGBFeI/x0G6a/IyXn/PiO/tdQxMc=
$ echo "aaaa,,,," | openssl base64 -d | openssl sha -sha256 -binary | openssl base64
# 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=

aaaaの場合とaaaa,,,,の場合で結果が異なる。 ,base64で使用していない文字なのでデコード出来ない。 そのため、,が入るとエラーになって、その結果毎回同じ文字列が返ってきている(ように見える)。

試しにaaaaaaaa,も試してみる

$ echo "aaaaaaa," | openssl base64 -d | openssl sha -sha256 -binary | openssl base64
# 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=

やっぱり47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=になる。

なぜ47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=が返ってくるかはOpenSSLの中身を見ていないのでわからない。

Apache Commons Codecだとこうなる

package Base64Test;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.apache.commons.codec.binary.Base64;

public class Base64TestExecutor {
    public static void main(String[] agrs) throws NoSuchAlgorithmException{
        String target1 = "aaaa";
        String target2 = "aaaa,,,,";
        String result1 = testDigest(target1);
        String result2 = testDigest(target2);
        System.out.println("result1 : " + result1);
        System.out.println("result2 : " + result2);
        System.out.println("isEqual : " + result1.equals(result2));
    }

    private static String testDigest(String targetString) throws NoSuchAlgorithmException {
        
        byte[] testResult = Base64.decodeBase64(targetString);
        
        MessageDigest md = MessageDigest.getInstance("SHA-256");
        byte[] digestVal = md.digest(testResult);
        String encPassword = Base64.encodeBase64String(digestVal);
        
        return encPassword;
    }
}

実行結果

run:
result1 : u6nbXovptodruQ0AGBFeI/x0G6a/IyXn/PiO/tdQxMc=
result2 : u6nbXovptodruQ0AGBFeI/x0G6a/IyXn/PiO/tdQxMc=
isEqual : true
ビルド成功(合計時間: 0秒)

こっちは中身を見てみた。

    /**
     * <p>
     * Decodes all of the provided data, starting at inPos, for inAvail bytes. Should be called at least twice: once
     * with the data to decode, and once with inAvail set to "-1" to alert decoder that EOF has been reached. The "-1"
     * call is not necessary when decoding, but it doesn't hurt, either.
     * </p>
     * <p>
     * Ignores all non-base64 characters. This is how chunked (e.g. 76 character) data is handled, since CR and LF are
     * silently ignored, but has implications for other bytes, too. This method subscribes to the garbage-in,
     * garbage-out philosophy: it will not check the provided data for validity.
     * </p>
     * <p>
     * Thanks to "commons" project in ws.apache.org for the bitwise operations, and general approach.
     * http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
     * </p>
     *
     * @param in
     *            byte[] array of ascii data to base64 decode.
     * @param inPos
     *            Position to start reading data from.
     * @param inAvail
     *            Amount of bytes available from input for encoding.
     * @param context
     *            the context to be used
     */
    @Override
    void decode(final byte[] in, int inPos, final int inAvail, final Context context) {
        if (context.eof) {
            return;
        }
        if (inAvail < 0) {
            context.eof = true;
        }
        for (int i = 0; i < inAvail; i++) {
            final byte[] buffer = ensureBufferSize(decodeSize, context);
            final byte b = in[inPos++];
            if (b == pad) {
                // We're done.
                context.eof = true;
                break;
            } else {
                if (b >= 0 && b < DECODE_TABLE.length) {
                    final int result = DECODE_TABLE[b];
                    if (result >= 0) {
                        context.modulus = (context.modulus+1) % BYTES_PER_ENCODED_BLOCK;
                        context.ibitWorkArea = (context.ibitWorkArea << BITS_PER_ENCODED_BYTE) + result;
                        if (context.modulus == 0) {
                            buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 16) & MASK_8BITS);
                            buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS);
                            buffer[context.pos++] = (byte) (context.ibitWorkArea & MASK_8BITS);
                        }
                    }
                }
            }
        }

        // Two forms of EOF as far as base64 decoder is concerned: actual
        // EOF (-1) and first time '=' character is encountered in stream.
        // This approach makes the '=' padding characters completely optional.
        if (context.eof && context.modulus != 0) {
            final byte[] buffer = ensureBufferSize(decodeSize, context);

            // We have some spare bits remaining
            // Output all whole multiples of 8 bits and ignore the rest
            switch (context.modulus) {
//              case 0 : // impossible, as excluded above
                case 1 : // 6 bits - ignore entirely
                    // TODO not currently tested; perhaps it is impossible?
                    break;
                case 2 : // 12 bits = 8 + 4
                    context.ibitWorkArea = context.ibitWorkArea >> 4; // dump the extra 4 bits
                    buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS);
                    break;
                case 3 : // 18 bits = 8 + 8 + 2
                    context.ibitWorkArea = context.ibitWorkArea >> 2; // dump 2 bits
                    buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS);
                    buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS);
                    break;
                default:
                    throw new IllegalStateException("Impossible modulus "+context.modulus);
            }
        }
    }

final int result = DECODE_TABLE[b]; の箇所でDECODE出来ない文字の場合は-1が返される。 そのため、result >= 0falseになるので、その文字は読み飛ばされる。 その結果、例えばaa,,,の結果はおなじになる。

こういうのは実際に試して動作確認して、中身のソースまで見てみないとわかんないですね!