using System; using System.Linq; using System.Text; using System.Threading.Tasks; using Bit.Core.Abstractions; using Bit.Core.Enums; using PCLCrypto; using static PCLCrypto.WinRTCrypto; namespace Bit.Core.Services { public class PclCryptoFunctionService : ICryptoFunctionService { private readonly ICryptoPrimitiveService _cryptoPrimitiveService; public PclCryptoFunctionService(ICryptoPrimitiveService cryptoPrimitiveService) { _cryptoPrimitiveService = cryptoPrimitiveService; } public Task Pbkdf2Async(string password, string salt, CryptoHashAlgorithm algorithm, int iterations) { password = NormalizePassword(password); return Pbkdf2Async(Encoding.UTF8.GetBytes(password), Encoding.UTF8.GetBytes(salt), algorithm, iterations); } public Task Pbkdf2Async(byte[] password, string salt, CryptoHashAlgorithm algorithm, int iterations) { return Pbkdf2Async(password, Encoding.UTF8.GetBytes(salt), algorithm, iterations); } public Task Pbkdf2Async(string password, byte[] salt, CryptoHashAlgorithm algorithm, int iterations) { password = NormalizePassword(password); return Pbkdf2Async(Encoding.UTF8.GetBytes(password), salt, algorithm, iterations); } public Task Pbkdf2Async(byte[] password, byte[] salt, CryptoHashAlgorithm algorithm, int iterations) { if (algorithm != CryptoHashAlgorithm.Sha256 && algorithm != CryptoHashAlgorithm.Sha512) { throw new ArgumentException("Unsupported PBKDF2 algorithm."); } return Task.FromResult(_cryptoPrimitiveService.Pbkdf2(password, salt, algorithm, iterations)); } public Task Argon2Async(string password, string salt, int iterations, int memory, int parallelism) { password = NormalizePassword(password); return Argon2Async(Encoding.UTF8.GetBytes(password), Encoding.UTF8.GetBytes(salt), iterations, memory, parallelism); } public Task Argon2Async(byte[] password, string salt, int iterations, int memory, int parallelism) { return Argon2Async(password, Encoding.UTF8.GetBytes(salt), iterations, memory, parallelism); } public Task Argon2Async(string password, byte[] salt, int iterations, int memory, int parallelism) { password = NormalizePassword(password); return Argon2Async(Encoding.UTF8.GetBytes(password), salt, iterations, memory, parallelism); } public Task Argon2Async(byte[] password, byte[] salt, int iterations, int memory, int parallelism) { return Task.FromResult(_cryptoPrimitiveService.Argon2id(password, salt, iterations, memory, parallelism)); } public async Task HkdfAsync(byte[] ikm, string salt, string info, int outputByteSize, HkdfAlgorithm algorithm) => await HkdfAsync(ikm, Encoding.UTF8.GetBytes(salt), Encoding.UTF8.GetBytes(info), outputByteSize, algorithm); public async Task HkdfAsync(byte[] ikm, byte[] salt, string info, int outputByteSize, HkdfAlgorithm algorithm) => await HkdfAsync(ikm, salt, Encoding.UTF8.GetBytes(info), outputByteSize, algorithm); public async Task HkdfAsync(byte[] ikm, string salt, byte[] info, int outputByteSize, HkdfAlgorithm algorithm) => await HkdfAsync(ikm, Encoding.UTF8.GetBytes(salt), info, outputByteSize, algorithm); public async Task HkdfAsync(byte[] ikm, byte[] salt, byte[] info, int outputByteSize, HkdfAlgorithm algorithm) { var prk = await HmacAsync(ikm, salt, HkdfAlgorithmToCryptoHashAlgorithm(algorithm)); return await HkdfExpandAsync(prk, info, outputByteSize, algorithm); } public async Task HkdfExpandAsync(byte[] prk, string info, int outputByteSize, HkdfAlgorithm algorithm) => await HkdfExpandAsync(prk, Encoding.UTF8.GetBytes(info), outputByteSize, algorithm); // ref: https://tools.ietf.org/html/rfc5869 public async Task HkdfExpandAsync(byte[] prk, byte[] info, int outputByteSize, HkdfAlgorithm algorithm) { var hashLen = algorithm == HkdfAlgorithm.Sha256 ? 32 : 64; var maxOutputByteSize = 255 * hashLen; if (outputByteSize > maxOutputByteSize) { throw new ArgumentException($"{nameof(outputByteSize)} is too large. Max is {maxOutputByteSize}, received {outputByteSize}"); } if (prk.Length < hashLen) { throw new ArgumentException($"{nameof(prk)} length is too small. Must be at least {hashLen} for {algorithm}"); } var cryptoHashAlgorithm = HkdfAlgorithmToCryptoHashAlgorithm(algorithm); var previousT = new byte[0]; var runningOkmLength = 0; var n = (int)Math.Ceiling((double)outputByteSize / hashLen); var okm = new byte[n * hashLen]; for (var i = 0; i < n; i++) { var t = new byte[previousT.Length + info.Length + 1]; previousT.CopyTo(t, 0); info.CopyTo(t, previousT.Length); t[t.Length - 1] = (byte)(i + 1); previousT = await HmacAsync(t, prk, cryptoHashAlgorithm); previousT.CopyTo(okm, runningOkmLength); runningOkmLength = previousT.Length; if (runningOkmLength >= outputByteSize) { break; } } return okm.Take(outputByteSize).ToArray(); } public Task HashAsync(string value, CryptoHashAlgorithm algorithm) { return HashAsync(Encoding.UTF8.GetBytes(value), algorithm); } public Task HashAsync(byte[] value, CryptoHashAlgorithm algorithm) { var provider = HashAlgorithmProvider.OpenAlgorithm(ToHashAlgorithm(algorithm)); return Task.FromResult(provider.HashData(value)); } public Task HmacAsync(byte[] value, byte[] key, CryptoHashAlgorithm algorithm) { var provider = MacAlgorithmProvider.OpenAlgorithm(ToMacAlgorithm(algorithm)); var hasher = provider.CreateHash(key); hasher.Append(value); return Task.FromResult(hasher.GetValueAndReset()); } public async Task CompareAsync(byte[] a, byte[] b) { var provider = MacAlgorithmProvider.OpenAlgorithm(MacAlgorithm.HmacSha256); var hasher = provider.CreateHash(await RandomBytesAsync(32)); hasher.Append(a); var mac1 = hasher.GetValueAndReset(); hasher.Append(b); var mac2 = hasher.GetValueAndReset(); if (mac1.Length != mac2.Length) { return false; } for (int i = 0; i < mac2.Length; i++) { if (mac1[i] != mac2[i]) { return false; } } return true; } public Task AesEncryptAsync(byte[] data, byte[] iv, byte[] key) { var provider = SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7); var cryptoKey = provider.CreateSymmetricKey(key); return Task.FromResult(CryptographicEngine.Encrypt(cryptoKey, data, iv)); } public Task AesDecryptAsync(byte[] data, byte[] iv, byte[] key) { var provider = SymmetricKeyAlgorithmProvider.OpenAlgorithm(SymmetricAlgorithm.AesCbcPkcs7); var cryptoKey = provider.CreateSymmetricKey(key); return Task.FromResult(CryptographicEngine.Decrypt(cryptoKey, data, iv)); } public Task RsaEncryptAsync(byte[] data, byte[] publicKey, CryptoHashAlgorithm algorithm) { var provider = AsymmetricKeyAlgorithmProvider.OpenAlgorithm(ToAsymmetricAlgorithm(algorithm)); var cryptoKey = provider.ImportPublicKey(publicKey, CryptographicPublicKeyBlobType.X509SubjectPublicKeyInfo); return Task.FromResult(CryptographicEngine.Encrypt(cryptoKey, data)); } public Task RsaDecryptAsync(byte[] data, byte[] privateKey, CryptoHashAlgorithm algorithm) { var provider = AsymmetricKeyAlgorithmProvider.OpenAlgorithm(ToAsymmetricAlgorithm(algorithm)); var cryptoKey = provider.ImportKeyPair(privateKey, CryptographicPrivateKeyBlobType.Pkcs8RawPrivateKeyInfo); return Task.FromResult(CryptographicEngine.Decrypt(cryptoKey, data)); } public Task RsaExtractPublicKeyAsync(byte[] privateKey) { // Have to specify some algorithm var provider = AsymmetricKeyAlgorithmProvider.OpenAlgorithm(AsymmetricAlgorithm.RsaOaepSha1); var cryptoKey = provider.ImportKeyPair(privateKey, CryptographicPrivateKeyBlobType.Pkcs8RawPrivateKeyInfo); return Task.FromResult(cryptoKey.ExportPublicKey(CryptographicPublicKeyBlobType.X509SubjectPublicKeyInfo)); } public Task> RsaGenerateKeyPairAsync(int length) { if (length != 1024 && length != 2048 && length != 4096) { throw new ArgumentException("Invalid key pair length."); } // Have to specify some algorithm var provider = AsymmetricKeyAlgorithmProvider.OpenAlgorithm(AsymmetricAlgorithm.RsaOaepSha1); var cryptoKey = provider.CreateKeyPair(length); var publicKey = cryptoKey.ExportPublicKey(CryptographicPublicKeyBlobType.X509SubjectPublicKeyInfo); var privateKey = cryptoKey.Export(CryptographicPrivateKeyBlobType.Pkcs8RawPrivateKeyInfo); return Task.FromResult(new Tuple(publicKey, privateKey)); } public Task RandomBytesAsync(int length) { return Task.FromResult(CryptographicBuffer.GenerateRandom(length)); } public byte[] RandomBytes(int length) { return CryptographicBuffer.GenerateRandom(length); } public Task RandomNumberAsync() { return Task.FromResult(CryptographicBuffer.GenerateRandomNumber()); } public uint RandomNumber() { return CryptographicBuffer.GenerateRandomNumber(); } private HashAlgorithm ToHashAlgorithm(CryptoHashAlgorithm algorithm) { switch (algorithm) { case CryptoHashAlgorithm.Sha1: return HashAlgorithm.Sha1; case CryptoHashAlgorithm.Sha256: return HashAlgorithm.Sha256; case CryptoHashAlgorithm.Sha512: return HashAlgorithm.Sha512; case CryptoHashAlgorithm.Md5: return HashAlgorithm.Md5; default: throw new ArgumentException("Unsupported hash algorithm."); } } private MacAlgorithm ToMacAlgorithm(CryptoHashAlgorithm algorithm) { switch (algorithm) { case CryptoHashAlgorithm.Sha1: return MacAlgorithm.HmacSha1; case CryptoHashAlgorithm.Sha256: return MacAlgorithm.HmacSha256; case CryptoHashAlgorithm.Sha512: return MacAlgorithm.HmacSha512; default: throw new ArgumentException("Unsupported mac algorithm."); } } private AsymmetricAlgorithm ToAsymmetricAlgorithm(CryptoHashAlgorithm algorithm) { switch (algorithm) { case CryptoHashAlgorithm.Sha1: return AsymmetricAlgorithm.RsaOaepSha1; // RsaOaepSha256 is not supported on iOS // ref: https://github.com/AArnott/PCLCrypto/issues/124 // case CryptoHashAlgorithm.SHA256: // return AsymmetricAlgorithm.RsaOaepSha256; default: throw new ArgumentException("Unsupported asymmetric algorithm."); } } // Some users like to copy/paste passwords from external files. Sometimes this can lead to two different // values on mobiles apps vs the web. For example, on Android an EditText will accept a new line character // (\n), whereas whenever you paste a new line character on the web in a HTML input box it is converted // to a space ( ). Normalize those values so that they are the same on all platforms. private string NormalizePassword(string password) { return password .Replace("\r\n", " ") // Windows-style new line => space .Replace("\n", " ") // New line => space .Replace(" ", " "); // No-break space (00A0) => space } private CryptoHashAlgorithm HkdfAlgorithmToCryptoHashAlgorithm(HkdfAlgorithm hkdfAlgorithm) { switch (hkdfAlgorithm) { case HkdfAlgorithm.Sha256: return CryptoHashAlgorithm.Sha256; case HkdfAlgorithm.Sha512: return CryptoHashAlgorithm.Sha512; default: throw new ArgumentException($"Invalid hkdf algorithm type, {hkdfAlgorithm}"); } } } }