Base32解码

45

我有一个base32字符串,需要将其转换为字节数组。但我在.NET框架中找不到相应的转换方法。我能找到base64的方法,但没有base32的。

类似Convert.FromBase64String这样的方法,如果有一个对于base32的方法就太完美了。

在.NET框架中是否有这样的方法,还是说我必须自己实现?


@Jamie 能否解释一下?被接受的答案到底有何问题,可能会造成什么“伤害”? - Yossi Sternlicht
@JoeStarbright,被采纳的答案已经改变。当我发表那条评论时,被采纳的答案是下面的一个。它只是复制了一个旧的.NET实现,包括所有缺陷(不符合标准)。我刚刚删除了那条评论,以避免进一步的混淆,因为它已经不再有效。 - Jamie
7个回答

130

我需要一个base32编解码器,所以今天下午花了几个小时把它搞定。我相信它符合这里列出的标准:https://www.rfc-editor.org/rfc/rfc4648#section-6

public class Base32Encoding
{
    public static byte[] ToBytes(string input)
    {
        if (string.IsNullOrEmpty(input))
        {
            throw new ArgumentNullException("input");
        }

        input = input.TrimEnd('='); //remove padding characters
        int byteCount = input.Length * 5 / 8; //this must be TRUNCATED
        byte[] returnArray = new byte[byteCount];
        
        byte curByte = 0, bitsRemaining = 8;
        int mask = 0, arrayIndex = 0;

        foreach (char c in input)
        {
            int cValue = CharToValue(c);

            if (bitsRemaining > 5)
            {
                mask = cValue << (bitsRemaining - 5);
                curByte = (byte)(curByte | mask);
                bitsRemaining -= 5;
            }
            else
            {
                mask = cValue >> (5 - bitsRemaining);
                curByte = (byte)(curByte | mask);
                returnArray[arrayIndex++] = curByte;
                curByte = (byte)(cValue << (3 + bitsRemaining));
                bitsRemaining += 3;
            }
        }

        //if we didn't end with a full byte
        if (arrayIndex != byteCount)
        {
            returnArray[arrayIndex] = curByte;
        }

        return returnArray;
    }

    public static string ToString(byte[] input)
    {
        if (input == null || input.Length == 0)
        {
            throw new ArgumentNullException("input");
        }

        int charCount = (int)Math.Ceiling(input.Length / 5d) * 8;
        char[] returnArray = new char[charCount];

        byte nextChar = 0, bitsRemaining = 5;
        int arrayIndex = 0;

        foreach (byte b in input)
        {
            nextChar = (byte)(nextChar | (b >> (8 - bitsRemaining)));
            returnArray[arrayIndex++] = ValueToChar(nextChar);
            
            if (bitsRemaining < 4)
            {
                nextChar = (byte)((b >> (3 - bitsRemaining)) & 31);
                returnArray[arrayIndex++] = ValueToChar(nextChar);
                bitsRemaining += 5;
            }
            
            bitsRemaining -= 3;
            nextChar = (byte)((b << bitsRemaining) & 31);
        }

        //if we didn't end with a full char
        if (arrayIndex != charCount)
        {
            returnArray[arrayIndex++] = ValueToChar(nextChar);
            while (arrayIndex != charCount) returnArray[arrayIndex++] = '='; //padding
        }

        return new string(returnArray);
    }

    private static int CharToValue(char c)
    {
        int value = (int)c;
        
        //65-90 == uppercase letters
        if (value < 91 && value > 64)
        {
            return value - 65;
        }
        //50-55 == numbers 2-7
        if (value < 56 && value > 49)
        {
            return value - 24;
        }
        //97-122 == lowercase letters
        if (value < 123 && value > 96)
        {
            return value - 97;
        }

        throw new ArgumentException("Character is not a Base32 character.", "c");
    }

    private static char ValueToChar(byte b)
    {
        if (b < 26)
        {
            return (char)(b + 65);
        }

        if (b < 32)
        {
            return (char)(b + 24);
        }

        throw new ArgumentException("Byte is not a value Base32 value.", "b");
    }

}

