summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--crypto/src/security/JksStore.cs144
-rw-r--r--crypto/test/data/jks/cacerts.jksbin0 -> 156996 bytes
-rw-r--r--crypto/test/src/security/test/JksStoreTest.cs16
3 files changed, 141 insertions, 19 deletions
diff --git a/crypto/src/security/JksStore.cs b/crypto/src/security/JksStore.cs
index 4df0b39db..a7d3f6ca3 100644
--- a/crypto/src/security/JksStore.cs
+++ b/crypto/src/security/JksStore.cs
@@ -726,20 +726,81 @@ namespace Org.BouncyCastle.Security
 
         private static string ReadUtf(BinaryReader br)
         {
-            byte[] utfBytes = ReadBufferWithInt16Length(br);
+            byte[] mUtfBytes = ReadBufferWithInt16Length(br);
+
+            int i = 0;
+            MemoryStream utfBytes = new MemoryStream(mUtfBytes.Length);
+
+            while (i < mUtfBytes.Length)
+            {
+                // Modified UTF-8 differs from regular UTF-8 in the following
+                // ways:
+                //
+                // 1. NULL bytes never occur in the stream and are always
+                //    two-byte encoded.
+                // 2. There are no four-byte values and are instead always
+                //    encoded using surrogate pairs.
+                //
+                // See also: https://docs.oracle.com/en/java/javase/16/docs/api/java.base/java/io/DataInput.html#modified-utf-8
+                byte mUtfByte = mUtfBytes[i];
+                if (mUtfByte == 0)
+                    throw new NotSupportedException("Unexpected NULL byte in modified UTF-8 encoding in JKS");
+
+                if ((mUtfByte & 0x80) == 0)
+                {
+                    // No transformation is applied to non-NULL ASCII bytes.
+                    utfBytes.WriteByte(mUtfByte);
+                    i += 1;
+                }
+                else if ((mUtfByte & 0xE0) == 0xC0)
+                {
+                    // Validate we have another byte.
+                    if ((i + 1) >= mUtfBytes.Length)
+                        throw new NotSupportedException("Two-byte sentinel found at end of input stream");
 
-            /*
-             * FIXME JKS actually uses a "modified UTF-8" format. For the moment we will just support single-byte
-             * encodings that aren't null bytes.
-             */
-            for (int i = 0; i < utfBytes.Length; ++i)
-            {
-                byte utfByte = utfBytes[i];
-                if (utfByte == 0 || (utfByte & 0x80) != 0)
-                    throw new NotSupportedException("Currently missing support for modified UTF-8 encoding in JKS");
+                    byte mUtfByteSecond = mUtfBytes[i+1];
+                    if ((mUtfByteSecond & 0xC0) != 0x80)
+                        throw new NotSupportedException("Second byte in two-byte modified UTF-8 encoding malformed");
+
+                    // We might have encoded the NULL byte as two bytes.
+                    if (mUtfByte == 0xC0 && mUtfByteSecond == 0x80)
+                    {
+                        utfBytes.WriteByte(0x00);
+                    } else {
+                        utfBytes.WriteByte(mUtfByte);
+                        utfBytes.WriteByte(mUtfByteSecond);
+                    }
+
+                    i += 2;
+                }
+                else if ((mUtfByte & 0xF0) == 0xE0)
+                {
+                    // Validate we have enough bytes.
+                    if ((i + 2) >= mUtfBytes.Length)
+                        throw new NotSupportedException("Three-byte sentinel found at end of input stream");
+
+                    byte mUtfByteSecond = mUtfBytes[i+1];
+                    if ((mUtfByteSecond & 0xC0) != 0x80)
+                        throw new NotSupportedException("Second byte in two-byte modified UTF-8 encoding malformed");
+
+                    byte mUtfByteThird = mUtfBytes[i+2];
+                    if ((mUtfByteThird & 0xC0) != 0x80)
+                        throw new NotSupportedException("Third byte in two-byte modified UTF-8 encoding malformed");
+
+                    utfBytes.WriteByte(mUtfByte);
+                    utfBytes.WriteByte(mUtfByteSecond);
+                    utfBytes.WriteByte(mUtfByteThird);
+
+                    i += 3;
+                }
+                else
+                {
+                    // Reachable when we have a non-standard four-byte sentinel mask.
+                    throw new NotSupportedException("Malformed modified UTF-8 encoding at index " + i);
+                }
             }
 
-            return Encoding.UTF8.GetString(utfBytes);
+            return Encoding.UTF8.GetString(utfBytes.ToArray());
         }
 
         private static void WriteBufferWithInt16Length(BinaryWriter bw, byte[] buffer)
@@ -770,18 +831,63 @@ namespace Org.BouncyCastle.Security
         {
             byte[] utfBytes = Encoding.UTF8.GetBytes(s);
 
-            /*
-             * FIXME JKS actually uses a "modified UTF-8" format. For the moment we will just support single-byte
-             * encodings that aren't null bytes.
-             */
-            for (int i = 0; i < utfBytes.Length; ++i)
+            int i = 0;
+            MemoryStream mUtfBytes = new MemoryStream();
+            while (i < utfBytes.Length)
             {
                 byte utfByte = utfBytes[i];
-                if (utfByte == 0 || (utfByte & 0x80) != 0)
-                    throw new NotSupportedException("Currently missing support for modified UTF-8 encoding in JKS");
+                if (utfByte == 0)
+                {
+                    // The NULL byte is encoded in two byte format.
+                    mUtfBytes.WriteByte(0xC0);
+                    mUtfBytes.WriteByte(0x80);
+                    i += 1;
+                }
+                else if ((utfByte & 0x80) == 0)
+                {
+                    // One byte UTF-8 bytes are written directly.
+                    mUtfBytes.WriteByte(utfByte);
+                    i += 1;
+                }
+                else if ((utfByte & 0xE0) == 0xC0)
+                {
+                    // Two byte UTF-8 values are preserved as-is.
+                    if ((i + 1) >= utfBytes.Length)
+                        throw new NotSupportedException("Malformed UTF-8: trailing two-byte character at end of string");
+
+                    if ((utfBytes[i+1] & 0xC0) != 0x80)
+                        throw new NotSupportedException("Malformed UTF-8: second byte has invalid prefix");
+
+                    mUtfBytes.WriteByte(utfByte);
+                    mUtfBytes.WriteByte(utfBytes[i+1]);
+                    i += 2;
+                }
+                else if ((utfByte & 0xF0) == 0xE0)
+                {
+                    // Three byte UTF-8 values are preserved as-is.
+                    if ((i + 2) >= utfBytes.Length)
+                        throw new NotSupportedException("Malformed UTF-8: trailing three-byte character at end of string");
+
+                    if ((utfBytes[i+1] & 0xC0) != 0x80)
+                        throw new NotSupportedException("Malformed UTF-8: second byte has invalid prefix");
+
+                    if ((utfBytes[i+2] & 0xC0) != 0x80)
+                        throw new NotSupportedException("Malformed UTF-8: third byte has invalid prefix");
+
+                    mUtfBytes.WriteByte(utfByte);
+                    mUtfBytes.WriteByte(utfBytes[i+1]);
+                    mUtfBytes.WriteByte(utfBytes[i+2]);
+                    i += 3;
+
+                }
+                else
+                {
+                    // Reachable when we have a non-standard four-byte sentinel mask.
+                    throw new NotSupportedException("Malformed modified UTF-8 encoding at index " + i);
+                }
             }
 
-            WriteBufferWithInt16Length(bw, utfBytes);
+            WriteBufferWithInt16Length(bw, mUtfBytes.ToArray());
         }
 
         /**
diff --git a/crypto/test/data/jks/cacerts.jks b/crypto/test/data/jks/cacerts.jks
new file mode 100644
index 000000000..ca0cbd33e
--- /dev/null
+++ b/crypto/test/data/jks/cacerts.jks
Binary files differdiff --git a/crypto/test/src/security/test/JksStoreTest.cs b/crypto/test/src/security/test/JksStoreTest.cs
index 335786f5e..8794d48a6 100644
--- a/crypto/test/src/security/test/JksStoreTest.cs
+++ b/crypto/test/src/security/test/JksStoreTest.cs
@@ -9,6 +9,8 @@ using Org.BouncyCastle.Crypto.Parameters;
 using Org.BouncyCastle.Math;
 using Org.BouncyCastle.Utilities.Encoders;
 
+using Org.BouncyCastle.Utilities.Test;
+
 namespace Org.BouncyCastle.Security.Tests
 {
     [TestFixture]
@@ -177,5 +179,19 @@ namespace Org.BouncyCastle.Security.Tests
                 // Expected
             }
         }
+
+        [Test]
+        public void TestJksModifiedUtf8Roundtrip()
+        {
+            JksStore ks = new JksStore();
+            Stream fIn = SimpleTest.GetTestDataAsStream("jks.cacerts.jks");
+
+            ks.Load(fIn, "changeit".ToCharArray());
+
+            MemoryStream bOut = new MemoryStream();
+            ks.Save(bOut, "changedit".ToCharArray());
+
+            ks.Load(new MemoryStream(bOut.ToArray()), "changedit".ToCharArray());
+        }
     }
 }