From f0eca137effeb00f90849fca18191b1440e78392 Mon Sep 17 00:00:00 2001 From: Kyle Spearrin Date: Mon, 8 Apr 2019 16:04:41 -0400 Subject: [PATCH] crypto service implementation --- .../Abstractions/ICryptoFunctionService.cs | 1 + src/Core/Constants.cs | 1 + src/Core/Enums/KdfType.cs | 7 + src/Core/Models/Domain/CipherString.cs | 2 +- src/Core/Services/CryptoService.cs | 621 +++++++++++++++++- src/Core/Services/PclCryptoFunctionService.cs | 5 + 6 files changed, 630 insertions(+), 7 deletions(-) create mode 100644 src/Core/Enums/KdfType.cs diff --git a/src/Core/Abstractions/ICryptoFunctionService.cs b/src/Core/Abstractions/ICryptoFunctionService.cs index 157c9efd7..f0d7cf0ab 100644 --- a/src/Core/Abstractions/ICryptoFunctionService.cs +++ b/src/Core/Abstractions/ICryptoFunctionService.cs @@ -21,5 +21,6 @@ namespace Bit.Core.Abstractions Task RsaExtractPublicKeyAsync(byte[] privateKey); Task> RsaGenerateKeyPairAsync(int length); Task RandomBytesAsync(int length); + Task RandomNumberAsync(); } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 4971dbfa3..eca671520 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -3,5 +3,6 @@ public static class Constants { public static string LockOptionKey = "lockOption"; + public static string PinProtectedKey = "pinProtectedKey"; } } diff --git a/src/Core/Enums/KdfType.cs b/src/Core/Enums/KdfType.cs new file mode 100644 index 000000000..1c845846a --- /dev/null +++ b/src/Core/Enums/KdfType.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Enums +{ + public enum KdfType : byte + { + PBKDF2_SHA256 = 0 + } +} diff --git a/src/Core/Models/Domain/CipherString.cs b/src/Core/Models/Domain/CipherString.cs index b76e972e3..5f2b65ebc 100644 --- a/src/Core/Models/Domain/CipherString.cs +++ b/src/Core/Models/Domain/CipherString.cs @@ -7,7 +7,7 @@ namespace Bit.Core.Models.Domain { private string _decryptedValue; - public CipherString(EncryptionType encryptionType, string data, string iv, string mac) + public CipherString(EncryptionType encryptionType, string data, string iv = null, string mac = null) { if(string.IsNullOrWhiteSpace(data)) { diff --git a/src/Core/Services/CryptoService.cs b/src/Core/Services/CryptoService.cs index 3adfc7b27..9b8a8f2af 100644 --- a/src/Core/Services/CryptoService.cs +++ b/src/Core/Services/CryptoService.cs @@ -5,6 +5,7 @@ using Bit.Core.Models.Response; using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Threading.Tasks; namespace Bit.Core.Services @@ -134,11 +135,12 @@ namespace Bit.Core.Services var encKeyCipher = new CipherString(encKey); if(encKeyCipher.EncryptionType == EncryptionType.AesCbc256_B64) { - // TODO + decEncKey = await DecryptToBytesAsync(encKeyCipher, key); } else if(encKeyCipher.EncryptionType == EncryptionType.AesCbc256_HmacSha256_B64) { - // TODO + var newKey = await StretchKeyAsync(key); + decEncKey = await DecryptToBytesAsync(encKeyCipher, newKey); } else { @@ -179,7 +181,7 @@ namespace Bit.Core.Services { return null; } - // TODO + _privateKey = await DecryptToBytesAsync(new CipherString(encPrivateKey), null); return _privateKey; } @@ -194,9 +196,616 @@ namespace Bit.Core.Services throw new Exception("No public key available."); } var keyFingerprint = await _cryptoFunctionService.HashAsync(publicKey, CryptoHashAlgorithm.Sha256); - // TODO - return null; + var userFingerprint = await HkdfExpandAsync(keyFingerprint, Encoding.UTF8.GetBytes(userId), 32); + return HashPhrase(userFingerprint); + } + + public async Task> GetOrgKeysAsync() + { + if(_orgKeys != null && _orgKeys.Count > 0) + { + return _orgKeys; + } + var encOrgKeys = await _storageService.GetAsync>(Keys_EncOrgKeys); + if(encOrgKeys == null) + { + return null; + } + var orgKeys = new Dictionary(); + var setKey = false; + foreach(var org in encOrgKeys) + { + var decValue = await RsaDecryptAsync(org.Value); + orgKeys.Add(org.Key, new SymmetricCryptoKey(decValue)); + setKey = true; + } + + if(setKey) + { + _orgKeys = orgKeys; + } + return _orgKeys; + } + + public async Task GetOrgKeyAsync(string orgId) + { + if(string.IsNullOrWhiteSpace(orgId)) + { + return null; + } + var orgKeys = await GetOrgKeysAsync(); + if(orgKeys == null || !orgKeys.ContainsKey(orgId)) + { + return null; + } + return orgKeys[orgId]; + } + + public async Task HasKeyAsync() + { + var key = await GetKeyAsync(); + return key != null; + } + + public async Task HasEncKeyAsync() + { + var encKey = await _storageService.GetAsync(Keys_EncKey); + return encKey != null; + } + + public async Task ClearKeyAsync() + { + _key = _legacyEtmKey = null; + await _secureStorageService.RemoveAsync(Keys_Key); + } + + public async Task ClearKeyHashAsync() + { + _keyHash = null; + await _storageService.RemoveAsync(Keys_KeyHash); + } + + public async Task ClearEncKeyAsync(bool memoryOnly = false) + { + _encKey = null; + if(!memoryOnly) + { + await _storageService.RemoveAsync(Keys_EncKey); + } + } + + public async Task ClearKeyPairAsync(bool memoryOnly = false) + { + _publicKey = _privateKey = null; + if(!memoryOnly) + { + await _storageService.RemoveAsync(Keys_EncPrivateKey); + } + } + + public async Task ClearOrgKeysAsync(bool memoryOnly = false) + { + _orgKeys = null; + if(!memoryOnly) + { + await _storageService.RemoveAsync(Keys_EncOrgKeys); + } + } + + public async Task ClearPinProtectedKeyAsync() + { + await _storageService.RemoveAsync(Constants.PinProtectedKey); + } + + public async Task ClearKeysAsync() + { + await Task.WhenAll(new Task[] + { + ClearKeyAsync(), + ClearKeyHashAsync(), + ClearOrgKeysAsync(), + ClearEncKeyAsync(), + ClearKeyPairAsync(), + ClearPinProtectedKeyAsync() + }); + } + + public async Task ToggleKeyAsync() + { + var key = await GetKeyAsync(); + var option = await _storageService.GetAsync(Constants.LockOptionKey); + if(option != null || option == 0) + { + await ClearKeyAsync(); + _key = key; + return; + } + await SetKeyAsync(key); + } + + public async Task MakeKeyAsync(string password, string salt, + KdfType? kdf, int? kdfIterations) + { + byte[] key = null; + if(kdf == null || kdf == KdfType.PBKDF2_SHA256) + { + if(kdfIterations == null) + { + kdfIterations = 5000; + } + if(kdfIterations < 5000) + { + throw new Exception("PBKDF2 iteration minimum is 5000."); + } + key = await _cryptoFunctionService.Pbkdf2Async(password, salt, + CryptoHashAlgorithm.Sha256, kdfIterations.Value); + } + else + { + throw new Exception("Unknown kdf."); + } + return new SymmetricCryptoKey(key); + } + + public async Task MakeKeyFromPinAsync(string pin, string salt, + KdfType kdf, int kdfIterations) + { + var pinProtectedKey = await _storageService.GetAsync(Constants.PinProtectedKey); + if(pinProtectedKey == null) + { + throw new Exception("No PIN protected key found."); + } + var protectedKeyCs = new CipherString(pinProtectedKey); + var pinKey = await MakePinKeyAysnc(pin, salt, kdf, kdfIterations); + var decKey = await DecryptToBytesAsync(protectedKeyCs, pinKey); + return new SymmetricCryptoKey(decKey); + } + + public async Task> MakeShareKeyAsync() + { + var shareKey = await _cryptoFunctionService.RandomBytesAsync(64); + var publicKey = await GetPublicKeyAsync(); + var encShareKey = await RsaEncryptAsync(shareKey, publicKey); + return new Tuple(encShareKey, new SymmetricCryptoKey(shareKey)); + } + + public async Task> MakeKeyPairAsync(SymmetricCryptoKey key = null) + { + var keyPair = await _cryptoFunctionService.RsaGenerateKeyPairAsync(2048); + var publicB64 = Convert.ToBase64String(keyPair.Item1); + var privateEnc = await EncryptAsync(keyPair.Item2, key); + return new Tuple(publicB64, privateEnc); + } + + public async Task MakePinKeyAysnc(string pin, string salt, KdfType kdf, int kdfIterations) + { + var pinKey = await MakeKeyAsync(pin, salt, kdf, kdfIterations); + return await StretchKeyAsync(pinKey); + } + + public async Task HashPasswordAsync(string password, SymmetricCryptoKey key) + { + if(key == null) + { + key = await GetKeyAsync(); + } + if(password == null || key == null) + { + throw new Exception("Invalid parameters."); + } + var hash = await _cryptoFunctionService.Pbkdf2Async(key.Key, password, CryptoHashAlgorithm.Sha256, 1); + return Convert.ToBase64String(hash); + } + + public async Task> MakeEncKeyAsync(SymmetricCryptoKey key) + { + var theKey = await GetKeyForEncryptionAsync(key); + var encKey = await _cryptoFunctionService.RandomBytesAsync(64); + return await BuildEncKeyAsync(theKey, encKey); + } + + public async Task> RemakeEncKeyAsync(SymmetricCryptoKey key) + { + var encKey = await GetEncKeyAsync(); + return await BuildEncKeyAsync(key, encKey.Key); + } + + public async Task EncryptAsync(string plainValue, SymmetricCryptoKey key = null) + { + if(plainValue == null) + { + return null; + } + return await EncryptAsync(Encoding.UTF8.GetBytes(plainValue), key); + } + + public async Task EncryptAsync(byte[] plainValue, SymmetricCryptoKey key = null) + { + if(plainValue == null) + { + return null; + } + var encObj = await AesEncryptAsync(plainValue, key); + var iv = Convert.ToBase64String(encObj.Iv); + var data = Convert.ToBase64String(encObj.Data); + var mac = encObj.Mac != null ? Convert.ToBase64String(encObj.Mac) : null; + return new CipherString(encObj.Key.EncType, data, iv, mac); + } + + public async Task EncryptToBytesAsync(byte[] plainValue, SymmetricCryptoKey key = null) + { + var encValue = await AesEncryptAsync(plainValue, key); + var macLen = 0; + if(encValue.Mac != null) + { + macLen = encValue.Mac.Length; + } + var encBytes = new byte[1 + encValue.Iv.Length + macLen + encValue.Data.Length]; + Buffer.BlockCopy(new byte[] { (byte)encValue.Key.EncType }, 0, encBytes, 0, 1); + Buffer.BlockCopy(encValue.Iv, 0, encBytes, 1, encValue.Iv.Length); + if(encValue.Mac != null) + { + Buffer.BlockCopy(encValue.Mac, 0, encBytes, 1 + encValue.Iv.Length, encValue.Mac.Length); + } + Buffer.BlockCopy(encValue.Data, 0, encBytes, 1 + encValue.Iv.Length + macLen, encValue.Data.Length); + return encBytes; + } + + public async Task RsaEncryptAsync(byte[] data, byte[] publicKey = null) + { + if(publicKey == null) + { + publicKey = await GetPublicKeyAsync(); + } + if(publicKey == null) + { + throw new Exception("Public key unavailable."); + } + var encBytes = await _cryptoFunctionService.RsaEncryptAsync(data, publicKey, CryptoHashAlgorithm.Sha1); + return new CipherString(EncryptionType.Rsa2048_OaepSha1_B64, Convert.ToBase64String(encBytes)); + } + + public async Task DecryptToBytesAsync(CipherString cipherString, SymmetricCryptoKey key = null) + { + var iv = Convert.FromBase64String(cipherString.Iv); + var data = Convert.FromBase64String(cipherString.Data); + var mac = !string.IsNullOrWhiteSpace(cipherString.Mac) ? Convert.FromBase64String(cipherString.Mac) : null; + return await AesDecryptToBytesAsync(cipherString.EncryptionType, data, iv, mac, key); + } + + public async Task DecryptToUtf8Async(CipherString cipherString, SymmetricCryptoKey key = null) + { + return await AesDecryptToUtf8Async(cipherString.EncryptionType, cipherString.Data, + cipherString.Iv, cipherString.Mac, key); + } + + public async Task DecryptFromBytesAsync(byte[] encBytes, SymmetricCryptoKey key) + { + if(encBytes == null) + { + throw new Exception("no encBytes."); + } + + var encType = (EncryptionType)encBytes[0]; + byte[] ctBytes = null; + byte[] ivBytes = null; + byte[] macBytes = null; + + switch(encType) + { + case EncryptionType.AesCbc128_HmacSha256_B64: + case EncryptionType.AesCbc256_HmacSha256_B64: + if(encBytes.Length < 49) // 1 + 16 + 32 + ctLength + { + return null; + } + ivBytes = new ArraySegment(encBytes, 1, 16).ToArray(); + macBytes = new ArraySegment(encBytes, 17, 32).ToArray(); + ctBytes = new ArraySegment(encBytes, 49, encBytes.Length - 49).ToArray(); + break; + case EncryptionType.AesCbc256_B64: + if(encBytes.Length < 17) // 1 + 16 + ctLength + { + return null; + } + ivBytes = new ArraySegment(encBytes, 1, 16).ToArray(); + ctBytes = new ArraySegment(encBytes, 17, encBytes.Length - 17).ToArray(); + break; + default: + return null; + } + + return await AesDecryptToBytesAsync(encType, ctBytes, ivBytes, macBytes, key); + } + + public async Task RandomNumberAsync(int min, int max) + { + // Make max inclusive + max = max + 1; + + var diff = (long)max - min; + var upperBound = uint.MaxValue / diff * diff; + uint ui; + do + { + ui = await _cryptoFunctionService.RandomNumberAsync(); + } while(ui >= upperBound); + return (int)(min + (ui % diff)); + } + + // Helpers + + private async Task AesEncryptAsync(byte[] data, SymmetricCryptoKey key) + { + var obj = new EncryptedObject + { + Key = await GetKeyForEncryptionAsync(key), + Iv = await _cryptoFunctionService.RandomBytesAsync(16) + }; + obj.Data = await _cryptoFunctionService.AesEncryptAsync(data, obj.Iv, obj.Key.EncKey); + if(obj.Key.MacKey != null) + { + var macData = new byte[obj.Iv.Length + obj.Data.Length]; + Buffer.BlockCopy(obj.Iv, 0, macData, 0, obj.Iv.Length); + Buffer.BlockCopy(obj.Data, 0, macData, obj.Iv.Length, obj.Data.Length); + obj.Mac = await _cryptoFunctionService.HmacAsync(macData, obj.Key.MacKey, CryptoHashAlgorithm.Sha256); + } + return obj; + } + + private async Task AesDecryptToUtf8Async(EncryptionType encType, string data, string iv, string mac, + SymmetricCryptoKey key) + { + var keyForEnc = await GetKeyForEncryptionAsync(key); + var theKey = ResolveLegacyKey(encType, keyForEnc); + if(theKey.MacKey != null && mac == null) + { + // Mac required. + return null; + } + if(theKey.EncType != encType) + { + // encType unavailable. + return null; + } + + // "Fast params" conversion + var encKey = theKey.EncKey; + var dataBytes = Convert.FromBase64String(data); + var ivBytes = Convert.FromBase64String(iv); + + var macDataBytes = new byte[ivBytes.Length + dataBytes.Length]; + Buffer.BlockCopy(ivBytes, 0, macDataBytes, 0, ivBytes.Length); + Buffer.BlockCopy(dataBytes, 0, macDataBytes, ivBytes.Length, dataBytes.Length); + + byte[] macKey = null; + if(key.MacKey != null) + { + macKey = key.MacKey; + } + byte[] macBytes = null; + if(mac != null) + { + macBytes = Convert.FromBase64String(mac); + } + + // Compute mac + if(macKey != null && macBytes != null) + { + var computedMac = await _cryptoFunctionService.HmacAsync(macDataBytes, macKey, + CryptoHashAlgorithm.Sha256); + var macsEqual = await _cryptoFunctionService.CompareAsync(macBytes, computedMac); + if(!macsEqual) + { + // Mac failed + return null; + } + } + + var decBytes = await _cryptoFunctionService.AesDecryptAsync(dataBytes, ivBytes, encKey); + return Encoding.UTF8.GetString(decBytes); + } + + private async Task AesDecryptToBytesAsync(EncryptionType encType, byte[] data, byte[] iv, byte[] mac, + SymmetricCryptoKey key) + { + + var keyForEnc = await GetKeyForEncryptionAsync(key); + var theKey = ResolveLegacyKey(encType, keyForEnc); + if(theKey.MacKey != null && mac == null) + { + // Mac required. + return null; + } + if(theKey.EncType != encType) + { + // encType unavailable. + return null; + } + + // Compute mac + if(theKey.MacKey != null && mac != null) + { + var macData = new byte[iv.Length + data.Length]; + Buffer.BlockCopy(iv, 0, macData, 0, iv.Length); + Buffer.BlockCopy(data, 0, macData, iv.Length, data.Length); + + var computedMac = await _cryptoFunctionService.HmacAsync(macData, theKey.MacKey, + CryptoHashAlgorithm.Sha256); + if(computedMac == null) + { + return null; + } + var macsMatch = await _cryptoFunctionService.CompareAsync(mac, computedMac); + if(!macsMatch) + { + // Mac failed + return null; + } + } + + return await _cryptoFunctionService.AesDecryptAsync(data, iv, theKey.EncKey); + } + + private async Task RsaDecryptAsync(string encValue) + { + var headerPieces = encValue.Split('.'); + EncryptionType? encType = null; + string[] encPieces = null; + + if(headerPieces.Length == 1) + { + encType = EncryptionType.Rsa2048_OaepSha256_B64; + encPieces = new string[] { headerPieces[0] }; + } + else if(headerPieces.Length == 2 && Enum.TryParse(headerPieces[0], out EncryptionType type)) + { + encType = type; + encPieces = headerPieces[1].Split('|'); + } + + if(!encType.HasValue) + { + throw new Exception("encType unavailable."); + } + if(encPieces == null || encPieces.Length == 0) + { + throw new Exception("encPieces unavailable."); + } + + var data = Convert.FromBase64String(encPieces[0]); + var privateKey = await GetPrivateKeyAsync(); + if(privateKey == null) + { + throw new Exception("No private key."); + } + + var alg = CryptoHashAlgorithm.Sha1; + switch(encType.Value) + { + case EncryptionType.Rsa2048_OaepSha256_B64: + case EncryptionType.Rsa2048_OaepSha256_HmacSha256_B64: + alg = CryptoHashAlgorithm.Sha256; + break; + case EncryptionType.Rsa2048_OaepSha1_B64: + case EncryptionType.Rsa2048_OaepSha1_HmacSha256_B64: + break; + default: + throw new Exception("encType unavailable."); + } + + return await _cryptoFunctionService.RsaDecryptAsync(data, privateKey, alg); + } + + private async Task GetKeyForEncryptionAsync(SymmetricCryptoKey key = null) + { + if(key != null) + { + return key; + } + var encKey = await GetEncKeyAsync(); + if(encKey != null) + { + return encKey; + } + return await GetKeyAsync(); + } + + private SymmetricCryptoKey ResolveLegacyKey(EncryptionType encKey, SymmetricCryptoKey key) + { + if(encKey == EncryptionType.AesCbc128_HmacSha256_B64 && key.EncType == EncryptionType.AesCbc256_B64) + { + // Old encrypt-then-mac scheme, make a new key + if(_legacyEtmKey == null) + { + _legacyEtmKey = new SymmetricCryptoKey(key.Key, EncryptionType.AesCbc128_HmacSha256_B64); + } + return _legacyEtmKey; + } + return key; + } + + private async Task StretchKeyAsync(SymmetricCryptoKey key) + { + var newKey = new byte[64]; + var enc = await HkdfExpandAsync(key.Key, Encoding.UTF8.GetBytes("enc"), 32); + Buffer.BlockCopy(enc, 0, newKey, 0, 32); + var mac = await HkdfExpandAsync(key.Key, Encoding.UTF8.GetBytes("mac"), 32); + Buffer.BlockCopy(mac, 0, newKey, 32, 32); + return new SymmetricCryptoKey(newKey); + } + + // ref: https://tools.ietf.org/html/rfc5869 + private async Task HkdfExpandAsync(byte[] prk, byte[] info, int size) + { + var hashLen = 32; // sha256 + var okm = new byte[size]; + var previousT = new byte[0]; + var n = (int)Math.Ceiling((double)size / hashLen); + for(int 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 _cryptoFunctionService.HmacAsync(t, prk, CryptoHashAlgorithm.Sha256); + previousT.CopyTo(okm, i * hashLen); + } + return okm; + } + + private List HashPhrase(byte[] hash, int minimumEntropy = 64) + { + // TODO: word list + var EEFLongWordList = new string[] { }; + + var entropyPerWord = Math.Log(EEFLongWordList.Length) / Math.Log(2); + var numWords = (int)Math.Ceiling(minimumEntropy / entropyPerWord); + + var entropyAvailable = hash.Length * 4; + if(numWords * entropyPerWord > entropyAvailable) + { + throw new Exception("Output entropy of hash function is too small"); + } + + var phrase = new List(); + // TODO: big int from array + var hashNumber = 123; // bigInt.fromArray(hash, 256); + while(numWords-- > 0) + { + // var remainder = hashNumber.mod(EEFLongWordList.Length); + // hashNumber = hashNumber.divide(EEFLongWordList.Length); + // phrase.Add(EEFLongWordList[remainder]); + } + return phrase; + } + + private async Task> BuildEncKeyAsync(SymmetricCryptoKey key, + byte[] encKey) + { + CipherString encKeyEnc = null; + if(key.Key.Length == 32) + { + var newKey = await StretchKeyAsync(key); + encKeyEnc = await EncryptAsync(encKey, newKey); + } + else if(key.Key.Length == 64) + { + encKeyEnc = await EncryptAsync(encKey, key); + } + else + { + throw new Exception("Invalid key size."); + } + return new Tuple(new SymmetricCryptoKey(encKey), encKeyEnc); + } + + private class EncryptedObject + { + public byte[] Iv { get; set; } + public byte[] Data { get; set; } + public byte[] Mac { get; set; } + public SymmetricCryptoKey Key { get; set; } } - } } diff --git a/src/Core/Services/PclCryptoFunctionService.cs b/src/Core/Services/PclCryptoFunctionService.cs index c9ebd0086..19e3470b1 100644 --- a/src/Core/Services/PclCryptoFunctionService.cs +++ b/src/Core/Services/PclCryptoFunctionService.cs @@ -142,6 +142,11 @@ namespace Bit.Core.Services return Task.FromResult(CryptographicBuffer.GenerateRandom(length)); } + public Task RandomNumberAsync() + { + return Task.FromResult(CryptographicBuffer.GenerateRandomNumber()); + } + private HashAlgorithm ToHashAlgorithm(CryptoHashAlgorithm algorithm) { switch(algorithm)