7
+1 太棒了!我一直在寻找一个好的 base32 编码/解码器。我使用 RFC 测试向量设置了一堆单元测试,但是没有一个 (我试过的) 能够通过所有测试。你的第一次就成功了,干得漂亮!我很惊讶为什么没有更多点赞。 - Devin
2
我的建议是:将 "charCount = (int)Math.Ceiling(input.Length / 5d) * 8;" 更改为 "(int)Math.Ceiling(input.Length / 5d * 8)",以去除多余的 "=" 填充。 - Darkthread
1
你好,我在正在开发的一个应用程序中使用了你的代码,我可以在给予适当的信用的情况下将其重新许可为MIT许可证吗?你可以在https://github.com/kappa7194/otp/blob/master/Albireo.Otp.Library/Base32.cs找到这段代码。 - Albireo
1
你好,我正在使用你的代码,并希望在http://www.opengl-tutorial.org/download/下获得许可证。 - Dirk Bester
谢谢,这是我在漫长的搜索后找到的第一个可行的答案。代码很棒。 - Ka0s
显示剩余2条评论

15

请查看这个针对.NET的FromBase32String实现,可以在此处找到。


编辑:上面的链接已经失效,您可以在archive.org中找到存档副本。

实际代码如下:

using System;
using System.Text;

public sealed class Base32 {

      // the valid chars for the encoding
      private static string ValidChars = "QAZ2WSX3" + "EDC4RFV5" + "TGB6YHN7" + "UJM8K9LP";

      /// <summary>
      /// Converts an array of bytes to a Base32-k string.
      /// </summary>
      public static string ToBase32String(byte[] bytes) {
            StringBuilder sb = new StringBuilder();         // holds the base32 chars
            byte index;
            int hi = 5;
            int currentByte = 0;

            while (currentByte < bytes.Length) {
                  // do we need to use the next byte?
                  if (hi > 8) {
                        // get the last piece from the current byte, shift it to the right
                        // and increment the byte counter
                        index = (byte)(bytes[currentByte++] >> (hi - 5));
                        if (currentByte != bytes.Length) {
                              // if we are not at the end, get the first piece from
                              // the next byte, clear it and shift it to the left
                              index = (byte)(((byte)(bytes[currentByte] << (16 - hi)) >> 3) | index);
                        }

                        hi -= 3;
                  } else if(hi == 8) { 
                        index = (byte)(bytes[currentByte++] >> 3);
                        hi -= 3; 
                  } else {

                        // simply get the stuff from the current byte
                        index = (byte)((byte)(bytes[currentByte] << (8 - hi)) >> 3);
                        hi += 5;
                  }

                  sb.Append(ValidChars[index]);
            }

            return sb.ToString();
      }


      /// <summary>
      /// Converts a Base32-k string into an array of bytes.
      /// </summary>
      /// <exception cref="System.ArgumentException">
      /// Input string <paramref name="s">s</paramref> contains invalid Base32-k characters.
      /// </exception>
      public static byte[] FromBase32String(string str) {
            int numBytes = str.Length * 5 / 8;
            byte[] bytes = new Byte[numBytes];

            // all UPPERCASE chars
            str = str.ToUpper();

            int bit_buffer;
            int currentCharIndex;
            int bits_in_buffer;

            if (str.Length < 3) {
                  bytes[0] = (byte)(ValidChars.IndexOf(str[0]) | ValidChars.IndexOf(str[1]) << 5);
                  return bytes;
            }

            bit_buffer = (ValidChars.IndexOf(str[0]) | ValidChars.IndexOf(str[1]) << 5);
            bits_in_buffer = 10;
            currentCharIndex = 2;
            for (int i = 0; i < bytes.Length; i++) {
                  bytes[i] = (byte)bit_buffer;
                  bit_buffer >>= 8;
                  bits_in_buffer -= 8;
                  while (bits_in_buffer < 8 && currentCharIndex < str.Length) {
                        bit_buffer |= ValidChars.IndexOf(str[currentCharIndex++]) << bits_in_buffer;
                        bits_in_buffer += 5;
                  }
            }

            return bytes;
      }
}

