Tuesday, 20 September 2016

Base32 Encoding and Decoding in C#

Gist available at https://gist.github.com/BravoTango86/2a085185c3b9bd8383a1f956600e515f
/*
 * Derived from https://github.com/google/google-authenticator-android/blob/master/AuthenticatorApp/src/main/java/com/google/android/apps/authenticator/Base32String.java
 * 
 * Copyright (C) 2016 BravoTango86
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;

public static class Base32 {

    private static readonly char[] DIGITS;
    private static readonly int MASK;
    private static readonly int SHIFT;
    private static Dictionary<char, int> CHAR_MAP = new Dictionary<char, int>();
    private const string SEPARATOR = "-";

    static Base32() {
        DIGITS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".ToCharArray();
        MASK = DIGITS.Length - 1;
        SHIFT = numberOfTrailingZeros(DIGITS.Length);
        for (int i = 0; i < DIGITS.Length; i++) CHAR_MAP[DIGITS[i]] = i;
    }

    private static int numberOfTrailingZeros(int i) {
        // HD, Figure 5-14
        int y;
        if (i == 0) return 32;
        int n = 31;
        y = i << 16; if (y != 0) { n = n - 16; i = y; }
        y = i << 8; if (y != 0) { n = n - 8; i = y; }
        y = i << 4; if (y != 0) { n = n - 4; i = y; }
        y = i << 2; if (y != 0) { n = n - 2; i = y; }
        return n - (int)((uint)(i << 1) >> 31);
    }

    public static byte[] Decode(string encoded) {
        // Remove whitespace and separators
        encoded = encoded.Trim().Replace(SEPARATOR, "");

        // Remove padding. Note: the padding is used as hint to determine how many
        // bits to decode from the last incomplete chunk (which is commented out
        // below, so this may have been wrong to start with).
        encoded = Regex.Replace(encoded, "[=]*$", "");

        // Canonicalize to all upper case
        encoded = encoded.ToUpper();
        if (encoded.Length == 0) {
            return new byte[0];
        }
        int encodedLength = encoded.Length;
        int outLength = encodedLength * SHIFT / 8;
        byte[] result = new byte[outLength];
        int buffer = 0;
        int next = 0;
        int bitsLeft = 0;
        foreach (char c in encoded.ToCharArray()) {
            if (!CHAR_MAP.ContainsKey(c)) {
                throw new DecodingException("Illegal character: " + c);
            }
            buffer <<= SHIFT;
            buffer |= CHAR_MAP[c] & MASK;
            bitsLeft += SHIFT;
            if (bitsLeft >= 8) {
                result[next++] = (byte)(buffer >> (bitsLeft - 8));
                bitsLeft -= 8;
            }
        }
        // We'll ignore leftover bits for now.
        //
        // if (next != outLength || bitsLeft >= SHIFT) {
        //  throw new DecodingException("Bits left: " + bitsLeft);
        // }
        return result;
    }


    public static string Encode(byte[] data, bool padOutput = false) {
        if (data.Length == 0) {
            return "";
        }

        // SHIFT is the number of bits per output character, so the length of the
        // output is the length of the input multiplied by 8/SHIFT, rounded up.
        if (data.Length >= (1 << 28)) {
            // The computation below will fail, so don't do it.
            throw new ArgumentOutOfRangeException("data");
        }

        int outputLength = (data.Length * 8 + SHIFT - 1) / SHIFT;
        StringBuilder result = new StringBuilder(outputLength);

        int buffer = data[0];
        int next = 1;
        int bitsLeft = 8;
        while (bitsLeft > 0 || next < data.Length) {
            if (bitsLeft < SHIFT) {
                if (next < data.Length) {
                    buffer <<= 8;
                    buffer |= (data[next++] & 0xff);
                    bitsLeft += 8;
                } else {
                    int pad = SHIFT - bitsLeft;
                    buffer <<= pad;
                    bitsLeft += pad;
                }
            }
            int index = MASK & (buffer >> (bitsLeft - SHIFT));
            bitsLeft -= SHIFT;
            result.Append(DIGITS[index]);
        }
        if (padOutput) {
            int padding = 8 - (result.Length % 8);
            if (padding > 0) result.Append(new string('=', padding == 8 ? 0 : padding));
        }
        return result.ToString();
    }

    private class DecodingException : Exception {
        public DecodingException(string message) : base(message) {
        }
    }
}

No comments:

Post a Comment