Bitwarden-app-android-iphon.../src/Core/Services/CryptoService.cs

1145 lines
43 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Threading.Tasks;
using Bit.Core.Abstractions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Domain;
using Bit.Core.Models.Response;
using Bit.Core.Utilities;
namespace Bit.Core.Services
{
public class CryptoService : ICryptoService
{
private const string RANDOM_STRING_CHARSET = "abcdefghijklmnopqrstuvwxyz1234567890";
private readonly IStateService _stateService;
private readonly ICryptoFunctionService _cryptoFunctionService;
private SymmetricCryptoKey _legacyEtmKey;
private string _masterKeyHash;
private byte[] _publicKey;
private byte[] _privateKey;
private Dictionary<string, OrgKey> _orgKeys;
private Task<Dictionary<string, OrgKey>> _getOrgKeysTask;
public CryptoService(
IStateService stateService,
ICryptoFunctionService cryptoFunctionService)
{
_stateService = stateService;
_cryptoFunctionService = cryptoFunctionService;
}
public void ClearCache()
{
_legacyEtmKey = null;
_masterKeyHash = null;
_publicKey = null;
_privateKey = null;
_orgKeys = null;
}
public async Task RefreshKeysAsync()
{
// Refresh or clear additional keys such as
// pin and auto unlock keys
await SetUserKeyAsync(await GetUserKeyAsync());
}
public async Task SetUserKeyAsync(UserKey userKey, string userId = null)
{
await _stateService.SetUserKeyAsync(userKey, userId);
await StoreAdditionalKeysAsync(userKey, userId);
}
public Task<UserKey> GetUserKeyAsync(string userId = null)
{
return _stateService.GetUserKeyAsync(userId);
}
public async Task<bool> IsLegacyUserAsync(MasterKey masterKey = null, string userId = null)
{
masterKey ??= await GetMasterKeyAsync(userId);
if (masterKey == null)
{
return false;
}
return await ValidateUserKeyAsync(new UserKey(masterKey.Key));
}
public async Task<UserKey> GetUserKeyWithLegacySupportAsync(string userId = null)
{
var userKey = await GetUserKeyAsync(userId);
if (userKey != null)
{
return userKey;
}
// Legacy support: encryption used to be done with the master key (derived from master password).
// Users who have not migrated will have a null user key and must use the master key instead.
return new UserKey((await GetMasterKeyAsync(userId)).Key);
}
public async Task<bool> HasUserKeyAsync(string userId = null)
{
return await GetUserKeyAsync(userId) != null;
}
public async Task<bool> HasEncryptedUserKeyAsync(string userId = null)
{
return await _stateService.GetMasterKeyEncryptedUserKeyAsync(userId) != null;
}
public async Task<UserKey> MakeUserKeyAsync()
{
return new UserKey(await _cryptoFunctionService.RandomBytesAsync(64));
}
public Task ClearUserKeyAsync(string userId = null)
{
return _stateService.SetUserKeyAsync(null, userId);
}
public Task SetMasterKeyEncryptedUserKeyAsync(string value, string userId = null)
{
return _stateService.SetMasterKeyEncryptedUserKeyAsync(value, userId);
}
public async Task<UserKey> GetAutoUnlockKeyAsync(string userId = null)
{
await MigrateAutoAndBioKeysIfNeededAsync(userId);
return await _stateService.GetUserKeyAutoUnlockAsync(userId);
}
public async Task<bool> HasAutoUnlockKeyAsync(string userId = null)
{
return await GetAutoUnlockKeyAsync(userId) != null;
}
public async Task<UserKey> GetBiometricUnlockKeyAsync(string userId = null)
{
await MigrateAutoAndBioKeysIfNeededAsync(userId);
return await _stateService.GetUserKeyBiometricUnlockAsync(userId);
}
public Task SetMasterKeyAsync(MasterKey masterKey, string userId = null)
{
return _stateService.SetMasterKeyAsync(masterKey, userId);
}
public async Task<MasterKey> GetMasterKeyAsync(string userId = null)
{
var masterKey = await _stateService.GetMasterKeyAsync(userId);
if (masterKey == null)
{
var masterKeyDecrypted = await _stateService.GetKeyDecryptedAsync(userId);
if (masterKeyDecrypted == null)
{
return null;
}
// Migration support
masterKey = new MasterKey(masterKeyDecrypted.Key);
if (masterKey != null)
{
await SetMasterKeyAsync(masterKey, userId);
}
}
return masterKey;
}
public Task<MasterKey> MakeMasterKeyAsync(string password, string email, KdfConfig kdfConfig)
{
return MakeKeyAsync(password, email, kdfConfig, keyBytes => new MasterKey(keyBytes));
}
public Task ClearMasterKeyAsync(string userId = null)
{
return _stateService.SetMasterKeyAsync(null, userId);
}
public async Task<Tuple<UserKey, EncString>> EncryptUserKeyWithMasterKeyAsync(MasterKey masterKey, UserKey userKey = null)
{
userKey ??= await GetUserKeyAsync();
if (userKey == null)
{
throw new UserKeyNullException();
}
return await BuildProtectedSymmetricKeyAsync(masterKey, userKey.Key, keyBytes => new UserKey(keyBytes));
}
public async Task<UserKey> DecryptUserKeyWithMasterKeyAsync(MasterKey masterKey, EncString encUserKey = null, string userId = null)
{
masterKey ??= await GetMasterKeyAsync(userId);
if (masterKey == null)
{
throw new MasterKeyNullException();
}
if (encUserKey == null)
{
var userKeyMasterKey = await _stateService.GetMasterKeyEncryptedUserKeyAsync(userId);
if (userKeyMasterKey is null)
{
// Migrate old key
var oldEncUserKey = await _stateService.GetEncKeyEncryptedAsync(userId);
if (oldEncUserKey is null)
{
throw new Exception("No encrypted user key nor old encKeyEncrypted found");
}
var userKey = await DecryptUserKeyWithMasterKeyAsync(
masterKey,
new EncString(oldEncUserKey),
userId
);
await SetMasterKeyEncryptedUserKeyAsync(oldEncUserKey, userId);
await _stateService.SetEncKeyEncryptedAsync(null, userId);
return userKey;
}
encUserKey = new EncString(userKeyMasterKey);
}
byte[] decUserKey = null;
if (encUserKey.EncryptionType == EncryptionType.AesCbc256_B64)
{
decUserKey = await DecryptToBytesAsync(encUserKey, masterKey);
}
else if (encUserKey.EncryptionType == EncryptionType.AesCbc256_HmacSha256_B64)
{
var newKey = await StretchKeyAsync(masterKey, keyBytes => new MasterKey(keyBytes));
decUserKey = await DecryptToBytesAsync(encUserKey, newKey);
}
else
{
throw new Exception($"Unsupported encrypted user key type: {encUserKey.EncryptionType}");
}
if (decUserKey == null)
{
return null;
}
return new UserKey(decUserKey);
}
public async Task<Tuple<SymmetricCryptoKey, EncString>> MakeDataEncKeyAsync(SymmetricCryptoKey key)
{
if (key is null)
{
throw new ArgumentNullException(nameof(key));
}
if (!(key is UserKey) && !(key is OrgKey) && !(key is CipherKey))
{
throw new ArgumentException($"Data encryption keys must be of type UserKey or OrgKey or CipherKey. {key.GetType().FullName} unsupported.");
}
var newSymKey = await _cryptoFunctionService.RandomBytesAsync(64);
return await BuildProtectedSymmetricKeyAsync(key, newSymKey, keyBytes => new SymmetricCryptoKey(keyBytes));
}
public async Task<string> HashMasterKeyAsync(string password, MasterKey masterKey, HashPurpose hashPurpose = HashPurpose.ServerAuthorization)
{
if (password is null)
{
throw new ArgumentNullException(nameof(password));
}
if (masterKey is null)
{
masterKey = await GetMasterKeyAsync();
if (masterKey is null)
{
throw new ArgumentNullException(nameof(masterKey));
}
}
var hash = await _cryptoFunctionService.Pbkdf2Async(masterKey.Key, password, CryptoHashAlgorithm.Sha256, (int)hashPurpose);
return Convert.ToBase64String(hash);
}
public Task SetMasterKeyHashAsync(string keyHash)
{
_masterKeyHash = keyHash;
return _stateService.SetKeyHashAsync(keyHash);
}
public async Task<string> GetMasterKeyHashAsync()
{
if (_masterKeyHash != null)
{
return _masterKeyHash;
}
var passwordHash = await _stateService.GetKeyHashAsync();
if (passwordHash != null)
{
_masterKeyHash = passwordHash;
}
return _masterKeyHash;
}
public Task ClearMasterKeyHashAsync(string userId = null)
{
_masterKeyHash = null;
return _stateService.SetKeyHashAsync(null, userId);
}
public async Task<bool> CompareAndUpdateKeyHashAsync(string masterPassword, MasterKey key)
{
var storedPasswordHash = await GetMasterKeyHashAsync();
if (masterPassword != null && storedPasswordHash != null)
{
var localPasswordHash = await HashMasterKeyAsync(masterPassword, key, HashPurpose.LocalAuthorization);
if (localPasswordHash != null && storedPasswordHash == localPasswordHash)
{
return true;
}
var serverPasswordHash = await HashMasterKeyAsync(masterPassword, key, HashPurpose.ServerAuthorization);
if (serverPasswordHash != null && storedPasswordHash == serverPasswordHash)
{
await SetMasterKeyHashAsync(localPasswordHash);
return true;
}
}
return false;
}
public Task SetOrgKeysAsync(IEnumerable<ProfileOrganizationResponse> orgs)
{
var orgKeys = orgs.ToDictionary(org => org.Id, org => org.Key);
_orgKeys = null;
return _stateService.SetOrgKeysEncryptedAsync(orgKeys);
}
public async Task<OrgKey> 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 Task<Dictionary<string, OrgKey>> GetOrgKeysAsync()
{
if (_orgKeys != null && _orgKeys.Count > 0)
{
return Task.FromResult(_orgKeys);
}
if (_getOrgKeysTask != null && !_getOrgKeysTask.IsCompleted && !_getOrgKeysTask.IsFaulted)
{
return _getOrgKeysTask;
}
async Task<Dictionary<string, OrgKey>> doTask()
{
try
{
var encOrgKeys = await _stateService.GetOrgKeysEncryptedAsync();
if (encOrgKeys == null)
{
return null;
}
var orgKeys = new Dictionary<string, OrgKey>();
var setKey = false;
foreach (var org in encOrgKeys)
{
var decValue = await RsaDecryptAsync(org.Value);
orgKeys.Add(org.Key, new OrgKey(decValue));
setKey = true;
}
if (setKey)
{
_orgKeys = orgKeys;
}
return _orgKeys;
}
finally
{
_getOrgKeysTask = null;
}
}
_getOrgKeysTask = doTask();
return _getOrgKeysTask;
}
public async Task ClearOrgKeysAsync(bool memoryOnly = false, string userId = null)
{
_orgKeys = null;
if (!memoryOnly)
{
await _stateService.SetOrgKeysEncryptedAsync(null, userId);
}
}
public async Task<byte[]> GetUserPublicKeyAsync()
{
if (_publicKey != null)
{
return _publicKey;
}
var privateKey = await GetUserPrivateKeyAsync();
if (privateKey == null)
{
return null;
}
_publicKey = await _cryptoFunctionService.RsaExtractPublicKeyAsync(privateKey);
return _publicKey;
}
public async Task SetUserPrivateKeyAsync(string encPrivateKey)
{
if (encPrivateKey == null)
{
return;
}
await _stateService.SetPrivateKeyEncryptedAsync(encPrivateKey);
_privateKey = null;
}
public async Task<byte[]> GetUserPrivateKeyAsync()
{
if (_privateKey != null)
{
return _privateKey;
}
var encPrivateKey = await _stateService.GetPrivateKeyEncryptedAsync();
if (encPrivateKey == null)
{
return null;
}
_privateKey = await DecryptToBytesAsync(new EncString(encPrivateKey), null);
return _privateKey;
}
public async Task<List<string>> GetFingerprintAsync(string userId, byte[] publicKey = null)
{
if (publicKey == null)
{
publicKey = await GetUserPublicKeyAsync();
}
if (publicKey == null)
{
throw new Exception("No public key available.");
}
var keyFingerprint = await _cryptoFunctionService.HashAsync(publicKey, CryptoHashAlgorithm.Sha256);
var userFingerprint = await _cryptoFunctionService.HkdfExpandAsync(keyFingerprint, Encoding.UTF8.GetBytes(userId), 32, HkdfAlgorithm.Sha256);
return HashPhrase(userFingerprint);
}
public async Task<Tuple<string, EncString>> 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, EncString>(publicB64, privateEnc);
}
public async Task ClearKeyPairAsync(bool memoryOnly = false, string userId = null)
{
_publicKey = _privateKey = null;
if (!memoryOnly)
{
await _stateService.SetPrivateKeyEncryptedAsync(null, userId);
}
}
public async Task<PinKey> MakePinKeyAsync(string pin, string salt, KdfConfig config)
{
var pinKey = await MakeKeyAsync(pin, salt, config, keyBytes => new PinKey(keyBytes));
return await StretchKeyAsync(pinKey, keyBytes => new PinKey(keyBytes));
}
public Task ClearPinKeysAsync(string userId = null)
{
return Task.WhenAll(
_stateService.SetPinKeyEncryptedUserKeyAsync(null, userId),
_stateService.SetPinKeyEncryptedUserKeyEphemeralAsync(null, userId),
_stateService.SetProtectedPinAsync(null, userId),
ClearDeprecatedPinKeysAsync(userId));
}
public async Task<UserKey> DecryptUserKeyWithPinAsync(string pin, string salt, KdfConfig kdfConfig, EncString pinProtectedUserKey = null)
{
pinProtectedUserKey ??= await _stateService.GetPinKeyEncryptedUserKeyAsync();
pinProtectedUserKey ??= await _stateService.GetPinKeyEncryptedUserKeyEphemeralAsync();
if (pinProtectedUserKey == null)
{
throw new Exception("No PIN protected user key found.");
}
var pinKey = await MakePinKeyAsync(pin, salt, kdfConfig);
var userKeyBytes = await DecryptToBytesAsync(pinProtectedUserKey, pinKey);
return new UserKey(userKeyBytes);
}
// Only for migration purposes
public async Task<MasterKey> DecryptMasterKeyWithPinAsync(
string pin,
string salt,
KdfConfig kdfConfig,
EncString pinProtectedMasterKey = null)
{
if (pinProtectedMasterKey == null)
{
var pinProtectedMasterKeyString = await _stateService.GetPinProtectedAsync();
if (pinProtectedMasterKeyString == null)
{
throw new Exception("No PIN protected master key found.");
}
pinProtectedMasterKey = new EncString(pinProtectedMasterKeyString);
}
var pinKey = await MakePinKeyAsync(pin, salt, kdfConfig);
var masterKey = await DecryptToBytesAsync(pinProtectedMasterKey, pinKey);
return new MasterKey(masterKey);
}
public async Task<SymmetricCryptoKey> MakeSendKeyAsync(byte[] keyMaterial)
{
var sendKey = await _cryptoFunctionService.HkdfAsync(keyMaterial, "bitwarden-send", "send", 64, HkdfAlgorithm.Sha256);
return new SymmetricCryptoKey(sendKey);
}
public async Task<EncString> RsaEncryptAsync(byte[] data, byte[] publicKey = null)
{
if (publicKey == null)
{
publicKey = await GetUserPublicKeyAsync();
}
if (publicKey == null)
{
throw new Exception("Public key unavailable.");
}
var encBytes = await _cryptoFunctionService.RsaEncryptAsync(data, publicKey, CryptoHashAlgorithm.Sha1);
return new EncString(EncryptionType.Rsa2048_OaepSha1_B64, Convert.ToBase64String(encBytes));
}
public async Task<byte[]> RsaDecryptAsync(string encValue, byte[] privateKey = null)
{
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]);
if (privateKey is null)
{
privateKey = await GetUserPrivateKeyAsync();
}
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);
}
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));
}
/// <summary>
/// Makes random string with length <paramref name="length"/> based on the charset <see cref="RANDOM_STRING_CHARSET"/>
/// </summary>
public async Task<string> RandomStringAsync(int length)
{
var sb = new StringBuilder();
for (var i = 0; i < length; i++)
{
var randomCharIndex = await RandomNumberAsync(0, RANDOM_STRING_CHARSET.Length - 1);
sb.Append(RANDOM_STRING_CHARSET[randomCharIndex]);
}
return sb.ToString();
}
// TODO: The following operations should be moved to a new encrypt service
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 Task<byte[]> DecryptToBytesAsync(EncString encString, SymmetricCryptoKey key = null)
{
var iv = Convert.FromBase64String(encString.Iv);
var data = Convert.FromBase64String(encString.Data);
var mac = !string.IsNullOrWhiteSpace(encString.Mac) ? Convert.FromBase64String(encString.Mac) : null;
return AesDecryptToBytesAsync(encString.EncryptionType, data, iv, mac, key);
}
public Task<string> DecryptToUtf8Async(EncString encString, SymmetricCryptoKey key = null)
{
return AesDecryptToUtf8Async(encString.EncryptionType, encString.Data,
encString.Iv, encString.Mac, key);
}
public async Task<EncString> EncryptAsync(string plainValue, SymmetricCryptoKey key = null)
{
if (plainValue == null)
{
return null;
}
return await EncryptAsync(Encoding.UTF8.GetBytes(plainValue), key);
}
public async Task<EncString> 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 EncString(encObj.Key.EncType, data, iv, mac);
}
public async Task<EncByteArray> 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 new EncByteArray(encBytes);
}
public async Task<MasterKey> GetOrDeriveMasterKeyAsync(string password, string userId = null)
{
var masterKey = await GetMasterKeyAsync(userId);
return masterKey ?? await this.MakeMasterKeyAsync(
password,
await _stateService.GetEmailAsync(userId),
await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile)));
}
// --HELPER METHODS--
private async Task StoreAdditionalKeysAsync(UserKey userKey, string userId = null)
{
// Set, refresh, or clear the pin key
if (await _stateService.GetProtectedPinAsync(userId) != null)
{
await UpdatePinKeyAsync(userKey, userId);
}
else
{
await _stateService.SetPinKeyEncryptedUserKeyAsync(null, userId);
await _stateService.SetPinKeyEncryptedUserKeyEphemeralAsync(null, userId);
}
// Set, refresh, or clear the auto unlock key
if (await _stateService.GetVaultTimeoutAsync(userId) == null)
{
await _stateService.SetUserKeyAutoUnlockAsync(userKey, userId);
}
else
{
await _stateService.SetUserKeyAutoUnlockAsync(null, userId);
}
// Set, refresh, or clear the biometric unlock key
if (await _stateService.GetBiometricUnlockAsync(userId) is true)
{
await _stateService.SetUserKeyBiometricUnlockAsync(userKey, userId);
}
else
{
await _stateService.SetUserKeyBiometricUnlockAsync(null, userId);
}
}
private async Task UpdatePinKeyAsync(UserKey userKey, string userId = null)
{
var pin = await DecryptToUtf8Async(new EncString(await _stateService.GetProtectedPinAsync(userId)));
var pinKey = await MakePinKeyAsync(
pin,
await _stateService.GetEmailAsync(userId),
await _stateService.GetActiveUserCustomDataAsync(a => new KdfConfig(a?.Profile))
);
var encPin = await EncryptAsync(userKey.Key, pinKey);
if (await _stateService.GetPinKeyEncryptedUserKeyAsync(userId) != null)
{
await _stateService.SetPinKeyEncryptedUserKeyAsync(encPin, userId);
return;
}
await _stateService.SetPinKeyEncryptedUserKeyEphemeralAsync(encPin, userId);
}
private async Task<EncryptedObject> AesEncryptAsync(byte[] data, SymmetricCryptoKey key)
{
var obj = new EncryptedObject
{
Key = key ?? await GetUserKeyWithLegacySupportAsync(),
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 = key ?? await GetUserKeyWithLegacySupportAsync();
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 (theKey.MacKey != null)
{
macKey = theKey.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 = key ?? await GetUserKeyWithLegacySupportAsync();
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 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;
}
// TODO: This needs to be moved into SymmetricCryptoKey model to remove the keyCreator hack
private async Task<TKey> StretchKeyAsync<TKey>(SymmetricCryptoKey key, Func<byte[], TKey> keyCreator)
where TKey : SymmetricCryptoKey
{
var newKey = new byte[64];
var enc = await _cryptoFunctionService.HkdfExpandAsync(key.Key, Encoding.UTF8.GetBytes("enc"), 32, HkdfAlgorithm.Sha256);
Buffer.BlockCopy(enc, 0, newKey, 0, 32);
var mac = await _cryptoFunctionService.HkdfExpandAsync(key.Key, Encoding.UTF8.GetBytes("mac"), 32, HkdfAlgorithm.Sha256);
Buffer.BlockCopy(mac, 0, newKey, 32, 32);
return keyCreator(newKey);
}
private List<string> HashPhrase(byte[] hash, int minimumEntropy = 64)
{
var wordLength = EEFLongWordList.Instance.List.Count;
var entropyPerWord = Math.Log(wordLength) / 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>();
var hashHex = string.Concat("0", BitConverter.ToString(hash).Replace("-", ""));
var hashNumber = BigInteger.Parse(hashHex, System.Globalization.NumberStyles.HexNumber);
while (numWords-- > 0)
{
var remainder = (int)(hashNumber % wordLength);
hashNumber = hashNumber / wordLength;
phrase.Add(EEFLongWordList.Instance.List[remainder]);
}
return phrase;
}
// TODO: This needs to be moved into SymmetricCryptoKey model to remove the keyCreator hack
private async Task<Tuple<TKey, EncString>> BuildProtectedSymmetricKeyAsync<TKey>(SymmetricCryptoKey key,
byte[] encKey, Func<byte[], TKey> keyCreator) where TKey : SymmetricCryptoKey
{
EncString encKeyEnc = null;
if (key.Key.Length == 32)
{
var newKey = await StretchKeyAsync(key, keyCreator);
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<TKey, EncString>(keyCreator(encKey), encKeyEnc);
}
// TODO: This needs to be moved into SymmetricCryptoKey model to remove the keyCreator hack
private async Task<TKey> MakeKeyAsync<TKey>(string password, string salt, KdfConfig kdfConfig, Func<byte[], TKey> keyCreator)
where TKey : SymmetricCryptoKey
{
byte[] key;
if (kdfConfig.Type == null || kdfConfig.Type == KdfType.PBKDF2_SHA256)
{
var iterations = kdfConfig.Iterations.GetValueOrDefault(5000);
if (iterations < 5000)
{
throw new Exception("PBKDF2 iteration minimum is 5000.");
}
key = await _cryptoFunctionService.Pbkdf2Async(password, salt,
CryptoHashAlgorithm.Sha256, iterations);
}
else if (kdfConfig.Type == KdfType.Argon2id)
{
var iterations = kdfConfig.Iterations.GetValueOrDefault(Constants.Argon2Iterations);
var memory = kdfConfig.Memory.GetValueOrDefault(Constants.Argon2MemoryInMB) * 1024;
var parallelism = kdfConfig.Parallelism.GetValueOrDefault(Constants.Argon2Parallelism);
if (kdfConfig.Iterations < 2)
{
throw new Exception("Argon2 iterations minimum is 2");
}
if (kdfConfig.Memory < 16)
{
throw new Exception("Argon2 memory minimum is 16 MB");
}
else if (kdfConfig.Memory > 1024)
{
throw new Exception("Argon2 memory maximum is 1024 MB");
}
if (kdfConfig.Parallelism < 1)
{
throw new Exception("Argon2 parallelism minimum is 1");
}
var saltHash = await _cryptoFunctionService.HashAsync(salt, CryptoHashAlgorithm.Sha256);
key = await _cryptoFunctionService.Argon2Async(password, saltHash, iterations, memory, parallelism);
}
else
{
throw new Exception("Unknown kdf.");
}
return keyCreator(key);
}
private async Task<bool> ValidateUserKeyAsync(UserKey key, string userId = null)
{
if (key == null)
{
return false;
}
try
{
var encPrivateKey = await _stateService.GetPrivateKeyEncryptedAsync(userId);
if (encPrivateKey == null)
{
return false;
}
var privateKey = await DecryptToBytesAsync(new EncString(encPrivateKey), key);
await _cryptoFunctionService.RsaExtractPublicKeyAsync(privateKey);
return true;
}
catch
{
return false;
}
}
private class EncryptedObject
{
public byte[] Iv { get; set; }
public byte[] Data { get; set; }
public byte[] Mac { get; set; }
public SymmetricCryptoKey Key { get; set; }
}
// --MIGRATION METHODS--
// We previously used the master key for additional keys, but now we use the user key.
// These methods support migrating the old keys to the new ones.
private async Task MigrateAutoAndBioKeysIfNeededAsync(string userId = null)
{
var oldKey = await _stateService.GetKeyEncryptedAsync(userId);
if (oldKey == null)
{
return;
}
// Decrypt
var masterKey = new MasterKey(Convert.FromBase64String(oldKey));
if (await IsLegacyUserAsync(masterKey, userId))
{
throw new LegacyUserException();
}
var encryptedUserKey = await _stateService.GetEncKeyEncryptedAsync(userId);
if (encryptedUserKey == null)
{
return;
}
var userKey = await DecryptUserKeyWithMasterKeyAsync(
masterKey,
new EncString(encryptedUserKey),
userId);
// Migrate
if (await _stateService.GetVaultTimeoutAsync(userId) == null)
{
await _stateService.SetUserKeyAutoUnlockAsync(userKey, userId);
}
if (await _stateService.GetBiometricUnlockAsync(userId) is true)
{
await _stateService.SetUserKeyBiometricUnlockAsync(userKey, userId);
}
await _stateService.SetKeyEncryptedAsync(null, userId);
// Set encrypted user key just in case the user locks without syncing
await SetMasterKeyEncryptedUserKeyAsync(encryptedUserKey);
}
public async Task<UserKey> DecryptAndMigrateOldPinKeyAsync(
bool masterPasswordOnRestart,
string pin,
string email,
KdfConfig kdfConfig,
EncString oldPinKey)
{
// Decrypt
var masterKey = await DecryptMasterKeyWithPinAsync(
pin,
email,
kdfConfig,
oldPinKey
);
if (await IsLegacyUserAsync(masterKey))
{
throw new LegacyUserException();
}
var encUserKey = await _stateService.GetEncKeyEncryptedAsync();
var userKey = await DecryptUserKeyWithMasterKeyAsync(
masterKey,
new EncString(encUserKey)
);
// Migrate
var pinKey = await MakePinKeyAsync(pin, email, kdfConfig);
var pinProtectedKey = await EncryptAsync(userKey.Key, pinKey);
if (masterPasswordOnRestart)
{
await _stateService.SetPinProtectedKeyAsync(null);
await _stateService.SetPinKeyEncryptedUserKeyEphemeralAsync(pinProtectedKey);
}
else
{
await _stateService.SetPinProtectedAsync(null);
await _stateService.SetPinKeyEncryptedUserKeyAsync(pinProtectedKey);
// We previously only set the protected pin if MP on Restart was enabled
// now we set it regardless
var encPin = await EncryptAsync(pin, userKey);
await _stateService.SetProtectedPinAsync(encPin.EncryptedString);
}
// Clear old key only if not needed for bio/auto migration
if (await _stateService.GetKeyEncryptedAsync() != null)
{
await _stateService.SetEncKeyEncryptedAsync(null);
}
return userKey;
}
public Task ClearDeprecatedPinKeysAsync(string userId = null)
{
return Task.WhenAll(
_stateService.SetPinProtectedAsync(null, userId),
_stateService.SetPinProtectedKeyAsync(null, userId));
}
}
}