1
+1 很好...他可能需要将编码数字更改为“标准”数字,但这并不糟糕。 - Daniel LeCheminant
2
没有给我预期的结果。 - Lars Truijens
好的,FromBase32String()对于无效字符不会抛出ArgumentException(或任何其他异常) - 因为IndexOf也不会。 如果未找到字符,IndexOf将返回-1。 - spacer
一个解决方案是为 string 定义一个扩展方法,例如 "IndexOfEx",它会抛出异常而不是返回 -1。 - spacer
希望大家可以看一下第二个解决方案,而不是盲目地选择这个。 - Jamie
显示剩余2条评论

7
这是我的编码和解码函数。我觉得它们比其他建议要短小精悍。所以如果你需要一个小巧的,可以试试这些。
public static string BytesToBase32(byte[] bytes) {
    const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
    string output = "";
    for (int bitIndex = 0; bitIndex < bytes.Length * 8; bitIndex += 5) {
        int dualbyte = bytes[bitIndex / 8] << 8;
        if (bitIndex / 8 + 1 < bytes.Length)
            dualbyte |= bytes[bitIndex / 8 + 1];
        dualbyte = 0x1f & (dualbyte >> (16 - bitIndex % 8 - 5));
        output += alphabet[dualbyte];
    }
    
    return output;
}

public static byte[] Base32ToBytes(string base32) {
    const string alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
    List<byte> output = new List<byte>();
    char[] bytes = base32.ToCharArray();
    for (int bitIndex = 0; bitIndex / 5 + 1 < bytes.Length; bitIndex += 8) {
        int dualbyte = alphabet.IndexOf(bytes[bitIndex / 5]) << 10;
        if (bitIndex / 5 + 1 < bytes.Length)
            dualbyte |= alphabet.IndexOf(bytes[bitIndex / 5 + 1]) << 5;
        if (bitIndex / 5 + 2 < bytes.Length)
            dualbyte |= alphabet.IndexOf(bytes[bitIndex / 5 + 2]);

        dualbyte = 0xff & (dualbyte >> (15 - bitIndex % 5 - 8));
        output.Add((byte)(dualbyte));
    }
    return output.ToArray();
}

编辑:根据@Patrick的修正进行更新

4
我很喜欢这个算法的易读性。但是,如果源字节数不是5的倍数,解码会在末尾添加一个零字节。 - sidon
1
for 循环条件更改为 bitIndex / 5 + 1 < bytes.Length - Patrick
在@Patrick的建议下修复后,这个代码变得简短而优雅,并且通过了RFC4648测试向量的验证。 另一个改进是跳过base32.ToCharArray(),而是使用StringBuilder。还可以通过预先分配对话所需的数据来进一步改进它。(并在函数之间重用常量) - NiKiZe
在@Patrick的建议下修复后,这个代码变得简短而优雅,并且通过了RFC4648的测试向量验证。 另一个改进是跳过base32.ToCharArray(),而是使用StringBuilder。还可以通过预先分配对话所需的数据来进一步改进代码(并在函数之间重用常量)。 - undefined

6

这是一个很老的问题,但我碰巧想要为OTP令牌做同样的事情。结果发现OTP.NET包中有内置的32进制功能:

Base32Encoding.ToBytes("(your base 32 string here)")

相反的也是可能的:
Base32Encoding.ToString(new byte[] { /* your bytes here */ })

4
Otp.NET的源代码指向了关于这个问题的Shane's answer…… - Taylor Buchanan
很高兴知道@TaylorBuchanan!我认为现在有一个库,这仍然是好的消息,所以不需要重新发明它;) - Joe Skeen

5

我编写了一些基于灵活的标准实现的各种Base32和Base64编码/解码方法,特别是:base64url(按照rfc4648)及其base32等效。

