crypto service implementation

This commit is contained in:
Kyle Spearrin 2019-04-08 16:04:41 -04:00
parent 3f5115728b
commit f0eca137ef
6 changed files with 630 additions and 7 deletions

View File

@ -21,5 +21,6 @@ namespace Bit.Core.Abstractions
Task<byte[]> RsaExtractPublicKeyAsync(byte[] privateKey);
Task<Tuple<byte[], byte[]>> RsaGenerateKeyPairAsync(int length);
Task<byte[]> RandomBytesAsync(int length);
Task<uint> RandomNumberAsync();
}
}

View File

@ -3,5 +3,6 @@
public static class Constants
{
public static string LockOptionKey = "lockOption";
public static string PinProtectedKey = "pinProtectedKey";
}
}

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Enums
{
public enum KdfType : byte
{
PBKDF2_SHA256 = 0
}
}

View File

@ -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))
{

View File

@ -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<Dictionary<string, SymmetricCryptoKey>> GetOrgKeysAsync()
{
if(_orgKeys != null && _orgKeys.Count > 0)
{
return _orgKeys;
}
var encOrgKeys = await _storageService.GetAsync<Dictionary<string, string>>(Keys_EncOrgKeys);
if(encOrgKeys == null)
{
return null;
}
var orgKeys = new Dictionary<string, SymmetricCryptoKey>();
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<SymmetricCryptoKey> 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<bool> HasKeyAsync()
{
var key = await GetKeyAsync();
return key != null;
}
public async Task<bool> HasEncKeyAsync()
{
var encKey = await _storageService.GetAsync<string>(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<int?>(Constants.LockOptionKey);
if(option != null || option == 0)
{
await ClearKeyAsync();
_key = key;
return;
}
await SetKeyAsync(key);
}
public async Task<SymmetricCryptoKey> 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<SymmetricCryptoKey> MakeKeyFromPinAsync(string pin, string salt,
KdfType kdf, int kdfIterations)
{
var pinProtectedKey = await _storageService.GetAsync<string>(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<Tuple<CipherString, SymmetricCryptoKey>> MakeShareKeyAsync()
{
var shareKey = await _cryptoFunctionService.RandomBytesAsync(64);
var publicKey = await GetPublicKeyAsync();
var encShareKey = await RsaEncryptAsync(shareKey, publicKey);
return new Tuple<CipherString, SymmetricCryptoKey>(encShareKey, new SymmetricCryptoKey(shareKey));
}
public async Task<Tuple<string, CipherString>> 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<string, CipherString>(publicB64, privateEnc);
}
public async Task<SymmetricCryptoKey> MakePinKeyAysnc(string pin, string salt, KdfType kdf, int kdfIterations)
{
var pinKey = await MakeKeyAsync(pin, salt, kdf, kdfIterations);
return await StretchKeyAsync(pinKey);
}
public async Task<string> 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<Tuple<SymmetricCryptoKey, CipherString>> MakeEncKeyAsync(SymmetricCryptoKey key)
{
var theKey = await GetKeyForEncryptionAsync(key);
var encKey = await _cryptoFunctionService.RandomBytesAsync(64);
return await BuildEncKeyAsync(theKey, encKey);
}
public async Task<Tuple<SymmetricCryptoKey, CipherString>> RemakeEncKeyAsync(SymmetricCryptoKey key)
{
var encKey = await GetEncKeyAsync();
return await BuildEncKeyAsync(key, encKey.Key);
}
public async Task<CipherString> EncryptAsync(string plainValue, SymmetricCryptoKey key = null)
{
if(plainValue == null)
{
return null;
}
return await EncryptAsync(Encoding.UTF8.GetBytes(plainValue), key);
}
public async Task<CipherString> 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<byte[]> 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<CipherString> 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<byte[]> 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<string> DecryptToUtf8Async(CipherString cipherString, SymmetricCryptoKey key = null)
{
return await AesDecryptToUtf8Async(cipherString.EncryptionType, cipherString.Data,
cipherString.Iv, cipherString.Mac, key);
}
public async Task<byte[]> 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<byte>(encBytes, 1, 16).ToArray();
macBytes = new ArraySegment<byte>(encBytes, 17, 32).ToArray();
ctBytes = new ArraySegment<byte>(encBytes, 49, encBytes.Length - 49).ToArray();
break;
case EncryptionType.AesCbc256_B64:
if(encBytes.Length < 17) // 1 + 16 + ctLength
{
return null;
}
ivBytes = new ArraySegment<byte>(encBytes, 1, 16).ToArray();
ctBytes = new ArraySegment<byte>(encBytes, 17, encBytes.Length - 17).ToArray();
break;
default:
return null;
}
return await AesDecryptToBytesAsync(encType, ctBytes, ivBytes, macBytes, key);
}
public async Task<int> 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<EncryptedObject> 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<string> 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<byte[]> 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<byte[]> 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<SymmetricCryptoKey> 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<SymmetricCryptoKey> 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<byte[]> 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<string> 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<string>();
// 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<Tuple<SymmetricCryptoKey, CipherString>> 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<SymmetricCryptoKey, CipherString>(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; }
}
}
}

View File

@ -142,6 +142,11 @@ namespace Bit.Core.Services
return Task.FromResult(CryptographicBuffer.GenerateRandom(length));
}
public Task<uint> RandomNumberAsync()
{
return Task.FromResult(CryptographicBuffer.GenerateRandomNumber());
}
private HashAlgorithm ToHashAlgorithm(CryptoHashAlgorithm algorithm)
{
switch(algorithm)