summary refs log tree commit diff
diff options
context:
space:
mode:
authorPeter Dettman <peter.dettman@bouncycastle.org>2022-09-12 12:04:41 +0700
committerPeter Dettman <peter.dettman@bouncycastle.org>2022-09-12 12:04:41 +0700
commit611e75935efc4266b9a66b25cff68c66f46a2bd2 (patch)
treea1b1b901bde0ac62ce0c6ba820cadc1707af53c0
parentImprove span-based GCM code (diff)
downloadBouncyCastle.NET-ed25519-611e75935efc4266b9a66b25cff68c66f46a2bd2.tar.xz
Add basic support for JKS keystores
-rw-r--r--crypto/src/security/JksStore.cs610
-rw-r--r--crypto/test/src/security/test/JksStoreTest.cs181
2 files changed, 791 insertions, 0 deletions
diff --git a/crypto/src/security/JksStore.cs b/crypto/src/security/JksStore.cs
new file mode 100644
index 000000000..9b4269278
--- /dev/null
+++ b/crypto/src/security/JksStore.cs
@@ -0,0 +1,610 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+using Org.BouncyCastle.Asn1;
+using Org.BouncyCastle.Asn1.Pkcs;
+using Org.BouncyCastle.Asn1.X509;
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Crypto.IO;
+using Org.BouncyCastle.Pkcs;
+using Org.BouncyCastle.Utilities;
+using Org.BouncyCastle.Utilities.Date;
+using Org.BouncyCastle.Utilities.IO;
+using Org.BouncyCastle.X509;
+
+namespace Org.BouncyCastle.Security
+{
+    public class JksStore
+    {
+        private static readonly int Magic = unchecked((int)0xFEEDFEED);
+
+        private static readonly AlgorithmIdentifier JksObfuscationAlg = new AlgorithmIdentifier(
+            new DerObjectIdentifier("1.3.6.1.4.1.42.2.17.1.1"), DerNull.Instance);
+
+        private readonly Dictionary<string, JksTrustedCertEntry> m_certificateEntries =
+            new Dictionary<string, JksTrustedCertEntry>(StringComparer.OrdinalIgnoreCase);
+        private readonly Dictionary<string, JksKeyEntry> m_keyEntries =
+            new Dictionary<string, JksKeyEntry>(StringComparer.OrdinalIgnoreCase);
+
+        public JksStore()
+        {
+        }
+
+        /// <exception cref="IOException"/>
+        public bool Probe(Stream stream)
+        {
+            using (var br = new BinaryReader(stream))
+            try
+            {
+                return Magic == ReadInt32(br);
+            }
+            catch (EndOfStreamException)
+            {
+                return false;
+            }
+        }
+
+        /// <exception cref="IOException"/>
+        public AsymmetricKeyParameter GetKey(string alias, char[] password)
+        {
+            if (alias == null)
+                throw new ArgumentNullException(nameof(alias));
+            if (password == null)
+                throw new ArgumentNullException(nameof(password));
+
+            if (!m_keyEntries.TryGetValue(alias, out JksKeyEntry keyEntry))
+                return null;
+
+            if (!JksObfuscationAlg.Equals(keyEntry.keyData.EncryptionAlgorithm))
+                throw new IOException("unknown encryption algorithm");
+
+            byte[] encryptedData = keyEntry.keyData.GetEncryptedData();
+
+            // key length is encryptedData - salt - checksum
+            int pkcs8Len = encryptedData.Length - 40;
+
+            IDigest digest = DigestUtilities.GetDigest("SHA-1");
+
+            // key decryption
+            byte[] keyStream = CalculateKeyStream(digest, password, encryptedData, pkcs8Len);
+            byte[] pkcs8Key = new byte[pkcs8Len];
+            for (int i = 0; i < pkcs8Len; ++i)
+            {
+                pkcs8Key[i] = (byte)(encryptedData[20 + i] ^ keyStream[i]);
+            }
+            Array.Clear(keyStream, 0, keyStream.Length);
+
+            // integrity check
+            byte[] checksum = GetKeyChecksum(digest, password, pkcs8Key);
+
+            if (!Arrays.ConstantTimeAreEqual(20, encryptedData, pkcs8Len + 20, checksum, 0))
+                throw new IOException("cannot recover key");
+
+            return PrivateKeyFactory.CreateKey(pkcs8Key);
+        }
+
+        private byte[] GetKeyChecksum(IDigest digest, char[] password, byte[] pkcs8Key)
+        {
+            AddPassword(digest, password);
+
+            return DigestUtilities.DoFinal(digest, pkcs8Key);
+        }
+
+        private byte[] CalculateKeyStream(IDigest digest, char[] password, byte[] salt, int count)
+        {
+            byte[] keyStream = new byte[count];
+            byte[] hash = Arrays.CopyOf(salt, 20);
+
+            int index = 0;
+            while (index < count)
+            {
+                AddPassword(digest, password);
+
+                digest.BlockUpdate(hash, 0, hash.Length);
+                digest.DoFinal(hash, 0);
+
+                int length = System.Math.Min(hash.Length, keyStream.Length - index);
+                Array.Copy(hash, 0, keyStream, index, length);
+                index += length;
+            }
+
+            return keyStream;
+        }
+
+        public X509Certificate[] GetCertificateChain(string alias)
+        {
+            if (m_keyEntries.TryGetValue(alias, out var keyEntry))
+                return CloneChain(keyEntry.chain);
+
+            return null;
+        }
+
+        public X509Certificate GetCertificate(string alias)
+        {
+            if (m_certificateEntries.TryGetValue(alias, out var certEntry))
+                return certEntry.cert;
+
+            if (m_keyEntries.TryGetValue(alias, out var keyEntry))
+                return keyEntry.chain?[0];
+
+            return null;
+        }
+
+        public DateTime? GetCreationDate(string alias)
+        {
+            if (m_certificateEntries.TryGetValue(alias, out var certEntry))
+                return certEntry.date;
+
+            if (m_keyEntries.TryGetValue(alias, out var keyEntry))
+                return keyEntry.date;
+
+            return null;
+        }
+
+        /// <exception cref="IOException"/>
+        public void SetKeyEntry(string alias, AsymmetricKeyParameter key, char[] password, X509Certificate[] chain)
+        {
+            alias = ConvertAlias(alias);
+
+            if (ContainsAlias(alias))
+                throw new IOException("alias [" + alias + "] already in use");
+
+            byte[] pkcs8Key = PrivateKeyInfoFactory.CreatePrivateKeyInfo(key).GetEncoded();
+            byte[] protectedKey = new byte[pkcs8Key.Length + 40];
+
+            SecureRandom rnd = new SecureRandom();
+            rnd.NextBytes(protectedKey, 0, 20);
+
+            IDigest digest = DigestUtilities.GetDigest("SHA-1");
+
+            byte[] checksum = GetKeyChecksum(digest, password, pkcs8Key);
+            Array.Copy(checksum, 0, protectedKey, 20 + pkcs8Key.Length, 20);
+
+            byte[] keyStream = CalculateKeyStream(digest, password, protectedKey, pkcs8Key.Length);
+            for (int i = 0; i != keyStream.Length; i++)
+            {
+                protectedKey[20 + i] = (byte)(pkcs8Key[i] ^ keyStream[i]);
+            }
+            Array.Clear(keyStream, 0, keyStream.Length);
+
+            try
+            {
+                var epki = new EncryptedPrivateKeyInfo(JksObfuscationAlg, protectedKey);
+                m_keyEntries.Add(alias, new JksKeyEntry(DateTime.UtcNow, epki.GetEncoded(), CloneChain(chain)));
+            }
+            catch (Exception e)
+            {
+                throw new IOException("unable to encode encrypted private key", e);
+            }
+        }
+
+        /// <exception cref="IOException"/>
+        public void SetKeyEntry(string alias, byte[] key, X509Certificate[] chain)
+        {
+            alias = ConvertAlias(alias);
+
+            if (ContainsAlias(alias))
+                throw new IOException("alias [" + alias + "] already in use");
+
+            m_keyEntries.Add(alias, new JksKeyEntry(DateTime.UtcNow, key, CloneChain(chain)));
+        }
+
+        /// <exception cref="IOException"/>
+        public void SetCertificateEntry(string alias, X509Certificate cert)
+        {
+            alias = ConvertAlias(alias);
+
+            if (ContainsAlias(alias))
+                throw new IOException("alias [" + alias + "] already in use");
+
+            m_certificateEntries.Add(alias, new JksTrustedCertEntry(DateTime.UtcNow, cert));
+        }
+
+        public void DeleteEntry(string alias)
+        {
+            if (!m_keyEntries.Remove(alias))
+            {
+                m_certificateEntries.Remove(alias);
+            }
+        }
+
+        public IEnumerable<string> Aliases
+        {
+            get
+            {
+                var aliases = new HashSet<string>(m_certificateEntries.Keys);
+                aliases.UnionWith(m_keyEntries.Keys);
+                // FIXME
+                //return CollectionUtilities.Proxy(aliases);
+                return aliases;
+            }
+        }
+
+        public bool ContainsAlias(string alias)
+        {
+            return IsCertificateEntry(alias) || IsKeyEntry(alias);
+        }
+
+        public int Count
+        {
+            get { return m_certificateEntries.Count + m_keyEntries.Count; }
+        }
+
+        public bool IsKeyEntry(string alias)
+        {
+            return m_keyEntries.ContainsKey(alias);
+        }
+
+        public bool IsCertificateEntry(string alias)
+        {
+            return m_certificateEntries.ContainsKey(alias);
+        }
+
+        public string GetCertificateAlias(X509Certificate cert)
+        {
+            foreach (var entry in m_certificateEntries)
+            {
+                if (entry.Value.cert.Equals(cert))
+                    return entry.Key;
+            }
+            return null;
+        }
+
+        /// <exception cref="IOException"/>
+        public void Save(Stream stream, char[] password)
+        {
+            if (stream == null)
+                throw new ArgumentNullException(nameof(stream));
+            if (password == null)
+                throw new ArgumentNullException(nameof(password));
+
+            IDigest checksumDigest = CreateChecksumDigest(password);
+            BinaryWriter bw = new BinaryWriter(new DigestStream(stream, null, checksumDigest));
+
+            WriteInt32(bw, Magic);
+            WriteInt32(bw, 2);
+
+            WriteInt32(bw, Count);
+
+            foreach (var entry in m_keyEntries)
+            {
+                string alias = entry.Key;
+                JksKeyEntry keyEntry = entry.Value;
+
+                WriteInt32(bw, 1);
+                WriteUtf(bw, alias);
+                WriteDateTime(bw, keyEntry.date);
+                WriteBufferWithLength(bw, keyEntry.keyData.GetEncoded());
+
+                X509Certificate[] chain = keyEntry.chain;
+                int chainLength = chain == null ? 0 : chain.Length;
+                WriteInt32(bw, chainLength);
+                for (int i = 0; i < chainLength; ++i)
+                {
+                    WriteTypedCertificate(bw, chain[i]);
+                }
+            }
+
+            foreach (var entry in m_certificateEntries) 
+            {
+                string alias = entry.Key;
+                JksTrustedCertEntry certEntry = entry.Value;
+
+                WriteInt32(bw, 2);
+                WriteUtf(bw, alias);
+                WriteDateTime(bw, certEntry.date);
+                WriteTypedCertificate(bw, certEntry.cert);
+            }
+
+            byte[] checksum = DigestUtilities.DoFinal(checksumDigest);
+            bw.Write(checksum);
+            bw.Flush();
+        }
+
+        /// <exception cref="IOException"/>
+        public void Load(Stream stream, char[] password)
+        {
+            if (stream == null)
+                throw new ArgumentNullException(nameof(stream));
+
+            m_certificateEntries.Clear();
+            m_keyEntries.Clear();
+
+            using (var storeStream = ValidateStream(stream, password))
+            {
+                BinaryReader dIn = new BinaryReader(storeStream);
+
+                int magic = ReadInt32(dIn);
+                int storeVersion = ReadInt32(dIn);
+
+                if (!(magic == Magic && (storeVersion == 1 || storeVersion == 2)))
+                    throw new IOException("Invalid keystore format");
+
+                int numEntries = ReadInt32(dIn);
+
+                for (int t = 0; t < numEntries; t++)
+                {
+                    int tag = ReadInt32(dIn);
+
+                    switch (tag)
+                    {
+                    case 1: // keys
+                    {
+                        string alias = ReadUtf(dIn);
+                        DateTime date = ReadDateTime(dIn);
+
+                        // encrypted key data
+                        byte[] keyData = ReadBufferWithLength(dIn);
+
+                        // certificate chain
+                        int chainLength = ReadInt32(dIn);
+                        X509Certificate[] chain = null;
+                        if (chainLength > 0)
+                        {
+                            var certs = new List<X509Certificate>(System.Math.Min(10, chainLength));
+                            for (int certNo = 0; certNo != chainLength; certNo++)
+                            {
+                                certs.Add(ReadTypedCertificate(dIn, storeVersion));
+                            }
+                            chain = certs.ToArray();
+                        }
+                        m_keyEntries.Add(alias, new JksKeyEntry(date, keyData, chain));
+                        break;
+                    }
+                    case 2: // certificate
+                    {
+                        string alias = ReadUtf(dIn);
+                        DateTime date = ReadDateTime(dIn);
+
+                        X509Certificate cert = ReadTypedCertificate(dIn, storeVersion);
+
+                        m_certificateEntries.Add(alias, new JksTrustedCertEntry(date, cert));
+                        break;
+                    }
+                    default:
+                        throw new IOException("unable to discern entry type");
+                    }
+                }
+
+                if (storeStream.Position != storeStream.Length)
+                    throw new IOException("password incorrect or store tampered with");
+            }
+        }
+
+        /*
+         * Validate password takes the checksum of the store and will either.
+         * 1. If password is null, load the store into memory, return the result.
+         * 2. If password is not null, load the store into memory, test the checksum, and if successful return
+         * a new input stream instance of the store.
+         * 3. Fail if there is a password and an invalid checksum.
+         *
+         * @param inputStream The input stream.
+         * @param password    the password.
+         * @return Either the passed in input stream or a new input stream.
+         */
+        /// <exception cref="IOException"/>
+        private ErasableByteStream ValidateStream(Stream inputStream, char[] password)
+        {
+            byte[] rawStore = Streams.ReadAll(inputStream);
+            int checksumPos = rawStore.Length - 20;
+
+            if (password != null)
+            {
+                byte[] checksum = CalculateChecksum(password, rawStore, 0, checksumPos);
+
+                if (!Arrays.ConstantTimeAreEqual(20, checksum, 0, rawStore, checksumPos))
+                {
+                    Array.Clear(rawStore, 0, rawStore.Length);
+                    throw new IOException("password incorrect or store tampered with");
+                }
+            }
+
+            return new ErasableByteStream(rawStore, 0, checksumPos);
+        }
+
+        private static void AddPassword(IDigest digest, char[] password)
+        {
+            // Encoding.BigEndianUnicode
+            for (int i = 0; i < password.Length; ++i)
+            {
+                digest.Update((byte)(password[i] >> 8));
+                digest.Update((byte)password[i]);
+            }
+        }
+
+        private static byte[] CalculateChecksum(char[] password, byte[] buffer, int offset, int length)
+        {
+            IDigest checksumDigest = CreateChecksumDigest(password);
+            checksumDigest.BlockUpdate(buffer, offset, length);
+            return DigestUtilities.DoFinal(checksumDigest);
+        }
+
+        private static X509Certificate[] CloneChain(X509Certificate[] chain)
+        {
+            return (X509Certificate[])chain?.Clone();
+        }
+
+        private static string ConvertAlias(string alias)
+        {
+            return alias.ToLowerInvariant();
+        }
+
+        private static IDigest CreateChecksumDigest(char[] password)
+        {
+            IDigest digest = DigestUtilities.GetDigest("SHA-1");
+            AddPassword(digest, password);
+
+            //
+            // This "Mighty Aphrodite" string goes all the way back to the
+            // first java betas in the mid 90's, why who knows? But see
+            // https://cryptosense.com/mighty-aphrodite-dark-secrets-of-the-java-keystore/
+            //
+            byte[] prefix = Encoding.UTF8.GetBytes("Mighty Aphrodite");
+            digest.BlockUpdate(prefix, 0, prefix.Length);
+            return digest;
+        }
+
+        private static byte[] ReadBufferWithLength(BinaryReader br)
+        {
+            int length = ReadInt32(br);
+            return br.ReadBytes(length);
+        }
+
+        private static DateTime ReadDateTime(BinaryReader br)
+        {
+            DateTime unixMs = DateTimeUtilities.UnixMsToDateTime(Longs.ReverseBytes(br.ReadInt64()));
+            DateTime utc = new DateTime(unixMs.Ticks, DateTimeKind.Utc);
+            return utc;
+        }
+
+        private static short ReadInt16(BinaryReader br)
+        {
+            short n = br.ReadInt16();
+            n = (short)(((n & 0xFF) << 8) | ((n >> 8) & 0xFF));
+            return n;
+        }
+
+        private static int ReadInt32(BinaryReader br)
+        {
+            return Integers.ReverseBytes(br.ReadInt32());
+        }
+
+        private static X509Certificate ReadTypedCertificate(BinaryReader br, int storeVersion)
+        {
+            if (storeVersion == 2)
+            {
+                string certFormat = ReadUtf(br);
+                if ("X.509" != certFormat)
+                    throw new IOException("Unsupported certificate format: " + certFormat);
+            }
+
+            byte[] certData = ReadBufferWithLength(br);
+            try
+            {
+                return new X509Certificate(certData);
+            }
+            finally
+            {
+                Array.Clear(certData, 0, certData.Length);
+            }
+        }
+
+        private static string ReadUtf(BinaryReader br)
+        {
+            short length = ReadInt16(br);
+            byte[] utfBytes = br.ReadBytes(length);
+
+            /*
+             * 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");
+            }
+
+            return Encoding.UTF8.GetString(utfBytes);
+        }
+
+        private static void WriteBufferWithLength(BinaryWriter bw, byte[] buffer)
+        {
+            WriteInt32(bw, buffer.Length);
+            bw.Write(buffer);
+        }
+
+        private static void WriteDateTime(BinaryWriter bw, DateTime dateTime)
+        {
+            bw.Write(Longs.ReverseBytes(DateTimeUtilities.DateTimeToUnixMs(dateTime.ToUniversalTime())));
+        }
+
+        private static void WriteInt16(BinaryWriter bw, short n)
+        {
+            n = (short)(((n & 0xFF) << 8) | ((n >> 8) & 0xFF));
+            bw.Write(n);
+        }
+
+        private static void WriteInt32(BinaryWriter bw, int n)
+        {
+            bw.Write(Integers.ReverseBytes(n));
+        }
+
+        private static void WriteTypedCertificate(BinaryWriter bw, X509Certificate cert)
+        {
+            WriteUtf(bw, "X.509");
+            WriteBufferWithLength(bw, cert.GetEncoded());
+        }
+
+        private static void WriteUtf(BinaryWriter bw, string s)
+        {
+            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)
+            {
+                byte utfByte = utfBytes[i];
+                if (utfByte == 0 || (utfByte & 0x80) != 0)
+                    throw new NotSupportedException("Currently missing support for modified UTF-8 encoding in JKS");
+            }
+
+            WriteInt16(bw, Convert.ToInt16(utfBytes.Length));
+            bw.Write(utfBytes);
+        }
+
+        /**
+         * JksTrustedCertEntry is a internal container for the certificate entry.
+         */
+        private sealed class JksTrustedCertEntry
+        {
+            internal readonly DateTime date;
+            internal readonly X509Certificate cert;
+
+            internal JksTrustedCertEntry(DateTime date, X509Certificate cert)
+            {
+                this.date = date;
+                this.cert = cert;
+            }
+        }
+
+        private sealed class JksKeyEntry
+        {
+            internal readonly DateTime date;
+            internal readonly EncryptedPrivateKeyInfo keyData;
+            internal readonly X509Certificate[] chain;
+
+            internal JksKeyEntry(DateTime date, byte[] keyData, X509Certificate[] chain)
+            {
+                this.date = date;
+                this.keyData = EncryptedPrivateKeyInfo.GetInstance(Asn1Sequence.GetInstance(keyData));
+                this.chain = chain;
+            }
+        }
+
+        private sealed class ErasableByteStream
+            : MemoryStream
+        {
+            internal ErasableByteStream(byte[] buffer, int index, int count)
+                : base(buffer, index, count, false, true)
+            {
+            }
+
+            protected override void Dispose(bool disposing)
+            {
+                if (disposing)
+                {
+                    Position = 0L;
+
+                    byte[] rawStore = GetBuffer();
+                    Array.Clear(rawStore, 0, rawStore.Length);
+                }
+                base.Dispose(disposing);
+            }
+        }
+    }
+}
diff --git a/crypto/test/src/security/test/JksStoreTest.cs b/crypto/test/src/security/test/JksStoreTest.cs
new file mode 100644
index 000000000..335786f5e
--- /dev/null
+++ b/crypto/test/src/security/test/JksStoreTest.cs
@@ -0,0 +1,181 @@
+using System;
+using System.IO;
+
+using NUnit.Framework;
+
+using Org.BouncyCastle.Crypto;
+using Org.BouncyCastle.Crypto.Generators;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Math;
+using Org.BouncyCastle.Utilities.Encoders;
+
+namespace Org.BouncyCastle.Security.Tests
+{
+    [TestFixture]
+    public class JksStoreTest
+    {
+        private static readonly byte[] Test1 = Base64.Decode(
+            "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");
+
+        private static readonly byte[] Test2 = Base64.Decode(
+            "/u3+7QAAAAIAAAABAAAAAQADcnNhAAABgdHfddkAAAUBMIIE/TAOBgorBgEE"
+          + "ASoCEQEBBQAEggTp21B7oEFqhbDbkLpnFR9CHrE14vEnUQnusmHWlp+qs7iH"
+          + "jiVWbi2gjrebtmQ9GjKhevV4CKAYnEr6b2efRr0ZhvA8osHLTy7NA6eIvK5t"
+          + "NK+5+NLNF3D8NAj5flBcEvfNSminFe1w51/kXwGVMtxD1YtCAMhIbyAvYoSC"
+          + "gHDzShT29/JfX/yCqEbQv7/KhogcHxbd0wARBeRDJcLIHXRoqfVsWMHByray"
+          + "e6Y/EkCH9EelgFqz8W7Lg1bQdiLtsjSS9ktyppRwb8SCHKRwSsm3oDS9qwBU"
+          + "7LkQjNeQkrU5H/7tRPC4A/IY9Y4EtGDisH1hjYdhfOSDqNnA+1m1WtoIYQDy"
+          + "oZ6PRG3doiS8yy4oAtVqMbScxO5zhwhcMaHhyUGdvOWXj9N385lVnCUlTL3W"
+          + "M45CBrayEUyj8R5jjP4g2SQMxhiKx01822MQh7rTSrenH8fYzq1Op/NukoHx"
+          + "qkknulS9RTjPe85+5pXcADgoTaiNzAfN1ut8lqXj9Oytn5dFCzsTD9rGMa1H"
+          + "rCVTQrqZ/2mz/kRpt11D7UFcxJuTdbSvOrcGvv0ghYRat1om/+YGGbfah1Dv"
+          + "SJlKWiSF4ErMaU3V952ndTTdLWQ7Wlpb1H2UgKEQIS/mf7aUSvxTWvfjvrnX"
+          + "DcdIA8mmVlqgyPYW+hh6zc9hX9brnqtj5J+YQU1yVCP2k4Evw2FeRsLpl14g"
+          + "8kX/z2gQNg+MkGEpun1QT4EDLAAwuG525Q0552UgoSJ6dO6hBPHHblKmrgs4"
+          + "X0GUEbFLWH1EHd90ZyKAXK3bKGI24WKc5Jzay3ZOkobqKrH47qva21pLDx4p"
+          + "ndSROole3vc++Fw2jmlaLww3ZSFj7iiIK+Tm8RZpnhq9cS1yF60IxW98CuWS"
+          + "IfCiGPSmgFyujjjmZ8Q6gNWnPjCpTR49P/npThSbgm6Hn96Eh6EH/0RZi86I"
+          + "CKPu+NZRyxrI2YHASCAYEBaZIFIcwxGgrrnJdzucoByuKhqE2Ei6tcods5Qx"
+          + "f9z75p0a/0tciZ8RatiBPWGyxv9rsOS6Go+JSrEMX38N+XczDRgaxl7RF5iY"
+          + "/HFbz1qsE0A6OhfUJFlrwRdKVZthLBFefP+u0EVprMMGBNM8qO/FupqDTLcX"
+          + "tI/6wP6kilok3BDLKUtkIknWNDvy3sLSh/CaGYDbnQh8bWNFcXLE+Ue+0hUr"
+          + "IK7FPPQP6JV/n9Z/pAXf4LaQS4qtPdjSYY4wYmoj3QpEv84DzhGVEJjXfL9L"
+          + "iWVyCnWduHG23nttvKJNG4YDx7PEKWKIeBYGDei983B5vTuji2Xud8W3FcVo"
+          + "inWyWg0SKu4E451xgEbqH3PVGN69BOvLVRwZdHTLt/Oq2O062qkEMYv/XmzT"
+          + "eDS7PNHN8TA5gdnMAZYCnE0mTxGSScg4s0hemMndBL02QOBkGxtbNs02M+ha"
+          + "UGTmUmjQUwz871Cge9I9+c+TxX/4yfFnO+LFNsM4sM+cACdPFQEk+Cgl8o6T"
+          + "4zcw8LEWZ4jWtCIrgrUM3pRFW5OMaP/K5/FmxWJddeFbkUfiPYi1hO/DbwU6"
+          + "gXlWZIpCTbUavueVl1xu8LBqMP8J3OmbraS2Ty5v5E9zYXNuI/VTB/ZZsScL"
+          + "lb8yRAAxHZwREx6HSPzBVFAgcv7JlCFPoA4c9dWv2JWLxtCRHwda9RMmOZA6"
+          + "Fltm3MaPTcLrAAAAAQAFWC41MDkAAANhMIIDXTCCAkWgAwIBAgIEGRFyZTAN"
+          + "BgkqhkiG9w0BAQsFADBfMQswCQYDVQQGEwJBVTEMMAoGA1UECBMDVmljMRAw"
+          + "DgYDVQQHEwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMQ4wDAYDVQQLEwVU"
+          + "ZXN0MjEOMAwGA1UEAxMFVGVzdDIwHhcNMjIwNzA2MDQ1ODA5WhcNMjIxMDA0"
+          + "MDQ1ODA5WjBfMQswCQYDVQQGEwJBVTEMMAoGA1UECBMDVmljMRAwDgYDVQQH"
+          + "EwdVbmtub3duMRAwDgYDVQQKEwdVbmtub3duMQ4wDAYDVQQLEwVUZXN0MjEO"
+          + "MAwGA1UEAxMFVGVzdDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB"
+          + "AQDMISX002jO7VkE4igeOwcqko0xnCOqZZtUqGMPsNS2ja3Tii/JuAzdAoP5"
+          + "CMltZpnbnigMYBJ350l7uXeu9bEh04KORuU1MJD4/8UCTudNk+Yeb8YWXov/"
+          + "I0Ahvgqnhs4qjMnqtpbUwYmybjshEUFS2Bt6fBpfKOt8gjacumxAUvEPov+/"
+          + "0TPFbbZ1HTR7uMfKdqOtzxJ26+CYwex+Xt/XslsBCvwvBvfOo2hm9wGd1R+J"
+          + "kpTLP5Z1OZhRSgAobmMZ2A3qt4q4bUlVJp+BORd0iwvhqqL2jVkx6EyJZcbo"
+          + "z69/aWuhb38FQ74ZHaVcdI57ctWwZ3hCWAaLx32A3puDAgMBAAGjITAfMB0G"
+          + "A1UdDgQWBBRDcYa1cHH0r0G4DZ0c64QF+5aWWTANBgkqhkiG9w0BAQsFAAOC"
+          + "AQEABdqABy8IEH6w7kKw99dv12GkmGe7xj+lknr6D2keF4apFAaA3ndA4HAG"
+          + "P+VoRPZtGIi5a3KJypE67LYDVEmu3d4EkImP+NUtf/kIl4C874JRE490JRKE"
+          + "zkWzWFDgM0rGS8b6DpKcC6BLE6UGRbASdvQx/6JO74ni+ObPrUSNqATScf6T"
+          + "Evaf2WqUpL2XGOc05w5k/0q2jy+bUKNM70DsvEXLUpZOTZC6M71WyHXHm0y5"
+          + "7zv3f3TPShwxVCj/DVcUQ4TS9FeHbAghx2j8n4vxw4JqqcpXKPox64x86fup"
+          + "QD1ljGJglRyx7R7CQACzgInjjq6JK2zkzbeDktpOn28RwZWCt1dw2vEperSx"
+          + "4fNHHstpt64C");
+
+        [Test]
+        public void TestJks()
+        {
+            JksStore ks = new JksStore();
+
+            ks.Load(new MemoryStream(Test1, false), "fredfred".ToCharArray());
+
+            ks.GetKey("rsa", "samsam".ToCharArray());
+
+            IAsymmetricCipherKeyPairGenerator kpGen = new RsaKeyPairGenerator();
+            kpGen.Init(new RsaKeyGenerationParameters(new BigInteger("10001", 16), new SecureRandom(), 1024, 100));
+
+            AsymmetricCipherKeyPair kp = kpGen.GenerateKeyPair();
+
+            ks.SetKeyEntry("fred", kp.Private, "bobbob".ToCharArray(), ks.GetCertificateChain("rsa"));
+
+            ks.GetKey("fred", "bobbob".ToCharArray());
+
+            MemoryStream bOut = new MemoryStream();
+
+            ks.Save(bOut, "billbill".ToCharArray());
+
+            ks = new JksStore();
+
+            ks.Load(new MemoryStream(Test2, false), "samsam".ToCharArray());
+
+            AsymmetricKeyParameter privKey = ks.GetKey("rsa", "samsam".ToCharArray());
+
+            ks = new JksStore();
+
+            try
+            {
+                ks.Load(new MemoryStream(bOut.ToArray()), "wrong".ToCharArray());
+                Assert.Fail("Exception expected for Load() with wrong password");
+            }
+            catch (Exception)
+            {
+                // Expected
+            }
+
+            ks.Load(new MemoryStream(bOut.ToArray()), "billbill".ToCharArray());
+
+            privKey = ks.GetKey("rsa", "samsam".ToCharArray());
+
+            privKey = ks.GetKey("fred", "bobbob".ToCharArray());
+
+            Assert.IsNull(ks.GetCertificate("george"));
+            Assert.IsNull(ks.GetCertificateChain("george"));
+            Assert.IsNull(ks.GetKey("george", "ignored".ToCharArray()));
+
+            try
+            {
+                privKey = ks.GetKey("fred", "wrong".ToCharArray());
+                Assert.Fail("Exception expected for GetKey() with wrong password");
+            }
+            catch (Exception)
+            {
+                // Expected
+            }
+        }
+    }
+}