summary refs log tree commit diff
path: root/crypto/src/security/JksStore.cs
diff options
context:
space:
mode:
authorPeter Dettman <peter.dettman@bouncycastle.org>2022-12-21 12:34:49 +0700
committerPeter Dettman <peter.dettman@bouncycastle.org>2022-12-21 12:34:49 +0700
commit3c508655db514af6702bb51be63dc0b3d176e11b (patch)
tree2ae0176a5616ecc1b0b30a85a29f2805dc4b5241 /crypto/src/security/JksStore.cs
parentAesWrap: update from bc-java (diff)
downloadBouncyCastle.NET-ed25519-3c508655db514af6702bb51be63dc0b3d176e11b.tar.xz
Span-based alternatives to char[]
Diffstat (limited to 'crypto/src/security/JksStore.cs')
-rw-r--r--crypto/src/security/JksStore.cs343
1 files changed, 292 insertions, 51 deletions
diff --git a/crypto/src/security/JksStore.cs b/crypto/src/security/JksStore.cs
index c5ef92a70..4df0b39db 100644
--- a/crypto/src/security/JksStore.cs
+++ b/crypto/src/security/JksStore.cs
@@ -50,11 +50,15 @@ namespace Org.BouncyCastle.Security
         /// <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 NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+            return GetKey(alias, password.AsSpan());
+#else
+            if (alias == null)
+                throw new ArgumentNullException(nameof(alias));
+
             if (!m_keyEntries.TryGetValue(alias, out JksKeyEntry keyEntry))
                 return null;
 
@@ -84,15 +88,89 @@ namespace Org.BouncyCastle.Security
                 throw new IOException("cannot recover key");
 
             return PrivateKeyFactory.CreateKey(pkcs8Key);
+#endif
         }
 
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+        /// <exception cref="IOException"/>
+        public AsymmetricKeyParameter GetKey(string alias, ReadOnlySpan<char> password)
+        {
+            if (alias == null)
+                throw new ArgumentNullException(nameof(alias));
+
+            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.FixedTimeEquals(20, encryptedData, pkcs8Len + 20, checksum, 0))
+                throw new IOException("cannot recover key");
+
+            return PrivateKeyFactory.CreateKey(pkcs8Key);
+        }
+#endif
+
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+        private byte[] GetKeyChecksum(IDigest digest, ReadOnlySpan<char> password, ReadOnlySpan<byte> pkcs8Key)
+        {
+            AddPassword(digest, password);
+
+            return DigestUtilities.DoFinal(digest, pkcs8Key);
+        }
+#else
         private byte[] GetKeyChecksum(IDigest digest, char[] password, byte[] pkcs8Key)
         {
             AddPassword(digest, password);
 
             return DigestUtilities.DoFinal(digest, pkcs8Key);
         }
+#endif
+
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+        private byte[] CalculateKeyStream(IDigest digest, ReadOnlySpan<char> password, ReadOnlySpan<byte> salt,
+            int count)
+        {
+            byte[] keyStream = new byte[count];
 
+            Span<byte> hash = stackalloc byte[20];
+            hash.CopyFrom(salt);
+
+            int index = 0;
+            while (index < count)
+            {
+                AddPassword(digest, password);
+
+                digest.BlockUpdate(hash);
+                digest.DoFinal(hash);
+
+                int length = System.Math.Min(hash.Length, keyStream.Length - index);
+                keyStream.AsSpan(index, length).CopyFrom(hash);
+                index += length;
+            }
+
+            return keyStream;
+        }
+#else
         private byte[] CalculateKeyStream(IDigest digest, char[] password, byte[] salt, int count)
         {
             byte[] keyStream = new byte[count];
@@ -113,6 +191,7 @@ namespace Org.BouncyCastle.Security
 
             return keyStream;
         }
+#endif
 
         public X509Certificate[] GetCertificateChain(string alias)
         {
@@ -128,7 +207,10 @@ namespace Org.BouncyCastle.Security
                 return certEntry.cert;
 
             if (m_keyEntries.TryGetValue(alias, out var keyEntry))
-                return keyEntry.chain?[0];
+            {
+                var chain = keyEntry.chain;
+                return chain == null || chain.Length == 0 ? null : chain[0];
+            }
 
             return null;
         }
@@ -147,6 +229,52 @@ namespace Org.BouncyCastle.Security
         /// <exception cref="IOException"/>
         public void SetKeyEntry(string alias, AsymmetricKeyParameter key, char[] password, X509Certificate[] chain)
         {
+            if (password == null)
+                throw new ArgumentNullException(nameof(password));
+
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+            SetKeyEntry(alias, key, password.AsSpan(), chain);
+#else
+            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 = CryptoServicesRegistrar.GetSecureRandom();
+            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);
+            }
+#endif
+        }
+
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+        /// <exception cref="IOException"/>
+        public void SetKeyEntry(string alias, AsymmetricKeyParameter key, ReadOnlySpan<char> password,
+            X509Certificate[] chain)
+        {
             alias = ConvertAlias(alias);
 
             if (ContainsAlias(alias))
@@ -180,6 +308,7 @@ namespace Org.BouncyCastle.Security
                 throw new IOException("unable to encode encrypted private key", e);
             }
         }
