From 3c508655db514af6702bb51be63dc0b3d176e11b Mon Sep 17 00:00:00 2001 From: Peter Dettman Date: Wed, 21 Dec 2022 12:34:49 +0700 Subject: Span-based alternatives to char[] --- crypto/src/cmp/ProtectedPkiMessage.cs | 16 + crypto/src/cms/CMSPBEKey.cs | 24 +- crypto/src/cms/PKCS5Scheme2PBEKey.cs | 14 +- crypto/src/cms/PKCS5Scheme2UTF8PBEKey.cs | 14 +- .../src/crmf/CertificateRequestMessageBuilder.cs | 6 + crypto/src/crmf/PKMacBuilder.cs | 48 ++- .../src/crmf/ProofOfPossessionSigningKeyBuilder.cs | 38 ++- crypto/src/crypto/PbeParametersGenerator.cs | 43 ++- .../generators/OpenSSLPBEParametersGenerator.cs | 17 +- crypto/src/openssl/MiscPemGenerator.cs | 69 ++++- crypto/src/openssl/PEMUtilities.cs | 111 ++++++- crypto/src/security/JksStore.cs | 343 ++++++++++++++++++--- crypto/src/util/Strings.cs | 22 ++ 13 files changed, 673 insertions(+), 92 deletions(-) diff --git a/crypto/src/cmp/ProtectedPkiMessage.cs b/crypto/src/cmp/ProtectedPkiMessage.cs index 8c9a4b152..df4c45143 100644 --- a/crypto/src/cmp/ProtectedPkiMessage.cs +++ b/crypto/src/cmp/ProtectedPkiMessage.cs @@ -119,6 +119,22 @@ namespace Org.BouncyCastle.Cmp return Arrays.FixedTimeEquals(result.Collect(), m_pkiMessage.Protection.GetBytes()); } +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public virtual bool Verify(PKMacBuilder pkMacBuilder, ReadOnlySpan password) + { + if (!CmpObjectIdentifiers.passwordBasedMac.Equals(m_pkiMessage.Header.ProtectionAlg.Algorithm)) + throw new InvalidOperationException("protection algorithm is not mac based"); + + PbmParameter parameter = PbmParameter.GetInstance(m_pkiMessage.Header.ProtectionAlg.Parameters); + + pkMacBuilder.SetParameters(parameter); + + IBlockResult result = Process(pkMacBuilder.Build(password).CreateCalculator()); + + return Arrays.FixedTimeEquals(result.Collect(), m_pkiMessage.Protection.GetBytes()); + } +#endif + private TResult Process(IStreamCalculator streamCalculator) { Asn1EncodableVector avec = new Asn1EncodableVector(); diff --git a/crypto/src/cms/CMSPBEKey.cs b/crypto/src/cms/CMSPBEKey.cs index 78360c2cd..4b3e542ee 100644 --- a/crypto/src/cms/CMSPBEKey.cs +++ b/crypto/src/cms/CMSPBEKey.cs @@ -45,7 +45,29 @@ namespace Org.BouncyCastle.Cms this.iterationCount = kdfParams.IterationCount.IntValue; } - ~CmsPbeKey() +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public CmsPbeKey(ReadOnlySpan password, ReadOnlySpan salt, int iterationCount) + { + this.password = password.ToArray(); + this.salt = salt.ToArray(); + this.iterationCount = iterationCount; + } + + public CmsPbeKey(ReadOnlySpan password, AlgorithmIdentifier keyDerivationAlgorithm) + { + if (!keyDerivationAlgorithm.Algorithm.Equals(PkcsObjectIdentifiers.IdPbkdf2)) + throw new ArgumentException("Unsupported key derivation algorithm: " + + keyDerivationAlgorithm.Algorithm); + + Pbkdf2Params kdfParams = Pbkdf2Params.GetInstance(keyDerivationAlgorithm.Parameters.ToAsn1Object()); + + this.password = password.ToArray(); + this.salt = kdfParams.GetSalt(); + this.iterationCount = kdfParams.IterationCount.IntValue; + } +#endif + + ~CmsPbeKey() { Array.Clear(this.password, 0, this.password.Length); } diff --git a/crypto/src/cms/PKCS5Scheme2PBEKey.cs b/crypto/src/cms/PKCS5Scheme2PBEKey.cs index 6606d5c45..78238292d 100644 --- a/crypto/src/cms/PKCS5Scheme2PBEKey.cs +++ b/crypto/src/cms/PKCS5Scheme2PBEKey.cs @@ -29,7 +29,19 @@ namespace Org.BouncyCastle.Cms { } - internal override KeyParameter GetEncoded( +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public Pkcs5Scheme2PbeKey(ReadOnlySpan password, ReadOnlySpan salt, int iterationCount) + : base(password, salt, iterationCount) + { + } + + public Pkcs5Scheme2PbeKey(ReadOnlySpan password, AlgorithmIdentifier keyDerivationAlgorithm) + : base(password, keyDerivationAlgorithm) + { + } +#endif + + internal override KeyParameter GetEncoded( string algorithmOid) { Pkcs5S2ParametersGenerator gen = new Pkcs5S2ParametersGenerator(); diff --git a/crypto/src/cms/PKCS5Scheme2UTF8PBEKey.cs b/crypto/src/cms/PKCS5Scheme2UTF8PBEKey.cs index e2a09b760..68eff7b44 100644 --- a/crypto/src/cms/PKCS5Scheme2UTF8PBEKey.cs +++ b/crypto/src/cms/PKCS5Scheme2UTF8PBEKey.cs @@ -29,7 +29,19 @@ namespace Org.BouncyCastle.Cms { } - internal override KeyParameter GetEncoded( +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public Pkcs5Scheme2Utf8PbeKey(ReadOnlySpan password, ReadOnlySpan salt, int iterationCount) + : base(password, salt, iterationCount) + { + } + + public Pkcs5Scheme2Utf8PbeKey(ReadOnlySpan password, AlgorithmIdentifier keyDerivationAlgorithm) + : base(password, keyDerivationAlgorithm) + { + } +#endif + + internal override KeyParameter GetEncoded( string algorithmOid) { Pkcs5S2ParametersGenerator gen = new Pkcs5S2ParametersGenerator(); diff --git a/crypto/src/crmf/CertificateRequestMessageBuilder.cs b/crypto/src/crmf/CertificateRequestMessageBuilder.cs index 38e95dfe7..363bfd136 100644 --- a/crypto/src/crmf/CertificateRequestMessageBuilder.cs +++ b/crypto/src/crmf/CertificateRequestMessageBuilder.cs @@ -163,7 +163,13 @@ namespace Org.BouncyCastle.Crmf return this; } + [Obsolete("Use 'SetAuthInfoPKMacBuilder' instead")] public CertificateRequestMessageBuilder SetAuthInfoPKMAC(PKMacBuilder pkmacFactory, char[] password) + { + return SetAuthInfoPKMacBuilder(pkmacFactory, password); + } + + public CertificateRequestMessageBuilder SetAuthInfoPKMacBuilder(PKMacBuilder pkmacFactory, char[] password) { this._pkMacBuilder = pkmacFactory; this._password = password; diff --git a/crypto/src/crmf/PKMacBuilder.cs b/crypto/src/crmf/PKMacBuilder.cs index 7261a9daf..6db80325d 100644 --- a/crypto/src/crmf/PKMacBuilder.cs +++ b/crypto/src/crmf/PKMacBuilder.cs @@ -217,17 +217,31 @@ namespace Org.BouncyCastle.Crmf /// IMacFactory public IMacFactory Build(char[] password) { - if (parameters != null) - return GenCalculator(parameters, password); - - byte[] salt = new byte[saltLength]; +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + return Build(password.AsSpan()); +#else + PbmParameter pbmParameter = parameters; + if (pbmParameter == null) + { + pbmParameter = GenParameters(); + } - this.random = CryptoServicesRegistrar.GetSecureRandom(random); + return GenCalculator(pbmParameter, password); +#endif + } - random.NextBytes(salt); +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public IMacFactory Build(ReadOnlySpan password) + { + PbmParameter pbmParameter = parameters; + if (pbmParameter == null) + { + pbmParameter = GenParameters(); + } - return GenCalculator(new PbmParameter(salt, owf, iterationCount, mac), password); + return GenCalculator(pbmParameter, password); } +#endif private void CheckIterationCountCeiling(int iterationCount) { @@ -235,7 +249,19 @@ namespace Org.BouncyCastle.Crmf throw new ArgumentException("iteration count exceeds limit (" + iterationCount + " > " + maxIterations + ")"); } +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + private IMacFactory GenCalculator(PbmParameter parameters, ReadOnlySpan password) + { + return GenCalculator(parameters, Strings.ToUtf8ByteArray(password)); + } +#else private IMacFactory GenCalculator(PbmParameter parameters, char[] password) + { + return GenCalculator(parameters, Strings.ToUtf8ByteArray(password)); + } +#endif + + private IMacFactory GenCalculator(PbmParameter parameters, byte[] pw) { // From RFC 4211 // @@ -252,7 +278,6 @@ namespace Org.BouncyCastle.Crmf // MAC = HASH( K XOR opad, HASH( K XOR ipad, data) ) // // Where opad and ipad are defined in [HMAC]. - byte[] pw = Strings.ToUtf8ByteArray(password); byte[] salt = parameters.Salt.GetOctets(); byte[] K = new byte[pw.Length + salt.Length]; @@ -280,5 +305,12 @@ namespace Org.BouncyCastle.Crmf return new PKMacFactory(key, parameters); } + + private PbmParameter GenParameters() + { + byte[] salt = SecureRandom.GetNextBytes(CryptoServicesRegistrar.GetSecureRandom(random), saltLength); + + return new PbmParameter(salt, owf, iterationCount, mac); + } } } diff --git a/crypto/src/crmf/ProofOfPossessionSigningKeyBuilder.cs b/crypto/src/crmf/ProofOfPossessionSigningKeyBuilder.cs index 4cd568e81..4530b18b8 100644 --- a/crypto/src/crmf/ProofOfPossessionSigningKeyBuilder.cs +++ b/crypto/src/crmf/ProofOfPossessionSigningKeyBuilder.cs @@ -38,27 +38,22 @@ namespace Org.BouncyCastle.Crmf { IMacFactory fact = generator.Build(password); - byte[] d = _pubKeyInfo.GetDerEncoded(); - - IStreamCalculator calc = fact.CreateCalculator(); - using (var stream = calc.Stream) - { - stream.Write(d, 0, d.Length); - } + return ImplSetPublicKeyMac(fact); + } - this._publicKeyMAC = new PKMacValue( - (AlgorithmIdentifier)fact.AlgorithmDetails, - new DerBitString(calc.GetResult().Collect())); +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public ProofOfPossessionSigningKeyBuilder SetPublicKeyMac(PKMacBuilder generator, ReadOnlySpan password) + { + IMacFactory fact = generator.Build(password); - return this; + return ImplSetPublicKeyMac(fact); } +#endif public PopoSigningKey Build(ISignatureFactory signer) { if (_name != null && _publicKeyMAC != null) - { throw new InvalidOperationException("name and publicKeyMAC cannot both be set."); - } PopoSigningKeyInput popo; @@ -86,5 +81,22 @@ namespace Org.BouncyCastle.Crmf return new PopoSigningKey(popo, (AlgorithmIdentifier)signer.AlgorithmDetails, new DerBitString(signature)); } + + private ProofOfPossessionSigningKeyBuilder ImplSetPublicKeyMac(IMacFactory fact) + { + byte[] d = _pubKeyInfo.GetDerEncoded(); + + IStreamCalculator calc = fact.CreateCalculator(); + using (var stream = calc.Stream) + { + stream.Write(d, 0, d.Length); + } + + this._publicKeyMAC = new PKMacValue( + (AlgorithmIdentifier)fact.AlgorithmDetails, + new DerBitString(calc.GetResult().Collect())); + + return this; + } } } diff --git a/crypto/src/crypto/PbeParametersGenerator.cs b/crypto/src/crypto/PbeParametersGenerator.cs index 86927d230..8c3ac30a8 100644 --- a/crypto/src/crypto/PbeParametersGenerator.cs +++ b/crypto/src/crypto/PbeParametersGenerator.cs @@ -44,6 +44,15 @@ namespace Org.BouncyCastle.Crypto this.mIterationCount = iterationCount; } +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public virtual void Init(ReadOnlySpan password, ReadOnlySpan salt, int iterationCount) + { + this.mPassword = password.ToArray(); + this.mSalt = salt.ToArray(); + this.mIterationCount = iterationCount; + } +#endif + public virtual byte[] Password { get { return Arrays.Clone(mPassword); } @@ -105,9 +114,21 @@ namespace Org.BouncyCastle.Crypto if (password == null) return new byte[0]; - return Encoding.UTF8.GetBytes(password); + return Strings.ToUtf8ByteArray(password); } +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public static byte[] Pkcs5PasswordToBytes(ReadOnlySpan password) + { + return Strings.ToByteArray(password); + } + + public static byte[] Pkcs5PasswordToUtf8Bytes(ReadOnlySpan password) + { + return Strings.ToUtf8ByteArray(password); + } +#endif + /** * converts a password to a byte array according to the scheme in * Pkcs12 (unicode, big endian, 2 zero pad bytes at the end). @@ -137,5 +158,25 @@ namespace Org.BouncyCastle.Crypto return bytes; } + +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public static byte[] Pkcs12PasswordToBytes(ReadOnlySpan password) + { + return Pkcs12PasswordToBytes(password, false); + } + + public static byte[] Pkcs12PasswordToBytes(ReadOnlySpan password, bool wrongPkcs12Zero) + { + if (password.IsEmpty) + return new byte[wrongPkcs12Zero ? 2 : 0]; + + // +1 for extra 2 pad bytes. + byte[] bytes = new byte[(password.Length + 1) * 2]; + + Encoding.BigEndianUnicode.GetBytes(password, bytes); + + return bytes; + } +#endif } } diff --git a/crypto/src/crypto/generators/OpenSSLPBEParametersGenerator.cs b/crypto/src/crypto/generators/OpenSSLPBEParametersGenerator.cs index 448cd1920..9bdb82317 100644 --- a/crypto/src/crypto/generators/OpenSSLPBEParametersGenerator.cs +++ b/crypto/src/crypto/generators/OpenSSLPBEParametersGenerator.cs @@ -58,10 +58,23 @@ namespace Org.BouncyCastle.Crypto.Generators base.Init(password, salt, 1); } - /** +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public override void Init(ReadOnlySpan password, ReadOnlySpan salt, int iterationCount) + { + // Ignore the provided iterationCount + base.Init(password, salt, 1); + } + + public virtual void Init(ReadOnlySpan password, ReadOnlySpan salt) + { + base.Init(password, salt, 1); + } +#endif + + /** * the derived key function, the ith hash of the password and the salt. */ - private byte[] GenerateDerivedKey( + private byte[] GenerateDerivedKey( int bytesNeeded) { byte[] buf = new byte[digest.GetDigestSize()]; diff --git a/crypto/src/openssl/MiscPemGenerator.cs b/crypto/src/openssl/MiscPemGenerator.cs index ada0b84ed..0e918f793 100644 --- a/crypto/src/openssl/MiscPemGenerator.cs +++ b/crypto/src/openssl/MiscPemGenerator.cs @@ -128,20 +128,62 @@ namespace Org.BouncyCastle.OpenSsl return new PemObject(type, encoding); } -// private string GetHexEncoded(byte[] bytes) -// { -// bytes = Hex.Encode(bytes); -// -// char[] chars = new char[bytes.Length]; -// -// for (int i = 0; i != bytes.Length; i++) -// { -// chars[i] = (char)bytes[i]; -// } -// -// return new string(chars); -// } +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + private static PemObject CreatePemObject(object obj, string algorithm, ReadOnlySpan password, + SecureRandom random) + { + if (obj == null) + throw new ArgumentNullException("obj"); + if (algorithm == null) + throw new ArgumentNullException("algorithm"); + if (random == null) + throw new ArgumentNullException("random"); + + if (obj is AsymmetricCipherKeyPair keyPair) + { + return CreatePemObject(keyPair.Private, algorithm, password, random); + } + + string type = null; + byte[] keyData = null; + if (obj is AsymmetricKeyParameter akp) + { + if (akp.IsPrivate) + { + keyData = EncodePrivateKey(akp, out type); + } + } + + if (type == null || keyData == null) + { + // TODO Support other types? + throw new PemGenerationException("Object type not supported: " + Platform.GetTypeName(obj)); + } + + + string dekAlgName = algorithm.ToUpperInvariant(); + + // Note: For backward compatibility + if (dekAlgName == "DESEDE") + { + dekAlgName = "DES-EDE3-CBC"; + } + + int ivLength = Platform.StartsWith(dekAlgName, "AES-") ? 16 : 8; + + byte[] iv = new byte[ivLength]; + random.NextBytes(iv); + + byte[] encData = PemUtilities.Crypt(true, keyData, password, dekAlgName, iv); + + var headers = new List(2); + headers.Add(new PemHeader("Proc-Type", "4,ENCRYPTED")); + headers.Add(new PemHeader("DEK-Info", dekAlgName + "," + Hex.ToHexString(iv).ToUpperInvariant())); + + return new PemObject(type, headers, encData); + } +#else private static PemObject CreatePemObject( object obj, string algorithm, @@ -201,6 +243,7 @@ namespace Org.BouncyCastle.OpenSsl return new PemObject(type, headers, encData); } +#endif private static byte[] EncodePrivateKey( AsymmetricKeyParameter akp, diff --git a/crypto/src/openssl/PEMUtilities.cs b/crypto/src/openssl/PEMUtilities.cs index 332768083..4ff340b12 100644 --- a/crypto/src/openssl/PEMUtilities.cs +++ b/crypto/src/openssl/PEMUtilities.cs @@ -50,6 +50,84 @@ namespace Org.BouncyCastle.OpenSsl throw new EncryptionException("Unknown DEK algorithm: " + dekAlgName); } +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + internal static byte[] Crypt(bool encrypt, ReadOnlySpan bytes, ReadOnlySpan password, + string dekAlgName, ReadOnlySpan iv) + { + PemBaseAlg baseAlg; + PemMode mode; + ParseDekAlgName(dekAlgName, out baseAlg, out mode); + + string padding; + switch (mode) + { + case PemMode.CBC: + case PemMode.ECB: + padding = "PKCS5Padding"; + break; + case PemMode.CFB: + case PemMode.OFB: + padding = "NoPadding"; + break; + default: + throw new EncryptionException("Unknown DEK algorithm: " + dekAlgName); + } + + string algorithm; + + ReadOnlySpan salt = iv; + switch (baseAlg) + { + case PemBaseAlg.AES_128: + case PemBaseAlg.AES_192: + case PemBaseAlg.AES_256: + algorithm = "AES"; + if (salt.Length > 8) + { + salt = iv[..8].ToArray(); + } + break; + case PemBaseAlg.BF: + algorithm = "BLOWFISH"; + break; + case PemBaseAlg.DES: + algorithm = "DES"; + break; + case PemBaseAlg.DES_EDE: + case PemBaseAlg.DES_EDE3: + algorithm = "DESede"; + break; + case PemBaseAlg.RC2: + case PemBaseAlg.RC2_40: + case PemBaseAlg.RC2_64: + algorithm = "RC2"; + break; + default: + throw new EncryptionException("Unknown DEK algorithm: " + dekAlgName); + } + + string cipherName = algorithm + "/" + mode + "/" + padding; + IBufferedCipher cipher = CipherUtilities.GetCipher(cipherName); + + ICipherParameters cParams = GetCipherParameters(password, baseAlg, salt); + + if (mode != PemMode.ECB) + { + cParams = new ParametersWithIV(cParams, iv); + } + + cipher.Init(encrypt, cParams); + + int outputSize = cipher.GetOutputSize(bytes.Length); + byte[] output = new byte[outputSize]; + int length = cipher.DoFinal(bytes, output); + if (length < outputSize) + { + output = Arrays.CopyOfRange(output, 0, length); + } + return output; + } +#else internal static byte[] Crypt( bool encrypt, byte[] bytes, @@ -124,7 +202,37 @@ namespace Org.BouncyCastle.OpenSsl return cipher.DoFinal(bytes); } +#endif + +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + private static ICipherParameters GetCipherParameters(ReadOnlySpan password, PemBaseAlg baseAlg, + ReadOnlySpan salt) + { + string algorithm; + int keyBits; + switch (baseAlg) + { + case PemBaseAlg.AES_128: keyBits = 128; algorithm = "AES128"; break; + case PemBaseAlg.AES_192: keyBits = 192; algorithm = "AES192"; break; + case PemBaseAlg.AES_256: keyBits = 256; algorithm = "AES256"; break; + case PemBaseAlg.BF: keyBits = 128; algorithm = "BLOWFISH"; break; + case PemBaseAlg.DES: keyBits = 64; algorithm = "DES"; break; + case PemBaseAlg.DES_EDE: keyBits = 128; algorithm = "DESEDE"; break; + case PemBaseAlg.DES_EDE3: keyBits = 192; algorithm = "DESEDE3"; break; + case PemBaseAlg.RC2: keyBits = 128; algorithm = "RC2"; break; + case PemBaseAlg.RC2_40: keyBits = 40; algorithm = "RC2"; break; + case PemBaseAlg.RC2_64: keyBits = 64; algorithm = "RC2"; break; + default: + return null; + } + + OpenSslPbeParametersGenerator pGen = new OpenSslPbeParametersGenerator(); + + pGen.Init(PbeParametersGenerator.Pkcs5PasswordToBytes(password), salt); + return pGen.GenerateDerivedParameters(algorithm, keyBits); + } +#else private static ICipherParameters GetCipherParameters( char[] password, PemBaseAlg baseAlg, @@ -154,5 +262,6 @@ namespace Org.BouncyCastle.OpenSsl return pGen.GenerateDerivedParameters(algorithm, keyBits); } - } +#endif + } } 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 /// 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 + /// + public AsymmetricKeyParameter GetKey(string alias, ReadOnlySpan 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 password, ReadOnlySpan 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 password, ReadOnlySpan salt, + int count) + { + byte[] keyStream = new byte[count]; + Span 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; } @@ -146,6 +228,52 @@ namespace Org.BouncyCastle.Security /// 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 + /// + public void SetKeyEntry(string alias, AsymmetricKeyParameter key, ReadOnlySpan password, + X509Certificate[] chain) { alias = ConvertAlias(alias); @@ -180,6 +308,7 @@ namespace Org.BouncyCastle.Security throw new IOException("unable to encode encrypted private key", e); } } +#endif /// public void SetKeyEntry(string alias, byte[] key, X509Certificate[] chain) @@ -254,12 +383,36 @@ namespace Org.BouncyCastle.Security /// 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 + /// + public void Save(Stream stream, ReadOnlySpan 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(); } + /// WARNING: If is null, no integrity check is performed. /// 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 + /// + public void Load(Stream stream, ReadOnlySpan 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); + /// Load without any integrity check. + /// + 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(System.Math.Min(10, chainLength)); + for (int certNo = 0; certNo != chainLength; certNo++) { - var certs = new List(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 + /// + private ErasableByteStream ValidateStream(Stream inputStream, ReadOnlySpan 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 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 password, ReadOnlySpan 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 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) { diff --git a/crypto/src/util/Strings.cs b/crypto/src/util/Strings.cs index 12eafd21e..29a95a07e 100644 --- a/crypto/src/util/Strings.cs +++ b/crypto/src/util/Strings.cs @@ -46,6 +46,18 @@ namespace Org.BouncyCastle.Utilities return bs; } +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public static byte[] ToByteArray(ReadOnlySpan cs) + { + byte[] bs = new byte[cs.Length]; + for (int i = 0; i < bs.Length; ++i) + { + bs[i] = Convert.ToByte(cs[i]); + } + return bs; + } +#endif + public static string FromAsciiByteArray(byte[] bytes) { return Encoding.ASCII.GetString(bytes); @@ -75,5 +87,15 @@ namespace Org.BouncyCastle.Utilities { return Encoding.UTF8.GetBytes(s); } + +#if NETCOREAPP2_1_OR_GREATER || NETSTANDARD2_1_OR_GREATER + public static byte[] ToUtf8ByteArray(ReadOnlySpan cs) + { + int count = Encoding.UTF8.GetByteCount(cs); + byte[] bytes = new byte[count]; + Encoding.UTF8.GetBytes(cs, bytes); + return bytes; + } +#endif } } -- cgit 1.4.1