默认情况下,Base32Url类仅使用A到Z和2到7这些字符进行编码。不使用连字符、下划线、加号、斜杠或等号,使其可用作URL令牌在几乎所有情况下。Base32Url还支持自定义字母表、区分大小写/不区分大小写、填充/不填充等功能。

这个已经发布在code project上了。


1
这是我快速记录的解决方案。它只适用于由8个base32字符组成的字符串。但确实有效。
public class Base32
{
  /// <summary>
  /// Decode a Base32 string
  /// This will only work on a multiple of 40 bits (5 bytes)
  /// http://www.garykessler.net/library/base64.html
  /// </summary>
  public static byte[] Decode(string Base32String)
  {
    // Ignore hyphens
    string str = Base32String.Replace("-", "");

    // Convert it to bits
    List<byte> bits = new List<byte>();
    foreach (char c in str)
    {
      int i = CharToValue(c);
      bits.Add((byte)((i & 16) > 0 ? 1 : 0));
      bits.Add((byte)((i & 8) > 0 ? 1 : 0));
      bits.Add((byte)((i & 4) > 0 ? 1 : 0));
      bits.Add((byte)((i & 2) > 0 ? 1 : 0));
      bits.Add((byte)((i & 1) > 0 ? 1 : 0));
    }

    // Convert bits into bytes
    List<byte> bytes = new List<byte>();
    for (int i = 0; i < bits.Count; i += 8)
    {
      bytes.Add((byte)(
        (bits[i + 0] << 7) +
        (bits[i + 1] << 6) +
        (bits[i + 2] << 5) +
        (bits[i + 3] << 4) +
        (bits[i + 4] << 3) +
        (bits[i + 5] << 2) +
        (bits[i + 6] << 1) +
        (bits[i + 7] << 0)));
    }

    return bytes.ToArray();
  }

  static int CharToValue(char c)
  {
    char cl = char.ToLower(c);
    if (cl == 'a') return 0;
    if (cl == 'b') return 1;
    if (cl == 'c') return 2;
    if (cl == 'd') return 3;
    if (cl == 'e') return 4;
    if (cl == 'f') return 5;
    if (cl == 'g') return 6;
    if (cl == 'h') return 7;
    if (cl == 'i') return 8;
    if (cl == 'j') return 9;
    if (cl == 'k') return 10;
    if (cl == 'l') return 11;
    if (cl == 'm') return 12;
    if (cl == 'n') return 13;
    if (cl == 'o') return 14;
    if (cl == 'p') return 15;
    if (cl == 'q') return 16;
    if (cl == 'r') return 17;
    if (cl == 's') return 18;
    if (cl == 't') return 19;
    if (cl == 'u') return 20;
    if (cl == 'v') return 21;
    if (cl == 'w') return 22;
    if (cl == 'x') return 23;
    if (cl == 'y') return 24;
    if (cl == 'z') return 25;
    if (cl == '2') return 26;
    if (cl == '3') return 27;
    if (cl == '4') return 28;
    if (cl == '5') return 29;
    if (cl == '6') return 30;
    if (cl == '7') return 31;
    throw new Exception("Not a base32 string");
  }
}

3
使用强制类型转换和减法操作,可以使CharToValue函数变得更加简单。例如,'b' - 'a' = 1。 - Daniel LeCheminant
1
CharToValue 是一个非常诡异的东西!此外,如果没有编码器,解码器有什么用处? - leppie
好观点,丹尼尔,我不确定当时我在吸什么! - Chris

1

我已经为VB.NET开发了自己的通用Base32编码器/解码器实现。我已经通过独立的网站验证了结果,因此它似乎非常准确。

欢迎提出有关如何改进代码的任何评论。

Option Compare Text
Imports System.ComponentModel

