diff options
Diffstat (limited to 'crypto/src/security/JksStore.cs')
-rw-r--r-- | crypto/src/security/JksStore.cs | 343 |
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) { |