+#endif
 
         /// <exception cref="IOException"/>
         public void SetKeyEntry(string alias, byte[] key, X509Certificate[] chain)
@@ -254,12 +383,36 @@ namespace Org.BouncyCastle.Security
         /// <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));
 
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+            Save(stream, password.AsSpan());
+#else
+            if (stream == null)
+                throw new ArgumentNullException(nameof(stream));
+
             IDigest checksumDigest = CreateChecksumDigest(password);
+
+            SaveStream(stream, checksumDigest);
+#endif
+        }
+
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+        /// <exception cref="IOException"/>
+        public void Save(Stream stream, ReadOnlySpan<char> password)
+        {
+            if (stream == null)
+                throw new ArgumentNullException(nameof(stream));
+
+            IDigest checksumDigest = CreateChecksumDigest(password);
+
+            SaveStream(stream, checksumDigest);
+        }
+#endif
+
+        private void SaveStream(Stream stream, IDigest checksumDigest)
+        {
             BinaryWriter bw = new BinaryWriter(new DigestStream(stream, null, checksumDigest));
 
             BinaryWriters.WriteInt32BigEndian(bw, Magic);
@@ -286,7 +439,7 @@ namespace Org.BouncyCastle.Security
                 }
             }
 
-            foreach (var entry in m_certificateEntries) 
+            foreach (var entry in m_certificateEntries)
             {
                 string alias = entry.Key;
                 JksTrustedCertEntry certEntry = entry.Value;
@@ -302,74 +455,101 @@ namespace Org.BouncyCastle.Security
             bw.Flush();
         }
 
+        /// <remarks>WARNING: If <paramref name="password"/> is <c>null</c>, no integrity check is performed.</remarks>
         /// <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))
+            {
+                LoadStream(storeStream);
+            }
+        }
+
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+        /// <exception cref="IOException"/>
+        public void Load(Stream stream, ReadOnlySpan<char> password)
+        {
+            if (stream == null)
+                throw new ArgumentNullException(nameof(stream));
 
             using (var storeStream = ValidateStream(stream, password))
             {
-                BinaryReader br = new BinaryReader(storeStream);
+                LoadStream(storeStream);
+            }
+        }
+#endif
 