Public Structure Base32(Of T)

        Private Const csValidStandardBase32Chars As String = "0123456789ABCDEFGHIJKLMNOPQRSTUV"

        <EditorBrowsable(EditorBrowsableState.Never)> _
        Class Base32Nibble

                <EditorBrowsable(EditorBrowsableState.Never)> _
                Friend mStore As New BitArray(5, False)

                Public ReadOnly Property Value As Byte
                        Get
                                Dim result As Byte = 0
                                For index As Byte = 0 To mStore.Count - 1
                                        If mStore(index) Then
                                                result += (2 ^ index)
                                        End If
                                Next
                                Return result
                        End Get
                End Property

                Public Overrides Function ToString() As String
                        Dim nibbleString As String = Nothing
                        For Each bit As Boolean In mStore
                                nibbleString = Math.Abs(CInt(bit)).ToString() & nibbleString
                        Next
                        Return nibbleString
                End Function

        End Class

        Private Shared mNibbles As List(Of Base32Nibble)

        Public ReadOnly Property Count As Long
                Get
                        Return mNibbles.Count
                End Get
        End Property

        Default Public ReadOnly Property Item(ByVal index As Integer) As Base32Nibble
                Get
                        Return DirectCast(mNibbles(index), Base32Nibble)
                End Get
        End Property

        Public Sub New(ByVal Value As T)
                Dim temp As Object = CType(Value, T)
                getNibbles(BitConverter.GetBytes(temp))
        End Sub

        Public Sub New(ByVal Value As Byte())
                getNibbles(Value)
        End Sub

        Public Shared Widening Operator CType(ByVal Value As T) As Base32(Of T)
                Return New Base32(Of T)(Value)
        End Operator

        Public Shared Widening Operator CType(ByVal Value As Byte()) As Base32(Of T)
                Return New Base32(Of T)(Value)
        End Operator

        Public ReadOnly Property Value As String
                Get
                        Dim result As String = Nothing
                        For Each Nib As Base32(Of T).Base32Nibble In mNibbles
                                result = csValidStandardBase32Chars(Nib.Value) & result
                        Next
                        Return result.TrimStart("0")
                End Get
        End Property

        Public Function ToNumeric(ByVal Base32String As String) As T
                Dim result As T = CType(CType(0, Object), T)
                Try
                        If Base32String.Trim.Length > 0 Then
                                Dim pos As Integer = 0
                                Do
                                        Dim temp As Object = getBase32Value(Base32String, pos)
                                        result = result + temp
                                        pos += 1
                                Loop While (pos < Base32String.Length)
                        End If
                Catch ex As Exception
                        ' Catch overflow errors if the generic type T doesn't have enough
                        ' room to store the result
                        System.Diagnostics.Debug.Print(ex.Message)
                End Try
                Return result
        End Function

        Private Shared Sub getNibbles(ByVal Value As Byte())

                Dim valueBytes As New BitArray(Value)
                Dim nib As Base32Nibble = Nothing

                mNibbles = New List(Of Base32Nibble)

                Dim padding As Byte = (1 - (valueBytes.Length / 5 - (valueBytes.Length \ 5))) * 5
                valueBytes.Length = valueBytes.Length + padding

                For element As Short = 0 To valueBytes.Count - 1
                        If (element Mod 5 = 0) Then
                                nib = New Base32Nibble()
                                mNibbles.Add(nib)
                        End If
                        nib.mStore.Item(element Mod 5) = valueBytes.Item(element)
                Next

        End Sub

        Private Function getBase32Char(ByVal InputString As String, ByVal InputPosition As Integer) As String
                Return csValidStandardBase32Chars.IndexOf(Mid(InputString, InputPosition + 1, 1))
        End Function

        Private Function getBase32Value(ByVal InputString As String, ByVal InputPosition As Integer) As T
                Return CType(CType((getBase32Char(InputString, InputPosition) * (32 ^ (InputString.Length - 1 - InputPosition))), Object), T)
        End Function

End Structure

这是将123456789转换为Base32的示例

Dim value As Base32(Of Int64) = 123456789
Console.WriteLine( "123456789 in Base32 = " & value.Value)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接