From 95d84723483319f3091c637abca48120797bceec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Sun, 29 Mar 2026 19:38:40 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Replace=20broken=20Base32=20enco?= =?UTF-8?q?der?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There were several problems with the Base32 encoder and decoder: * It did not add padding, which is not RFC-compliant (unless referred to from another document allowing it). * The decoder did not correctly ignore padding and/or non-alphabet characters, returning erroneous data. Now, the first issue is only really a problem if you expose the values for decoding by other implementations. However, all instances where Base32-encoded values are used, use the encoded string verbatim and so is probably not an issue for Freemail. However, Base32-encoded values without padding are used in a lot of places, e.g. in the actual Freemail address, or the slot management, and so, an “encodeWithoutPadding” method has been provided to allow the creation of a non-padded value. The decoding issue might be more serious, as Freemail seems to be decoding Base32-encoded values as part of the slot management, but I haven’t delved into this deeply enough to be sure that this was not a problem. Fixing it seemed like a good, conservative approach. 😄 The Base32 class has been completely rewritten to be easier to read, and to conform to RFC 4648 as closely as possible. For example, the RFC states that Base32 is case-insensitive, but explicitely defines its alphabet as containing only the upper-case characters, not making an allowance for them to be lower-cased. As this is probably an oversight, this implementation can decode lower-case letters as well. The Base32 class has also been moved out of its original package, because it has nothing in common with the original implementation anymore and can simply live in our own packages now. --- src/main/java/org/archive/util/Base32.java | 165 ------------------ .../freemail/AccountManager.java | 2 +- .../freemail/FreemailAccount.java | 4 +- .../freemail/HashSlotManager.java | 4 +- .../freemail/imap/IMAPHandler.java | 2 +- .../freemail/smtp/SMTPHandler.java | 2 +- .../freemail/transport/Channel.java | 6 +- .../freemail/transport/MessageHandler.java | 4 +- .../freemail/ui/web/OutboxToadlet.java | 4 +- .../freenetproject/freemail/utils/Base32.java | 129 ++++++++++++++ .../freemail/utils/EmailAddress.java | 1 - .../freenetproject/freemail/wot/Identity.java | 4 +- src/test/java/data/TestId1Data.java | 4 +- src/test/java/data/TestId2Data.java | 4 +- src/test/java/fakes/MockFreemailAccount.java | 4 +- src/test/java/fakes/MockIdentity.java | 4 +- .../freemail/FreemailAccountTest.java | 15 +- .../freemail/utils/Base32Test.java | 139 +++++++++++++++ 18 files changed, 306 insertions(+), 191 deletions(-) delete mode 100644 src/main/java/org/archive/util/Base32.java create mode 100644 src/main/java/org/freenetproject/freemail/utils/Base32.java create mode 100644 src/test/java/org/freenetproject/freemail/utils/Base32Test.java diff --git a/src/main/java/org/archive/util/Base32.java b/src/main/java/org/archive/util/Base32.java deleted file mode 100644 index c5aded40..00000000 --- a/src/main/java/org/archive/util/Base32.java +++ /dev/null @@ -1,165 +0,0 @@ -/* Base32 -* -* $Id: Base32.java,v 1.4 2004/04/15 19:04:01 stack-sf Exp $ -* -* Created on Jan 21, 2004 -* -* Copyright (C) 2004 Internet Archive. -* -* This file is part of the Heritrix web crawler (crawler.archive.org). -* -* Heritrix is free software; you can redistribute it and/or modify -* it under the terms of the GNU Lesser Public License as published by -* the Free Software Foundation; either version 2.1 of the License, or -* any later version. -* -* Heritrix is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU Lesser Public License for more details. -* -* You should have received a copy of the GNU Lesser Public License -* along with Heritrix; if not, write to the Free Software -* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -*/ -package org.archive.util; - -/** - * Base32 - encodes and decodes RFC3548 Base32 - * (see http://www.faqs.org/rfcs/rfc3548.html ) - * - * Imported public-domain code of Bitzi. - * - * @author Robert Kaye - * @author Gordon Mohr - */ -public class Base32 { - private static final String base32Chars = - "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; - private static final int[] base32Lookup = - { 0xFF,0xFF,0x1A,0x1B,0x1C,0x1D,0x1E,0x1F, // '0', '1', '2', '3', '4', '5', '6', '7' - 0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF, // '8', '9', ':', ';', '<', '=', '>', '?' - 0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '@', 'A', 'B', 'C', 'D', 'E', 'F', 'G' - 0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O' - 0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W' - 0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF, // 'X', 'Y', 'Z', '[', '\', ']', '^', '_' - 0xFF,0x00,0x01,0x02,0x03,0x04,0x05,0x06, // '`', 'a', 'b', 'c', 'd', 'e', 'f', 'g' - 0x07,0x08,0x09,0x0A,0x0B,0x0C,0x0D,0x0E, // 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o' - 0x0F,0x10,0x11,0x12,0x13,0x14,0x15,0x16, // 'p', 'q', 'r', 's', 't', 'u', 'v', 'w' - 0x17,0x18,0x19,0xFF,0xFF,0xFF,0xFF,0xFF // 'x', 'y', 'z', '{', '|', '}', '~', 'DEL' - }; - - /** - * Encodes byte array to Base32 String. - * - * @param bytes Bytes to encode. - * @return Encoded byte array bytes as a String. - * - */ - static public String encode(final byte[] bytes) { - int i = 0, index = 0, digit = 0; - int currByte, nextByte; - StringBuffer base32 = new StringBuffer((bytes.length + 7) * 8 / 5); - - while (i < bytes.length) { - currByte = (bytes[i] >= 0) ? bytes[i] : (bytes[i] + 256); // unsign - - /* Is the current digit going to span a byte boundary? */ - if (index > 3) { - if ((i + 1) < bytes.length) { - nextByte = - (bytes[i + 1] >= 0) ? bytes[i + 1] : (bytes[i + 1] + 256); - } else { - nextByte = 0; - } - - digit = currByte & (0xFF >> index); - index = (index + 5) % 8; - digit <<= index; - digit |= nextByte >> (8 - index); - i++; - } else { - digit = (currByte >> (8 - (index + 5))) & 0x1F; - index = (index + 5) % 8; - if (index == 0) - i++; - } - base32.append(base32Chars.charAt(digit)); - } - - return base32.toString(); - } - - /** - * Decodes the given Base32 String to a raw byte array. - * - * @param base32 the base32 string that should be decoded - * @return Decoded base32 String as a raw byte array. - */ - static public byte[] decode(final String base32) { - int i, index, lookup, offset, digit; - byte[] bytes = new byte[base32.length() * 5 / 8]; - - for (i = 0, index = 0, offset = 0; i < base32.length(); i++) { - lookup = base32.charAt(i) - '0'; - - /* Skip chars outside the lookup table */ - if (lookup < 0 || lookup >= base32Lookup.length) { - continue; - } - - digit = base32Lookup[lookup]; - - /* If this digit is not in the table, ignore it */ - if (digit == 0xFF) { - continue; - } - - if (index <= 3) { - index = (index + 5) % 8; - if (index == 0) { - bytes[offset] |= digit; - offset++; - if (offset >= bytes.length) - break; - } else { - bytes[offset] |= digit << (8 - index); - } - } else { - index = (index + 5) % 8; - bytes[offset] |= (digit >>> index); - offset++; - - if (offset >= bytes.length) { - break; - } - bytes[offset] |= digit << (8 - index); - } - } - return bytes; - } - - /** For testing, take a command-line argument in Base32, decode, print in hex, - * encode, print - * - * @param args contains the base32 encoded string that should be decoded as the only element - */ - static public void main(String[] args) { - if (args.length == 0) { - System.out.println("Supply a Base32-encoded argument."); - return; - } - System.out.println(" Original: " + args[0]); - byte[] decoded = Base32.decode(args[0]); - System.out.print(" Hex: "); - for (int i = 0; i < decoded.length; i++) { - int b = decoded[i]; - if (b < 0) { - b += 256; - } - System.out.print((Integer.toHexString(b + 256)).substring(1)); - } - System.out.println(); - System.out.println("Reencoded: " + Base32.encode(decoded)); - } -} diff --git a/src/main/java/org/freenetproject/freemail/AccountManager.java b/src/main/java/org/freenetproject/freemail/AccountManager.java index 60474a86..cc739948 100644 --- a/src/main/java/org/freenetproject/freemail/AccountManager.java +++ b/src/main/java/org/freenetproject/freemail/AccountManager.java @@ -38,13 +38,13 @@ import java.util.Map; import java.util.TimeZone; -import org.archive.util.Base32; import org.bouncycastle.crypto.AsymmetricCipherKeyPair; import org.bouncycastle.crypto.digests.MD5Digest; import org.bouncycastle.crypto.generators.RSAKeyPairGenerator; import org.bouncycastle.crypto.params.RSAKeyGenerationParameters; import org.bouncycastle.crypto.params.RSAKeyParameters; import org.bouncycastle.util.encoders.Hex; +import org.freenetproject.freemail.utils.Base32; import org.freenetproject.freemail.utils.EmailAddress; import org.freenetproject.freemail.utils.Logger; import org.freenetproject.freemail.utils.PropsFile; diff --git a/src/main/java/org/freenetproject/freemail/FreemailAccount.java b/src/main/java/org/freenetproject/freemail/FreemailAccount.java index 0432a6fa..608166f2 100644 --- a/src/main/java/org/freenetproject/freemail/FreemailAccount.java +++ b/src/main/java/org/freenetproject/freemail/FreemailAccount.java @@ -22,9 +22,9 @@ import java.io.File; import java.util.Locale; -import org.archive.util.Base32; import org.freenetproject.freemail.fcp.HighLevelFCPClientFactory; import org.freenetproject.freemail.transport.MessageHandler; +import org.freenetproject.freemail.utils.Base32; import org.freenetproject.freemail.utils.PropsFile; import freenet.support.Base64; @@ -68,7 +68,7 @@ public String getIdentity() { public String getDomain() { try { - return Base32.encode(Base64.decode(identity)).toLowerCase(Locale.ROOT) + ".freemail"; + return Base32.encodeWithoutPadding(Base64.decode(identity)).toLowerCase(Locale.ROOT) + ".freemail"; } catch(IllegalBase64Exception e) { //This would mean that WoT has changed the encoding of the identity string throw new AssertionError("Got IllegalBase64Exception when decoding " + identity); diff --git a/src/main/java/org/freenetproject/freemail/HashSlotManager.java b/src/main/java/org/freenetproject/freemail/HashSlotManager.java index 904ba46d..c158f7a9 100644 --- a/src/main/java/org/freenetproject/freemail/HashSlotManager.java +++ b/src/main/java/org/freenetproject/freemail/HashSlotManager.java @@ -19,7 +19,7 @@ package org.freenetproject.freemail; -import org.archive.util.Base32; +import org.freenetproject.freemail.utils.Base32; import org.bouncycastle.crypto.digests.SHA256Digest; @@ -35,6 +35,6 @@ protected String incSlot(String slot) { sha256.update(buf, 0, buf.length); sha256.doFinal(buf, 0); - return Base32.encode(buf); + return Base32.encodeWithoutPadding(buf); } } diff --git a/src/main/java/org/freenetproject/freemail/imap/IMAPHandler.java b/src/main/java/org/freenetproject/freemail/imap/IMAPHandler.java index 14cb311e..9abb45b1 100644 --- a/src/main/java/org/freenetproject/freemail/imap/IMAPHandler.java +++ b/src/main/java/org/freenetproject/freemail/imap/IMAPHandler.java @@ -46,12 +46,12 @@ import java.util.Date; import java.util.concurrent.TimeUnit; -import org.archive.util.Base32; import org.freenetproject.freemail.AccountManager; import org.freenetproject.freemail.FreemailAccount; import org.freenetproject.freemail.MailMessage; import org.freenetproject.freemail.MessageBank; import org.freenetproject.freemail.ServerHandler; +import org.freenetproject.freemail.utils.Base32; import org.freenetproject.freemail.utils.EmailAddress; import org.freenetproject.freemail.utils.Logger; diff --git a/src/main/java/org/freenetproject/freemail/smtp/SMTPHandler.java b/src/main/java/org/freenetproject/freemail/smtp/SMTPHandler.java index 6815ea0f..6ed169e9 100644 --- a/src/main/java/org/freenetproject/freemail/smtp/SMTPHandler.java +++ b/src/main/java/org/freenetproject/freemail/smtp/SMTPHandler.java @@ -42,13 +42,13 @@ import freenet.support.api.Bucket; import freenet.support.io.FileBucket; -import org.archive.util.Base32; import org.bouncycastle.util.encoders.Base64; import org.freenetproject.freemail.AccountManager; import org.freenetproject.freemail.Freemail; import org.freenetproject.freemail.FreemailAccount; import org.freenetproject.freemail.ServerHandler; import org.freenetproject.freemail.transport.MessageHandler; +import org.freenetproject.freemail.utils.Base32; import org.freenetproject.freemail.utils.Logger; import org.freenetproject.freemail.wot.Identity; import org.freenetproject.freemail.wot.IdentityMatcher; diff --git a/src/main/java/org/freenetproject/freemail/transport/Channel.java b/src/main/java/org/freenetproject/freemail/transport/Channel.java index ad8cb729..d0039844 100644 --- a/src/main/java/org/freenetproject/freemail/transport/Channel.java +++ b/src/main/java/org/freenetproject/freemail/transport/Channel.java @@ -39,7 +39,6 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; -import org.archive.util.Base32; import org.bouncycastle.crypto.AsymmetricBlockCipher; import org.bouncycastle.crypto.InvalidCipherTextException; import org.bouncycastle.crypto.digests.SHA256Digest; @@ -66,6 +65,7 @@ import org.freenetproject.freemail.fcp.FCPPutFailedException; import org.freenetproject.freemail.fcp.HighLevelFCPClient; import org.freenetproject.freemail.fcp.SSKKeyPair; +import org.freenetproject.freemail.utils.Base32; import org.freenetproject.freemail.utils.DateStringFactory; import org.freenetproject.freemail.utils.Logger; import org.freenetproject.freemail.utils.PropsFile; @@ -593,14 +593,14 @@ boolean canSendMessages() { private String calculateNextSlot(String slot) { byte[] buf = Base32.decode(slot); - return Base32.encode(SHA256.digest(buf)); + return Base32.encodeWithoutPadding(SHA256.digest(buf)); } private String generateRandomSlot() { SHA256Digest sha256 = new SHA256Digest(); byte[] buf = new byte[sha256.getDigestSize()]; Freemail.getRNG().nextBytes(buf); - return Base32.encode(buf); + return Base32.encodeWithoutPadding(buf); } private class Fetcher implements Runnable { diff --git a/src/main/java/org/freenetproject/freemail/transport/MessageHandler.java b/src/main/java/org/freenetproject/freemail/transport/MessageHandler.java index 5a62b0d5..9ba0d8c9 100644 --- a/src/main/java/org/freenetproject/freemail/transport/MessageHandler.java +++ b/src/main/java/org/freenetproject/freemail/transport/MessageHandler.java @@ -39,7 +39,6 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.TimeUnit; -import org.archive.util.Base32; import org.freenetproject.freemail.Freemail; import org.freenetproject.freemail.Freemail.TaskType; import org.freenetproject.freemail.FreemailAccount; @@ -48,6 +47,7 @@ import org.freenetproject.freemail.Postman; import org.freenetproject.freemail.fcp.HighLevelFCPClientFactory; import org.freenetproject.freemail.transport.Channel.ChannelEventCallback; +import org.freenetproject.freemail.utils.Base32; import org.freenetproject.freemail.utils.EmailAddress; import org.freenetproject.freemail.utils.Logger; import org.freenetproject.freemail.utils.PropsFile; @@ -550,7 +550,7 @@ private class AckCallback extends Postman implements ChannelEventCallback { private AckCallback(String remoteId) { assert (remoteId != null); try { - this.remoteId = Base32.encode(Base64.decode(remoteId)).toLowerCase(Locale.ROOT); + this.remoteId = Base32.encodeWithoutPadding(Base64.decode(remoteId)).toLowerCase(Locale.ROOT); } catch (IllegalBase64Exception e) { throw new AssertionError(); } diff --git a/src/main/java/org/freenetproject/freemail/ui/web/OutboxToadlet.java b/src/main/java/org/freenetproject/freemail/ui/web/OutboxToadlet.java index 8015a1c4..c746261f 100644 --- a/src/main/java/org/freenetproject/freemail/ui/web/OutboxToadlet.java +++ b/src/main/java/org/freenetproject/freemail/ui/web/OutboxToadlet.java @@ -25,12 +25,12 @@ import java.util.List; import java.util.concurrent.TimeUnit; -import org.archive.util.Base32; import org.freenetproject.freemail.AccountManager; import org.freenetproject.freemail.FreemailAccount; import org.freenetproject.freemail.FreemailPlugin; import org.freenetproject.freemail.l10n.FreemailL10n; import org.freenetproject.freemail.transport.MessageHandler.OutboxMessage; +import org.freenetproject.freemail.utils.Base32; import org.freenetproject.freemail.utils.Logger; import org.freenetproject.freemail.utils.Timer; import org.freenetproject.freemail.wot.Identity; @@ -101,7 +101,7 @@ HTTPResponse makeWebPageGet(URI uri, HTTPRequest req, ToadletContext ctx, PageNo //Fall back to showing the address without the nickname String id; try { - id = Base32.encode(Base64.decode(message.recipient)); + id = Base32.encodeWithoutPadding(Base64.decode(message.recipient)); } catch (IllegalBase64Exception e) { //This should never happen since we couldn't possibly have sent the message if //the id couldn't be decoded diff --git a/src/main/java/org/freenetproject/freemail/utils/Base32.java b/src/main/java/org/freenetproject/freemail/utils/Base32.java new file mode 100644 index 00000000..aaa402b1 --- /dev/null +++ b/src/main/java/org/freenetproject/freemail/utils/Base32.java @@ -0,0 +1,129 @@ +/* Base32 + * + * $Id: Base32.java,v 1.4 2004/04/15 19:04:01 stack-sf Exp $ + * + * Created on Jan 21, 2004 + * + * Copyright (C) 2004 Internet Archive. + * + * This file is part of the Heritrix web crawler (crawler.archive.org). + * + * Heritrix is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser Public License as published by + * the Free Software Foundation; either version 2.1 of the License, or + * any later version. + * + * Heritrix is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser Public License for more details. + * + * You should have received a copy of the GNU Lesser Public License + * along with Heritrix; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ +package org.freenetproject.freemail.utils; + +import java.util.Arrays; +import java.util.stream.IntStream; + +/** + * RFC-compliant Base32 encoder and decoder. + * + * @see RFC 4648 + */ +public class Base32 { + + /** + * Encodes the given byte array using the Base32 encoding and returns + * the result string. + * + * @param bytes The bytes to encode + * @return The Base32-encoded string + */ + public static String encode(byte[] bytes) { + String encodedValue = encodeWithoutPadding(bytes); + + // now pad with as many ‘=’ as needed to make the length a multiple of 8 + return encodedValue + "======".substring(0, (8 - (encodedValue.length() % 8)) % 8); + } + + /** + * Encodes the given byte array using the Base32 encoding and returns + * the result string without any padding. + * + *

+ * Omitting the padding is a violation of the RFC, so use this with caution. + *

+ * + * @param bytes The bytes to encode + * @return The string without any padding + */ + public static String encodeWithoutPadding(byte[] bytes) { + StringBuilder result = new StringBuilder(); + int buffer = 0; + int availableBits = 0; + + // shift each byte into a buffer and then use as many 5-bit units as possible + for (byte b : bytes) { + buffer = (buffer << 8) | (b & 0xff); + availableBits += 8; + while (availableBits >= 5) { + result.append(base32Characters[(buffer >> (availableBits - 5)) & 0x1f]); + availableBits -= 5; + } + } + + // one character might be left, append it + if (availableBits > 0) { + result.append(base32Characters[(buffer << (5 - availableBits)) & 0x1f]); + } + + return result.toString(); + } + + /** + * Decodes the given Base32-encoded string and returns the resulting + * byte array. Non-alphanet characters are ignored. + * + * @param encodedValue The Base32-encoded string + * @return The decoded byte array + */ + public static byte[] decode(String encodedValue) { + byte[] output = new byte[encodedValue.length() * 5 / 8]; + int buffer = 0; + int availableBits = 0; + int totalBytes = 0; + + for (char character : encodedValue.toCharArray()) { + int decodedValue = (character < base32DecodedValues.length) ? base32DecodedValues[character] : -1; + if (decodedValue == -1) { + continue; + } + buffer = (buffer << 5) | decodedValue; + availableBits += 5; + if (availableBits >= 8) { + output[totalBytes++] = (byte) (buffer >> (availableBits - 8)); + availableBits -= 8; + } + } + + return Arrays.copyOf(output, totalBytes); + } + + private static final char[] base32Characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567".toCharArray(); + private static final int[] base32DecodedValues = generateBase32DecodedValues(); + + private static int[] generateBase32DecodedValues() { + int[] result = new int[128]; + Arrays.fill(result, (byte) -1); + IntStream.range(0, base32Characters.length).forEach(index -> { + result[base32Characters[index]] = index; + // add the lower-case letters as well; for the numbers, ORing + // them with 0x20 doesn’t do anything, as that bit is already set. + result[base32Characters[index] | 0x20] = index; + }); + return result; + } + +} diff --git a/src/main/java/org/freenetproject/freemail/utils/EmailAddress.java b/src/main/java/org/freenetproject/freemail/utils/EmailAddress.java index fc928df6..4cfce201 100644 --- a/src/main/java/org/freenetproject/freemail/utils/EmailAddress.java +++ b/src/main/java/org/freenetproject/freemail/utils/EmailAddress.java @@ -26,7 +26,6 @@ import freenet.support.Base64; -import org.archive.util.Base32; import org.freenetproject.freemail.AccountManager; import org.freenetproject.freemail.MailSite; diff --git a/src/main/java/org/freenetproject/freemail/wot/Identity.java b/src/main/java/org/freenetproject/freemail/wot/Identity.java index 35c7f971..bece814a 100644 --- a/src/main/java/org/freenetproject/freemail/wot/Identity.java +++ b/src/main/java/org/freenetproject/freemail/wot/Identity.java @@ -22,7 +22,7 @@ import java.util.Locale; -import org.archive.util.Base32; +import org.freenetproject.freemail.utils.Base32; import freenet.support.Base64; import freenet.support.IllegalBase64Exception; @@ -44,7 +44,7 @@ public String getIdentityID() { public String getBase32IdentityID() { try { - return Base32.encode(Base64.decode(identityID)).toLowerCase(Locale.ROOT); + return Base32.encodeWithoutPadding(Base64.decode(identityID)).toLowerCase(Locale.ROOT); } catch (IllegalBase64Exception e) { //Can't happen since we always get the id from WoT throw new AssertionError(); diff --git a/src/test/java/data/TestId1Data.java b/src/test/java/data/TestId1Data.java index 99aeeb7e..f54b12b9 100644 --- a/src/test/java/data/TestId1Data.java +++ b/src/test/java/data/TestId1Data.java @@ -19,7 +19,7 @@ package data; -import org.archive.util.Base32; +import org.freenetproject.freemail.utils.Base32; import org.freenetproject.freemail.utils.DateStringFactory; import freenet.support.Base64; @@ -30,7 +30,7 @@ public class TestId1Data { public static final String BASE32_ID; static { try { - BASE32_ID = Base32.encode(Base64.decode(BASE64_ID)); + BASE32_ID = Base32.encodeWithoutPadding(Base64.decode(BASE64_ID)); } catch (IllegalBase64Exception e) { throw new AssertionError(e); } diff --git a/src/test/java/data/TestId2Data.java b/src/test/java/data/TestId2Data.java index eece16db..bf8a8d52 100644 --- a/src/test/java/data/TestId2Data.java +++ b/src/test/java/data/TestId2Data.java @@ -19,7 +19,7 @@ package data; -import org.archive.util.Base32; +import org.freenetproject.freemail.utils.Base32; import org.freenetproject.freemail.utils.DateStringFactory; import freenet.support.Base64; @@ -30,7 +30,7 @@ public class TestId2Data { private static final String BASE32_ID; static { try { - BASE32_ID = Base32.encode(Base64.decode(BASE64_ID)); + BASE32_ID = Base32.encodeWithoutPadding(Base64.decode(BASE64_ID)); } catch (IllegalBase64Exception e) { throw new AssertionError(e); } diff --git a/src/test/java/fakes/MockFreemailAccount.java b/src/test/java/fakes/MockFreemailAccount.java index 26d591c0..92e8286b 100644 --- a/src/test/java/fakes/MockFreemailAccount.java +++ b/src/test/java/fakes/MockFreemailAccount.java @@ -22,9 +22,9 @@ import java.io.File; import java.util.Locale; -import org.archive.util.Base32; import org.freenetproject.freemail.Freemail; import org.freenetproject.freemail.NullFreemailAccount; +import org.freenetproject.freemail.utils.Base32; import org.freenetproject.freemail.utils.PropsFile; import freenet.support.Base64; @@ -60,7 +60,7 @@ public PropsFile getProps() { @Override public String getDomain() { try { - return Base32.encode(Base64.decode(identity)).toLowerCase(Locale.ROOT) + ".freemail"; + return Base32.encodeWithoutPadding(Base64.decode(identity)).toLowerCase(Locale.ROOT) + ".freemail"; } catch(IllegalBase64Exception e) { //This would mean that WoT has changed the encoding of the identity string throw new AssertionError("Got IllegalBase64Exception when decoding " + identity); diff --git a/src/test/java/fakes/MockIdentity.java b/src/test/java/fakes/MockIdentity.java index 70f68ff7..3757c8e0 100644 --- a/src/test/java/fakes/MockIdentity.java +++ b/src/test/java/fakes/MockIdentity.java @@ -19,7 +19,7 @@ package fakes; -import org.archive.util.Base32; +import org.freenetproject.freemail.utils.Base32; import org.freenetproject.freemail.utils.Logger; import freenet.support.Base64; @@ -45,7 +45,7 @@ public String getRequestURI() { public String getBase32IdentityID() { Logger.debug(this, "getBase32IdentityID()"); try { - return Base32.encode(Base64.decode(identityID)); + return Base32.encodeWithoutPadding(Base64.decode(identityID)); } catch (IllegalBase64Exception e) { throw new AssertionError(); } diff --git a/src/test/java/org/freenetproject/freemail/FreemailAccountTest.java b/src/test/java/org/freenetproject/freemail/FreemailAccountTest.java index 93af5e97..95262393 100644 --- a/src/test/java/org/freenetproject/freemail/FreemailAccountTest.java +++ b/src/test/java/org/freenetproject/freemail/FreemailAccountTest.java @@ -19,13 +19,18 @@ package org.freenetproject.freemail; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.*; +import freenet.crypt.Yarrow; +import freenet.keys.InsertableClientSSK; import java.util.Locale; import org.junit.Test; -import org.archive.util.Base32; +import org.freenetproject.freemail.utils.Base32; import freenet.support.Base64; @@ -47,4 +52,12 @@ public void simpleAddress() { Locale.setDefault(defaultLocale); } } + + @Test + public void freemailAccountStripsBase32PaddingFromDomain() { + byte[] routingKey = InsertableClientSSK.createRandom(new Yarrow(), "").getURI().getRoutingKey(); + FreemailAccount account = new FreemailAccount(Base64.encode(routingKey), null, null, null); + assertThat(account.getDomain(), not(containsString("="))); + } + } diff --git a/src/test/java/org/freenetproject/freemail/utils/Base32Test.java b/src/test/java/org/freenetproject/freemail/utils/Base32Test.java new file mode 100644 index 00000000..14d5905d --- /dev/null +++ b/src/test/java/org/freenetproject/freemail/utils/Base32Test.java @@ -0,0 +1,139 @@ +package org.freenetproject.freemail.utils; + +import org.junit.Test; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.freenetproject.freemail.utils.Base32.decode; +import static org.freenetproject.freemail.utils.Base32.encode; +import static org.freenetproject.freemail.utils.Base32.encodeWithoutPadding; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +public class Base32Test { + + @Test + public void decodingAnEmptyStringResultsInAnEmptyArray() { + assertThat(decode(""), equalTo(new byte[0])); + } + + @Test + public void encodingAnEmptyArrayResultsInAnEmptyString() { + assertThat(encode(new byte[0]), equalTo("")); + } + + @Test + public void encodingAnEmptyArrayWithoutPaddingResultsInAnEmptyString() { + assertThat(encode(new byte[0]), equalTo("")); + } + + @Test + public void encodingASingleByteResultsInAnEightCharacterString() { + assertThat(encode(bytes(0xff)), equalTo("74======")); + } + + @Test + public void encodingASingleByteWithoutPaddingResultsInATwoCharacterString() { + assertThat(encodeWithoutPadding(bytes(0xff)), equalTo("74")); + } + + @Test + public void twoBytesAreEncodedCorrectly() { + assertThat(encode(bytes(0x55, 0xaa)), equalTo("KWVA====")); + } + + @Test + public void twoBytesAreEncodedWithoutPaddingCorrectly() { + assertThat(encodeWithoutPadding(bytes(0x55, 0xaa)), equalTo("KWVA")); + } + + @Test + public void threeBytesAreEncodedCorrectly() { + assertThat(encode(bytes(0xaa, 0x55, 0xff)), equalTo("VJK76===")); + } + + @Test + public void threeBytesAreEncodedWithoutPaddingCorrectly() { + assertThat(encodeWithoutPadding(bytes(0xaa, 0x55, 0xff)), equalTo("VJK76")); + } + + @Test + public void fourBytesAreEncodedCorrectly() { + assertThat(encode(bytes(0x12, 0x34, 0x56, 0x78)), equalTo("CI2FM6A=")); + } + + @Test + public void fourBytesAreEncodedWithoutPaddingCorrectly() { + assertThat(encodeWithoutPadding(bytes(0x12, 0x34, 0x56, 0x78)), equalTo("CI2FM6A")); + } + + @Test + public void fiveBytesAreEncodedCorrectly() { + assertThat(encode(bytes(0xff, 0xfe, 0xfd, 0xfc, 0xfb)), equalTo("777P37H3")); + } + + @Test + public void fiveBytesAreEncodedWithoutPaddingCorrectly() { + assertThat(encodeWithoutPadding(bytes(0xff, 0xfe, 0xfd, 0xfc, 0xfb)), equalTo("777P37H3")); + } + + @Test + public void decodingAnEightCharacterStringReturnsTheCorrectByte() { + assertThat(decode("74======"), equalTo(bytes(0xff))); + } + + // https://datatracker.ietf.org/doc/html/draft-ietf-sasl-gssapi-00, § 2.1 + @Test + public void exampleDataFromSaslGssapiDraftIsEncodedCorrectly() { + assertThat(encode(bytes(0x57, 0xee, 0x81, 0x82, 0x4e, 0xac, 0x4d, 0xb0, 0xe6, 0x50)), equalTo("K7XIDASOVRG3BZSQ")); + } + + // https://datatracker.ietf.org/doc/html/draft-ietf-sasl-gssapi-00, § 2.1 + @Test + public void exampleDataFromSaslGssapiDraftIsDecodedCorrectly() { + assertThat(decode("K7XIDASOVRG3BZSQ"), equalTo(bytes(0x57, 0xee, 0x81, 0x82, 0x4e, 0xac, 0x4d, 0xb0, 0xe6, 0x50))); + } + + @Test + public void testVectorsFromRfc4648AreEncodedCorrectly() { + assertThat(encode("f".getBytes(UTF_8)), equalTo("MY======")); + assertThat(encode("fo".getBytes(UTF_8)), equalTo("MZXQ====")); + assertThat(encode("foo".getBytes(UTF_8)), equalTo("MZXW6===")); + assertThat(encode("foob".getBytes(UTF_8)), equalTo("MZXW6YQ=")); + assertThat(encode("fooba".getBytes(UTF_8)), equalTo("MZXW6YTB")); + assertThat(encode("foobar".getBytes(UTF_8)), equalTo("MZXW6YTBOI======")); + } + + @Test + public void testVectorsFromRfc4648AreDecodedCorrectly() { + assertThat(decode("MY======"), equalTo(bytes('f'))); + assertThat(decode("MZXQ===="), equalTo(bytes('f', 'o'))); + assertThat(decode("MZXW6==="), equalTo(bytes('f', 'o', 'o'))); + assertThat(decode("MZXW6YQ="), equalTo(bytes('f', 'o', 'o', 'b'))); + assertThat(decode("MZXW6YTB"), equalTo(bytes('f', 'o', 'o', 'b', 'a'))); + assertThat(decode("MZXW6YTBOI======"), equalTo(bytes('f', 'o', 'o', 'b', 'a', 'r'))); + } + + @Test + public void lowercaseAlphabetCharactersAreDecodedCorrectly() { + assertThat(decode("hntq3blvijjhyg4t4cdq===="), equalTo(bytes(0x3b, 0x67, 0x0d, 0x85, 0x75, 0x42, 0x52, 0x7c, 0x1b, 0x93, 0xe0, 0x87))); + } + + @Test + public void missingPaddingIsIgnored() { + assertThat(decode("HNTQ3BLVIJJHYG4T4CDQ="), equalTo(bytes(0x3b, 0x67, 0x0d, 0x85, 0x75, 0x42, 0x52, 0x7c, 0x1b, 0x93, 0xe0, 0x87))); + } + + @Test + public void nonAlphabetCharactersAreIgnored() { + assertThat(decode("ßK87-X%I$D#A/S!O'V\"RäG930B(Z)S}Q["), equalTo(bytes(0x57, 0xee, 0x81, 0x82, 0x4e, 0xac, 0x4d, 0xb0, 0xe6, 0x50))); + } + + private static byte[] bytes(int... values) { + byte[] result = new byte[values.length]; + for (int index = 0; index < values.length; index++) { + result[index] = (byte) values[index]; + } + return result; + } + +}