-                int magic = BinaryReaders.ReadInt32BigEndian(br);
-                int storeVersion = BinaryReaders.ReadInt32BigEndian(br);
+        /// <summary>Load without any integrity check.</summary>
+        /// <exception cref="IOException"/>
+        public void LoadUnchecked(Stream stream)
+        {
+            Load(stream, null);
+        }
+
+        private void LoadStream(ErasableByteStream storeStream)
+        {
+            m_certificateEntries.Clear();
+            m_keyEntries.Clear();
 
-                if (!(magic == Magic && (storeVersion == 1 || storeVersion == 2)))
-                    throw new IOException("Invalid keystore format");
+            BinaryReader br = new BinaryReader(storeStream);
 
-                int numEntries = BinaryReaders.ReadInt32BigEndian(br);
+            int magic = BinaryReaders.ReadInt32BigEndian(br);
+            int storeVersion = BinaryReaders.ReadInt32BigEndian(br);
 
-                for (int t = 0; t < numEntries; t++)
-                {
-                    int tag = BinaryReaders.ReadInt32BigEndian(br);
+            if (!(magic == Magic && (storeVersion == 1 || storeVersion == 2)))
+                throw new IOException("Invalid keystore format");
 
-                    switch (tag)
-                    {
-                    case 1: // keys
-                    {
-                        string alias = ReadUtf(br);
-                        DateTime date = ReadDateTime(br);
+            int numEntries = BinaryReaders.ReadInt32BigEndian(br);
+
+            for (int t = 0; t < numEntries; t++)
+            {
+                int tag = BinaryReaders.ReadInt32BigEndian(br);
 
-                        // encrypted key data
-                        byte[] keyData = ReadBufferWithInt32Length(br);
+                switch (tag)
+                {
+                case 1: // keys
+                {
+                    string alias = ReadUtf(br);
+                    DateTime date = ReadDateTime(br);
 
-                        // certificate chain
-                        int chainLength = BinaryReaders.ReadInt32BigEndian(br);
-                        X509Certificate[] chain = null;
-                        if (chainLength > 0)
+                    // encrypted key data
+                    byte[] keyData = ReadBufferWithInt32Length(br);
+
+                    // certificate chain
+                    int chainLength = BinaryReaders.ReadInt32BigEndian(br);
+                    X509Certificate[] chain = null;
+                    if (chainLength > 0)
+                    {
+                        var certs = new List<X509Certificate>(System.Math.Min(10, chainLength));
+                        for (int certNo = 0; certNo != chainLength; certNo++)
                         {
-                            var certs = new List<X509Certificate>(System.Math.Min(10, chainLength));
-                            for (int certNo = 0; certNo != chainLength; certNo++)
-                            {
-                                certs.Add(ReadTypedCertificate(br, storeVersion));
-                            }
-                            chain = certs.ToArray();
+                            certs.Add(ReadTypedCertificate(br, storeVersion));
                         }
-                        m_keyEntries.Add(alias, new JksKeyEntry(date, keyData, chain));
-                        break;
+                        chain = certs.ToArray();
                     }
-                    case 2: // certificate
-                    {
-                        string alias = ReadUtf(br);
-                        DateTime date = ReadDateTime(br);
+                    m_keyEntries.Add(alias, new JksKeyEntry(date, keyData, chain));
+                    break;
+                }
+                case 2: // certificate
+                {
+                    string alias = ReadUtf(br);
+                    DateTime date = ReadDateTime(br);
 
-                        X509Certificate cert = ReadTypedCertificate(br, storeVersion);
+                    X509Certificate cert = ReadTypedCertificate(br, storeVersion);
 
-                        m_certificateEntries.Add(alias, new JksTrustedCertEntry(date, cert));
-                        break;
-                    }
-                    default:
-                        throw new IOException("unable to discern entry type");
-                    }
+                    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");
             }
+
+            if (storeStream.Position != storeStream.Length)
+                throw new IOException("password incorrect or store tampered with");
         }
 
         /*
@@ -391,7 +571,11 @@ namespace Org.BouncyCastle.Security
 
             if (password != null)
             {
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+                byte[] checksum = CalculateChecksum(password, rawStore.AsSpan(0, checksumPos));
+#else
                 byte[] checksum = CalculateChecksum(password, rawStore, 0, checksumPos);
+#endif
 
                 if (!Arrays.FixedTimeEquals(20, checksum, 0, rawStore, checksumPos))
                 {
@@ -403,6 +587,36 @@ namespace Org.BouncyCastle.Security
             return new ErasableByteStream(rawStore, 0, checksumPos);
         }
 
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+        /// <exception cref="IOException"/>
+        private ErasableByteStream ValidateStream(Stream inputStream, ReadOnlySpan<char> password)
+        {
+            byte[] rawStore = Streams.ReadAll(inputStream);
+            int checksumPos = rawStore.Length - 20;
+
+            byte[] checksum = CalculateChecksum(password, rawStore.AsSpan(0, checksumPos));
+
+            if (!Arrays.FixedTimeEquals(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);
+        }
+#endif
+
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+        private static void AddPassword(IDigest digest, ReadOnlySpan<char> password)
+        {
+            // Encoding.BigEndianUnicode
+            for (int i = 0; i < password.Length; ++i)
+            {
+                digest.Update((byte)(password[i] >> 8));
+                digest.Update((byte)password[i]);
+            }
+        }
+#else
         private static void AddPassword(IDigest digest, char[] password)
         {
             // Encoding.BigEndianUnicode
@@ -412,13 +626,23 @@ namespace Org.BouncyCastle.Security
                 digest.Update((byte)password[i]);
             }
         }
+#endif
 
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+        private static byte[] CalculateChecksum(ReadOnlySpan<char> password, ReadOnlySpan<byte> buffer)
+        {
+            IDigest checksumDigest = CreateChecksumDigest(password);
+            checksumDigest.BlockUpdate(buffer);
+            return DigestUtilities.DoFinal(checksumDigest);
+        }
+#else
         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);
         }
+#endif
 
         private static X509Certificate[] CloneChain(X509Certificate[] chain)
         {
@@ -430,6 +654,22 @@ namespace Org.BouncyCastle.Security
             return alias.ToLowerInvariant();
         }
 
+#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER
+        private static IDigest CreateChecksumDigest(ReadOnlySpan<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);
+            return digest;
+        }
+#else
         private static IDigest CreateChecksumDigest(char[] password)
         {
             IDigest digest = DigestUtilities.GetDigest("SHA-1");
@@ -444,6 +684,7 @@ namespace Org.BouncyCastle.Security
             digest.BlockUpdate(prefix, 0, prefix.Length);
             return digest;
         }
+#endif
 
         private static byte[] ReadBufferWithInt16Length(BinaryReader br)
         {