diff --git a/src/Uno.UWP/Security/Credentials/PasswordCredential.cs b/src/Uno.UWP/Security/Credentials/PasswordCredential.cs index 2e1d6039794b..0f2bad9f5770 100644 --- a/src/Uno.UWP/Security/Credentials/PasswordCredential.cs +++ b/src/Uno.UWP/Security/Credentials/PasswordCredential.cs @@ -1,46 +1,45 @@ using System; -namespace Windows.Security.Credentials +namespace Windows.Security.Credentials; + +public sealed partial class PasswordCredential { - public sealed partial class PasswordCredential + private string _userName; + private string _resource; + private string _password; + + public PasswordCredential() + : this(string.Empty, string.Empty, string.Empty) + { + } + + public PasswordCredential(string resource, string userName, string password) + { + Resource = resource; + UserName = userName; + Password = password; + } + + public string Resource + { + get => _resource; + set => _resource = value ?? throw new ArgumentNullException(nameof(Resource)); + } + + public string UserName + { + get => _userName; + set => _userName = value ?? throw new ArgumentNullException(nameof(UserName)); + } + + public string Password + { + get => _password; + set => _password = value ?? throw new ArgumentNullException(nameof(Password)); + } + + public void RetrievePassword() { - private string _userName; - private string _resource; - private string _password; - - public PasswordCredential() - : this(string.Empty, string.Empty, string.Empty) - { - } - - public PasswordCredential(string resource, string userName, string password) - { - Resource = resource; - UserName = userName; - Password = password; - } - - public string Resource - { - get => _resource; - set => _resource = value ?? throw new ArgumentNullException(nameof(Resource)); - } - - public string UserName - { - get => _userName; - set => _userName = value ?? throw new ArgumentNullException(nameof(UserName)); - } - - public string Password - { - get => _password; - set => _password = value ?? throw new ArgumentNullException(nameof(Password)); - } - - public void RetrievePassword() - { - // Nothing to do, we never hide the password - } + // Nothing to do, we never hide the password } } diff --git a/src/Uno.UWP/Security/Credentials/PasswordVault.Android.cs b/src/Uno.UWP/Security/Credentials/PasswordVault.Android.cs index 72c242cfb3f8..1a0fba29ab4f 100644 --- a/src/Uno.UWP/Security/Credentials/PasswordVault.Android.cs +++ b/src/Uno.UWP/Security/Credentials/PasswordVault.Android.cs @@ -13,182 +13,181 @@ using Javax.Crypto.Spec; using CipherMode = Javax.Crypto.CipherMode; -namespace Windows.Security.Credentials +namespace Windows.Security.Credentials; + +sealed partial class PasswordVault { - sealed partial class PasswordVault + public PasswordVault() + : this(Build.VERSION.SdkInt > BuildVersionCodes.LollipopMr1 ? new KeyStorePersister() : (IPersister)new UnSecureKeyStorePersister()) { - public PasswordVault() - : this(Build.VERSION.SdkInt > BuildVersionCodes.LollipopMr1 ? new KeyStorePersister() : (IPersister)new UnSecureKeyStorePersister()) - { - } + } - public PasswordVault(string filePath) - : this(Build.VERSION.SdkInt > BuildVersionCodes.LollipopMr1 ? new KeyStorePersister() : (IPersister)new UnSecureKeyStorePersister()) - { - } + public PasswordVault(string filePath) + : this(Build.VERSION.SdkInt > BuildVersionCodes.LollipopMr1 ? new KeyStorePersister() : (IPersister)new UnSecureKeyStorePersister()) + { + } - private sealed class KeyStorePersister : FilePersister - { - private const string _notSupported = @"There is no way to properly persist secured content on this device. + private sealed class KeyStorePersister : FilePersister + { + private const string _notSupported = @"There is no way to properly persist secured content on this device. The 'AndroidKeyStore' is missing (or is innacessible), but it is a requirement for the 'PasswordVault' to store data securly. This usually means that the device is using an API older than 18 (4.3). More details: https://developer.android.com/reference/java/security/KeyStore"; - private const string _algo = KeyProperties.KeyAlgorithmAes; - private const string _block = KeyProperties.BlockModeCbc; - private const string _padding = KeyProperties.EncryptionPaddingPkcs7; - private const string _fullTransform = _algo + "/" + _block + "/" + _padding; - private const string _provider = "AndroidKeyStore"; - private const string _alias = "uno_passwordvault"; - private static readonly byte[] _iv = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(_alias)); + private const string _algo = KeyProperties.KeyAlgorithmAes; + private const string _block = KeyProperties.BlockModeCbc; + private const string _padding = KeyProperties.EncryptionPaddingPkcs7; + private const string _fullTransform = _algo + "/" + _block + "/" + _padding; + private const string _provider = "AndroidKeyStore"; + private const string _alias = "uno_passwordvault"; + private static readonly byte[] _iv = SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(_alias)); - private readonly IKey _key; + private readonly IKey _key; - public KeyStorePersister(string filePath = null) - : base(filePath) + public KeyStorePersister(string filePath = null) + : base(filePath) + { + KeyStore store; + try { - KeyStore store; - try - { - store = KeyStore.GetInstance(_provider); - } - catch (Exception e) - { - throw new NotSupportedException(_notSupported, e); - } - if (store == null) - { - throw new NotSupportedException(_notSupported); - } + store = KeyStore.GetInstance(_provider); + } + catch (Exception e) + { + throw new NotSupportedException(_notSupported, e); + } + if (store == null) + { + throw new NotSupportedException(_notSupported); + } - store.Load(null); + store.Load(null); - if (store.ContainsAlias(_alias)) - { - var key = store.GetKey(_alias, null); + if (store.ContainsAlias(_alias)) + { + var key = store.GetKey(_alias, null); - _key = key; - } - else - { - var generator = KeyGenerator.GetInstance(_algo, _provider); - generator.Init(new KeyGenParameterSpec.Builder(_alias, KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt) - .SetBlockModes(_block) - .SetEncryptionPaddings(_padding) - .SetRandomizedEncryptionRequired(false) - .Build()); - _key = generator.GenerateKey(); - } + _key = key; } - - /// - protected override Stream Encrypt(Stream outputStream) + else { - var cipher = Cipher.GetInstance(_fullTransform); - var iv = new IvParameterSpec(_iv, 0, cipher.BlockSize); + var generator = KeyGenerator.GetInstance(_algo, _provider); + generator.Init(new KeyGenParameterSpec.Builder(_alias, KeyStorePurpose.Encrypt | KeyStorePurpose.Decrypt) + .SetBlockModes(_block) + .SetEncryptionPaddings(_padding) + .SetRandomizedEncryptionRequired(false) + .Build()); + _key = generator.GenerateKey(); + } + } - cipher.Init(CipherMode.EncryptMode, _key, iv); + /// + protected override Stream Encrypt(Stream outputStream) + { + var cipher = Cipher.GetInstance(_fullTransform); + var iv = new IvParameterSpec(_iv, 0, cipher.BlockSize); - return new CipherStreamAdapter(new CipherOutputStream(outputStream, cipher)); - } + cipher.Init(CipherMode.EncryptMode, _key, iv); - /// - protected override Stream Decrypt(Stream inputStream) - { - var cipher = Cipher.GetInstance(_fullTransform); - var iv = new IvParameterSpec(_iv, 0, cipher.BlockSize); + return new CipherStreamAdapter(new CipherOutputStream(outputStream, cipher)); + } - cipher.Init(CipherMode.DecryptMode, _key, iv); + /// + protected override Stream Decrypt(Stream inputStream) + { + var cipher = Cipher.GetInstance(_fullTransform); + var iv = new IvParameterSpec(_iv, 0, cipher.BlockSize); - return new InputStreamInvoker(new CipherInputStream(inputStream, cipher)); - } + cipher.Init(CipherMode.DecryptMode, _key, iv); + + return new InputStreamInvoker(new CipherInputStream(inputStream, cipher)); + } + + private class CipherStreamAdapter : Stream + { + private readonly CipherOutputStream _output; + private readonly Stream _adapter; - private class CipherStreamAdapter : Stream + private bool _isDisposed; + + public CipherStreamAdapter(CipherOutputStream output) { - private readonly CipherOutputStream _output; - private readonly Stream _adapter; + _output = output; + _adapter = new OutputStreamInvoker(output); + } - private bool _isDisposed; + public override bool CanRead => _adapter.CanRead; - public CipherStreamAdapter(CipherOutputStream output) - { - _output = output; - _adapter = new OutputStreamInvoker(output); - } + public override bool CanSeek => _adapter.CanSeek; - public override bool CanRead => _adapter.CanRead; + public override bool CanWrite => _adapter.CanWrite; - public override bool CanSeek => _adapter.CanSeek; + public override long Length => _adapter.Length; - public override bool CanWrite => _adapter.CanWrite; + public override long Position + { + get => _adapter.Position; + set => _adapter.Position = value; + } - public override long Length => _adapter.Length; + public override void Flush() + => _adapter.Flush(); - public override long Position + protected override void Dispose(bool disposing) + { + if (_isDisposed) { - get => _adapter.Position; - set => _adapter.Position = value; + // We cannot .Close() the _output multiple times. + return; } + _isDisposed = true; - public override void Flush() - => _adapter.Flush(); - - protected override void Dispose(bool disposing) + if (disposing) { - if (_isDisposed) - { - // We cannot .Close() the _output multiple times. - return; - } - _isDisposed = true; - - if (disposing) - { - _output.Close(); - } - - _adapter.Dispose(); - _output.Dispose(); - base.Dispose(disposing); + _output.Close(); } - public override int Read(byte[] buffer, int offset, int count) - => _adapter.Read(buffer, offset, count); + _adapter.Dispose(); + _output.Dispose(); + base.Dispose(disposing); + } - public override long Seek(long offset, SeekOrigin origin) - => _adapter.Seek(offset, origin); + public override int Read(byte[] buffer, int offset, int count) + => _adapter.Read(buffer, offset, count); - public override void SetLength(long value) - => _adapter.SetLength(value); + public override long Seek(long offset, SeekOrigin origin) + => _adapter.Seek(offset, origin); - public override void Write(byte[] buffer, int offset, int count) - => _adapter.Write(buffer, offset, count); - } - } + public override void SetLength(long value) + => _adapter.SetLength(value); - /// - /// Persister for devices bellow Android level 23. - /// RSA/ECB/PKCS1Padding only is supported and is not considered secure. - /// - private sealed class UnSecureKeyStorePersister : FilePersister - { - private const string _notSupported = @"RSA/ECB/PKCS1Padding with asymetric key is considered not secure and will not be supported for device under API level 23"; + public override void Write(byte[] buffer, int offset, int count) + => _adapter.Write(buffer, offset, count); + } + } - public UnSecureKeyStorePersister(string filePath = null) - : base(filePath) - { - throw new NotSupportedException(_notSupported); - } + /// + /// Persister for devices bellow Android level 23. + /// RSA/ECB/PKCS1Padding only is supported and is not considered secure. + /// + private sealed class UnSecureKeyStorePersister : FilePersister + { + private const string _notSupported = @"RSA/ECB/PKCS1Padding with asymetric key is considered not secure and will not be supported for device under API level 23"; - protected override Stream Decrypt(Stream inputStream) - { - throw new NotSupportedException(_notSupported); - } + public UnSecureKeyStorePersister(string filePath = null) + : base(filePath) + { + throw new NotSupportedException(_notSupported); + } - protected override Stream Encrypt(Stream outputStream) - { - throw new NotSupportedException(_notSupported); - } + protected override Stream Decrypt(Stream inputStream) + { + throw new NotSupportedException(_notSupported); } + protected override Stream Encrypt(Stream outputStream) + { + throw new NotSupportedException(_notSupported); + } } + } diff --git a/src/Uno.UWP/Security/Credentials/PasswordVault.cs b/src/Uno.UWP/Security/Credentials/PasswordVault.cs index cb37ae2b3cbc..d8f813ad8a90 100644 --- a/src/Uno.UWP/Security/Credentials/PasswordVault.cs +++ b/src/Uno.UWP/Security/Credentials/PasswordVault.cs @@ -12,450 +12,449 @@ using Uno.Extensions; using Uno.Foundation.Logging; -namespace Windows.Security.Credentials +namespace Windows.Security.Credentials; + +public partial class PasswordVault { - public partial class PasswordVault + private readonly object _updateGate = new object(); + private readonly IPersister _persister; + + private ImmutableList _credentials; + + protected PasswordVault(IPersister persister) { - private readonly object _updateGate = new object(); - private readonly IPersister _persister; + _persister = persister ?? throw new ArgumentNullException(nameof(persister)); + _credentials = Load(); + } - private ImmutableList _credentials; + public IReadOnlyList RetrieveAll() + => _credentials; - protected PasswordVault(IPersister persister) + public IReadOnlyList FindAllByResource(string resource) + { + // UWP: 'resource' is case sensitive + var result = _credentials.Where(cred => cred.Resource == resource).ToImmutableList(); + if (result.IsEmpty) { - _persister = persister ?? throw new ArgumentNullException(nameof(persister)); - _credentials = Load(); + throw new Exception("No match"); // UWP: Throw 'Exception' if no match } - public IReadOnlyList RetrieveAll() - => _credentials; + return result; + } - public IReadOnlyList FindAllByResource(string resource) + public IReadOnlyList FindAllByUserName(string userName) + { + // UWP: 'userName' is case sensitive + var result = _credentials.Where(cred => cred.UserName == userName).ToImmutableList(); + if (result.IsEmpty) { - // UWP: 'resource' is case sensitive - var result = _credentials.Where(cred => cred.Resource == resource).ToImmutableList(); - if (result.IsEmpty) - { - throw new Exception("No match"); // UWP: Throw 'Exception' if no match - } - - return result; + throw new Exception("No match"); // UWP: Throw 'Exception' if no match } - public IReadOnlyList FindAllByUserName(string userName) - { - // UWP: 'userName' is case sensitive - var result = _credentials.Where(cred => cred.UserName == userName).ToImmutableList(); - if (result.IsEmpty) - { - throw new Exception("No match"); // UWP: Throw 'Exception' if no match - } + return result; + } - return result; + public PasswordCredential Retrieve(string resource, string userName) + { + // UWP: Retrieve is case IN-sensitive for both 'resource' and 'userName' + var result = _credentials.FirstOrDefault(cred => Comparer.Instance.Equals(cred, resource, userName)); + if (result == null) + { + throw new Exception("No match"); // UWP: Throw 'Exception' if no match } - public PasswordCredential Retrieve(string resource, string userName) + return result; + } + + public void Remove(PasswordCredential credential) + { + while (true) { - // UWP: Retrieve is case IN-sensitive for both 'resource' and 'userName' - var result = _credentials.FirstOrDefault(cred => Comparer.Instance.Equals(cred, resource, userName)); - if (result == null) + var capture = _credentials; + var updated = capture.Remove(credential, Comparer.Instance); + + if (capture == updated) { - throw new Exception("No match"); // UWP: Throw 'Exception' if no match + return; } - return result; + lock (_updateGate) + { + if (capture == _credentials) + { + Persist(updated); + _credentials = updated; + + return; + } + } } + } - public void Remove(PasswordCredential credential) + public void Add(PasswordCredential credential) + { + while (true) { - while (true) + var capture = _credentials; + var existing = capture.FirstOrDefault(c => Comparer.Instance.Equals(c, credential)); + + ImmutableList updated; + if (existing == null) + { + updated = capture.Add(credential); + } + else { - var capture = _credentials; - var updated = capture.Remove(credential, Comparer.Instance); + existing.RetrievePassword(); + credential.RetrievePassword(); - if (capture == updated) + if (existing.Password == credential.Password) { + // no change, abort update! return; } - lock (_updateGate) + updated = capture.Replace(existing, credential); + } + + lock (_updateGate) + { + if (capture == _credentials) { - if (capture == _credentials) - { - Persist(updated); - _credentials = updated; + Persist(updated); + _credentials = updated; - return; - } + return; } } } + } - public void Add(PasswordCredential credential) + public ImmutableList Load() + { + try { - while (true) + if (_persister.TryOpenRead(out var src)) { - var capture = _credentials; - var existing = capture.FirstOrDefault(c => Comparer.Instance.Equals(c, credential)); - - ImmutableList updated; - if (existing == null) - { - updated = capture.Add(credential); - } - else + using (src) + using (var reader = new BinaryReader(src)) { - existing.RetrievePassword(); - credential.RetrievePassword(); + var count = reader.ReadInt32(); + var credentials = ImmutableList.CreateBuilder(); - if (existing.Password == credential.Password) + for (var i = 0; i < count; i++) { - // no change, abort update! - return; - } - - updated = capture.Replace(existing, credential); - } - - lock (_updateGate) - { - if (capture == _credentials) - { - Persist(updated); - _credentials = updated; + var res = reader.ReadString(); + var use = reader.ReadString(); + var pwd = reader.ReadString(); - return; + credentials.Add(new PasswordCredential(res, use, pwd)); } + + return credentials.ToImmutable(); } } } - - public ImmutableList Load() + catch (Exception e) { - try - { - if (_persister.TryOpenRead(out var src)) - { - using (src) - using (var reader = new BinaryReader(src)) - { - var count = reader.ReadInt32(); - var credentials = ImmutableList.CreateBuilder(); + this.Log().Warn("Failed to load values from persister, assume empty.", e); + } - for (var i = 0; i < count; i++) - { - var res = reader.ReadString(); - var use = reader.ReadString(); - var pwd = reader.ReadString(); + return ImmutableList.Empty; + } - credentials.Add(new PasswordCredential(res, use, pwd)); - } + public void Persist(ImmutableList credentials) + { + using (var transaction = _persister.OpenWrite(out var dst)) + using (dst) + using (var writer = new BinaryWriter(dst)) + { + writer.Write(credentials.Count); - return credentials.ToImmutable(); - } - } - } - catch (Exception e) + foreach (var credential in credentials) { - this.Log().Warn("Failed to load values from persister, assume empty.", e); + credential.RetrievePassword(); + + writer.Write(credential.Resource); + writer.Write(credential.UserName); + writer.Write(credential.Password); } - return ImmutableList.Empty; + writer.Flush(); + transaction.Commit(); } + } - public void Persist(ImmutableList credentials) - { - using (var transaction = _persister.OpenWrite(out var dst)) - using (dst) - using (var writer = new BinaryWriter(dst)) - { - writer.Write(credentials.Count); + /// + /// A persister responsible to securely persist the credentials managed by a PasswordVault + /// + protected interface IPersister + { + /// + /// Tries to open the source stream from which credentials can be read. + /// + /// The source stream which should be parsed to reload credentials + /// A bool which indicates if the is valid or not. + bool TryOpenRead(out Stream inputStream); - foreach (var credential in credentials) - { - credential.RetrievePassword(); + /// + /// Open the target stream which where credentials should be stored. + /// + /// The target stream where credentials can be stored + /// A which ensure to atomatically update the credentials + WriteTransaction OpenWrite(out Stream outputStream); + } - writer.Write(credential.Resource); - writer.Write(credential.UserName); - writer.Write(credential.Password); - } + /// + /// A transaction used to persist credentials to ensure ACID + /// + protected sealed class WriteTransaction : IDisposable + { + private readonly Action _onCommit; + private readonly Action _onComplete; - writer.Flush(); - transaction.Commit(); - } + private int _state = State.New; + + private static class State + { + public const int New = 0; + public const int Commited = 1; + public const int Disposed = int.MaxValue; } /// - /// A persister responsible to securely persist the credentials managed by a PasswordVault + /// Creates a new transaction /// - protected interface IPersister + /// Callback invoked when this transaction is committed (cf. . + /// Callback invoked when this transaction completes (i.e. Disposed). + public WriteTransaction(Action onCommit = null, Action onComplete = null) { - /// - /// Tries to open the source stream from which credentials can be read. - /// - /// The source stream which should be parsed to reload credentials - /// A bool which indicates if the is valid or not. - bool TryOpenRead(out Stream inputStream); - - /// - /// Open the target stream which where credentials should be stored. - /// - /// The target stream where credentials can be stored - /// A which ensure to atomatically update the credentials - WriteTransaction OpenWrite(out Stream outputStream); + _onCommit = onCommit; + _onComplete = onComplete; } /// - /// A transaction used to persist credentials to ensure ACID + /// A boolean which indicates if this transaction was committed or not (cf. ). /// - protected sealed class WriteTransaction : IDisposable + public bool IsCommited { - private readonly Action _onCommit; - private readonly Action _onComplete; - - private int _state = State.New; - - private static class State - { - public const int New = 0; - public const int Commited = 1; - public const int Disposed = int.MaxValue; - } - - /// - /// Creates a new transaction - /// - /// Callback invoked when this transaction is committed (cf. . - /// Callback invoked when this transaction completes (i.e. Disposed). - public WriteTransaction(Action onCommit = null, Action onComplete = null) - { - _onCommit = onCommit; - _onComplete = onComplete; - } - - /// - /// A boolean which indicates if this transaction was committed or not (cf. ). - /// - public bool IsCommited + get { - get + var state = _state; + if (state == State.Disposed) { - var state = _state; - if (state == State.Disposed) - { - throw new ObjectDisposedException(nameof(WriteTransaction)); - } - - return state == State.Commited; + throw new ObjectDisposedException(nameof(WriteTransaction)); } + + return state == State.Commited; } + } - /// - /// Makes the changes persistent - /// - public void Commit() + /// + /// Makes the changes persistent + /// + public void Commit() + { + switch (Interlocked.CompareExchange(ref _state, State.Commited, State.New)) { - switch (Interlocked.CompareExchange(ref _state, State.Commited, State.New)) - { - case State.New: - _onCommit?.Invoke(); - break; + case State.New: + _onCommit?.Invoke(); + break; - case State.Disposed: - throw new ObjectDisposedException(nameof(WriteTransaction)); - } + case State.Disposed: + throw new ObjectDisposedException(nameof(WriteTransaction)); } + } - /// - public void Dispose() + /// + public void Dispose() + { + var previousState = Interlocked.Exchange(ref _state, State.Disposed); + if (previousState != State.Disposed) { - var previousState = Interlocked.Exchange(ref _state, State.Disposed); - if (previousState != State.Disposed) - { - _onComplete?.Invoke(previousState == State.Commited); - } + _onComplete?.Invoke(previousState == State.Commited); } } + } + + /// + /// A base class to persist a PasswordVault in a file on the disk + /// + protected abstract class FilePersister : IPersister + { + private readonly string _tmp; + private readonly string _dst; /// - /// A base class to persist a PasswordVault in a file on the disk + /// Creates a new instance /// - protected abstract class FilePersister : IPersister + /// The path where the vault should be persisted + protected FilePersister(string filePath = null) { - private readonly string _tmp; - private readonly string _dst; - - /// - /// Creates a new instance - /// - /// The path where the vault should be persisted - protected FilePersister(string filePath = null) - { - _dst = filePath ?? Path.Combine(ApplicationData.Current.LocalFolder.Path, ".vault"); - _tmp = _dst + ".tmp"; - } + _dst = filePath ?? Path.Combine(ApplicationData.Current.LocalFolder.Path, ".vault"); + _tmp = _dst + ".tmp"; + } - /// - /// Wraps a given encrypted stream into a stream which ensure decryption - /// - /// The encrypted stream - /// The decrypted stream - protected abstract Stream Encrypt(Stream outputStream); - - /// - /// Wraps a given raw stream into a stream which ensure encryption - /// - /// The raw stream - /// The encrypted stream - protected abstract Stream Decrypt(Stream inputStream); - - /// - public bool TryOpenRead(out Stream inputStream) - { - var dst = new FileInfo(_dst); + /// + /// Wraps a given encrypted stream into a stream which ensure decryption + /// + /// The encrypted stream + /// The decrypted stream + protected abstract Stream Encrypt(Stream outputStream); - var exists = dst.Exists; + /// + /// Wraps a given raw stream into a stream which ensure encryption + /// + /// The raw stream + /// The encrypted stream + protected abstract Stream Decrypt(Stream inputStream); - if (exists) - { - var length = dst.Length; + /// + public bool TryOpenRead(out Stream inputStream) + { + var dst = new FileInfo(_dst); - inputStream = Decrypt(File.Open(_dst, FileMode.Open, FileAccess.Read, FileShare.Read)); - return true; - } - else - { - inputStream = Stream.Null; - return false; - } - } + var exists = dst.Exists; - /// - public WriteTransaction OpenWrite(out Stream outputStream) + if (exists) { - var fileStream = File.Open(_tmp, FileMode.Create, FileAccess.Write, FileShare.None); - var encryptedStream = Encrypt(fileStream); + var length = dst.Length; - outputStream = encryptedStream; + inputStream = Decrypt(File.Open(_dst, FileMode.Open, FileAccess.Read, FileShare.Read)); + return true; + } + else + { + inputStream = Stream.Null; + return false; + } + } - return new WriteTransaction(onComplete: Complete); + /// + public WriteTransaction OpenWrite(out Stream outputStream) + { + var fileStream = File.Open(_tmp, FileMode.Create, FileAccess.Write, FileShare.None); + var encryptedStream = Encrypt(fileStream); - void Complete(bool isCommitted) - { - // The encryptedStream has been disposed by the "Persist" but make sure to dispose - // the underlying file stream before accessing to the file itself. - //fileStream.Flush(); - //fileStream.Close(); - fileStream.Dispose(); + outputStream = encryptedStream; - if (!isCommitted) - { - return; - } + return new WriteTransaction(onComplete: Complete); - if (File.Exists(_dst)) - { - // Note: We don't use the backup file. We don't want that removed credentials - // can be restored by altering the current '_dst' file. - File.Replace(_tmp, _dst, null, ignoreMetadataErrors: true); - } - else - { - File.Move(_tmp, _dst); - } + void Complete(bool isCommitted) + { + // The encryptedStream has been disposed by the "Persist" but make sure to dispose + // the underlying file stream before accessing to the file itself. + //fileStream.Flush(); + //fileStream.Close(); + fileStream.Dispose(); + + if (!isCommitted) + { + return; + } + + if (File.Exists(_dst)) + { + // Note: We don't use the backup file. We don't want that removed credentials + // can be restored by altering the current '_dst' file. + File.Replace(_tmp, _dst, null, ignoreMetadataErrors: true); + } + else + { + File.Move(_tmp, _dst); } } } + } + + /// + /// A basically encrypted persister which does not provide an acceptable security level for sensitive data like a password. + /// + protected sealed class UnsecuredPersister : FilePersister + { + private readonly byte[] _key; + private readonly byte[] _iv; /// - /// A basically encrypted persister which does not provide an acceptable security level for sensitive data like a password. + /// Creates a new instance providing the secrets for encryption (TripleDES) /// - protected sealed class UnsecuredPersister : FilePersister + /// The key, must be 24 bytes length + /// The IV, must be 8 bytes length + /// The path where the vault should be persisted + public UnsecuredPersister(byte[] key, byte[] iv, string filePath = null) + : base(filePath) { - private readonly byte[] _key; - private readonly byte[] _iv; - - /// - /// Creates a new instance providing the secrets for encryption (TripleDES) - /// - /// The key, must be 24 bytes length - /// The IV, must be 8 bytes length - /// The path where the vault should be persisted - public UnsecuredPersister(byte[] key, byte[] iv, string filePath = null) - : base(filePath) - { - _key = key ?? throw new ArgumentNullException(nameof(key)); - _iv = iv ?? throw new ArgumentNullException(nameof(iv)); + _key = key ?? throw new ArgumentNullException(nameof(key)); + _iv = iv ?? throw new ArgumentNullException(nameof(iv)); - if (_key.Length != 24) - { - throw new InvalidOperationException("The secret must have 24 bytes"); - } - if (_iv.Length != 8) - { - throw new InvalidOperationException("The iv must have 8 bytes"); - } + if (_key.Length != 24) + { + throw new InvalidOperationException("The secret must have 24 bytes"); } - - /// - /// Creates a new instance providing a simple password used to encrypt the file - /// - /// The password used to encrypt the file - /// The path where the vault should be persisted - public UnsecuredPersister(string password = null, string filePath = null) - : base(filePath) + if (_iv.Length != 8) { - (_key, _iv) = GenerateSecrets(password ?? GetEntryPointIdentifier()); + throw new InvalidOperationException("The iv must have 8 bytes"); } + } - private static string GetEntryPointIdentifier() - { - var assembly = Assembly.GetEntryAssembly(); - var method = assembly.EntryPoint.DeclaringType; + /// + /// Creates a new instance providing a simple password used to encrypt the file + /// + /// The password used to encrypt the file + /// The path where the vault should be persisted + public UnsecuredPersister(string password = null, string filePath = null) + : base(filePath) + { + (_key, _iv) = GenerateSecrets(password ?? GetEntryPointIdentifier()); + } - return assembly.GetName().Name + method.DeclaringType.FullName; - } + private static string GetEntryPointIdentifier() + { + var assembly = Assembly.GetEntryAssembly(); + var method = assembly.EntryPoint.DeclaringType; - private static (byte[] key, byte[] iv) GenerateSecrets(string password) + return assembly.GetName().Name + method.DeclaringType.FullName; + } + + private static (byte[] key, byte[] iv) GenerateSecrets(string password) + { + if (string.IsNullOrWhiteSpace(password)) { - if (string.IsNullOrWhiteSpace(password)) - { - throw new ArgumentOutOfRangeException(nameof(password), "Password is empty"); - } + throw new ArgumentOutOfRangeException(nameof(password), "Password is empty"); + } - var key = new byte[24]; - var iv = new byte[8]; + var key = new byte[24]; + var iv = new byte[8]; - var src = SHA256.HashData(Encoding.UTF8.GetBytes(password)); + var src = SHA256.HashData(Encoding.UTF8.GetBytes(password)); - Array.Copy(src, 0, key, 0, 24); - Array.Copy(src, 24, iv, 0, 8); + Array.Copy(src, 0, key, 0, 24); + Array.Copy(src, 24, iv, 0, 8); - return (key, iv); - } + return (key, iv); + } - protected override Stream Decrypt(Stream input) - => new CryptoStream(input, TripleDES.Create().CreateDecryptor(_key, _iv), CryptoStreamMode.Read); + protected override Stream Decrypt(Stream input) + => new CryptoStream(input, TripleDES.Create().CreateDecryptor(_key, _iv), CryptoStreamMode.Read); - protected override Stream Encrypt(Stream output) - => new CryptoStream(output, TripleDES.Create().CreateEncryptor(_key, _iv), CryptoStreamMode.Write); - } + protected override Stream Encrypt(Stream output) + => new CryptoStream(output, TripleDES.Create().CreateEncryptor(_key, _iv), CryptoStreamMode.Write); + } - private class Comparer : EqualityComparer - { - public static readonly Comparer Instance = new Comparer(); + private class Comparer : EqualityComparer + { + public static readonly Comparer Instance = new Comparer(); - public bool Equals(PasswordCredential obj, string resource, string userName) - => StringComparer.OrdinalIgnoreCase.Equals(obj.Resource, resource) - && StringComparer.OrdinalIgnoreCase.Equals(obj.UserName, userName); + public bool Equals(PasswordCredential obj, string resource, string userName) + => StringComparer.OrdinalIgnoreCase.Equals(obj.Resource, resource) + && StringComparer.OrdinalIgnoreCase.Equals(obj.UserName, userName); - public override bool Equals(PasswordCredential left, PasswordCredential right) - => StringComparer.OrdinalIgnoreCase.Equals(left.Resource, right.Resource) - && StringComparer.OrdinalIgnoreCase.Equals(left.UserName, right.UserName); + public override bool Equals(PasswordCredential left, PasswordCredential right) + => StringComparer.OrdinalIgnoreCase.Equals(left.Resource, right.Resource) + && StringComparer.OrdinalIgnoreCase.Equals(left.UserName, right.UserName); - public override int GetHashCode(PasswordCredential obj) - => StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Resource) - ^ StringComparer.OrdinalIgnoreCase.GetHashCode(obj.UserName); - } + public override int GetHashCode(PasswordCredential obj) + => StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Resource) + ^ StringComparer.OrdinalIgnoreCase.GetHashCode(obj.UserName); } } diff --git a/src/Uno.UWP/Security/Credentials/PasswordVault.iOSmacOS.cs b/src/Uno.UWP/Security/Credentials/PasswordVault.iOSmacOS.cs index 0e6ba4e074c8..1251e22f6742 100644 --- a/src/Uno.UWP/Security/Credentials/PasswordVault.iOSmacOS.cs +++ b/src/Uno.UWP/Security/Credentials/PasswordVault.iOSmacOS.cs @@ -3,81 +3,80 @@ using Foundation; using Security; -namespace Windows.Security.Credentials +namespace Windows.Security.Credentials; + +sealed partial class PasswordVault { - sealed partial class PasswordVault + public PasswordVault() + : this(new KeyChainPersister()) { - public PasswordVault() - : this(new KeyChainPersister()) - { - } + } - private sealed class KeyChainPersister : IPersister - { - private const string _accountId = "uno_passwordvault"; + private sealed class KeyChainPersister : IPersister + { + private const string _accountId = "uno_passwordvault"; - private readonly SecRecord _query = new SecRecord(SecKind.GenericPassword) { Account = _accountId }; + private readonly SecRecord _query = new SecRecord(SecKind.GenericPassword) { Account = _accountId }; - public bool TryOpenRead(out Stream inputStream) + public bool TryOpenRead(out Stream inputStream) + { + var record = SecKeyChain.QueryAsRecord(_query, out var statusCode); + if (statusCode == SecStatusCode.Success) { - var record = SecKeyChain.QueryAsRecord(_query, out var statusCode); - if (statusCode == SecStatusCode.Success) - { - inputStream = record.ValueData.AsStream(); - return true; - } - else - { - CheckCommonStatusCodes(statusCode); + inputStream = record.ValueData.AsStream(); + return true; + } + else + { + CheckCommonStatusCodes(statusCode); - inputStream = Stream.Null; - return false; - } + inputStream = Stream.Null; + return false; } + } - public WriteTransaction OpenWrite(out Stream outputStream) + public WriteTransaction OpenWrite(out Stream outputStream) + { + var stream = new MemoryStream(); + outputStream = stream; + + return new WriteTransaction(onCommit: () => Commit()); + + void Commit() { - var stream = new MemoryStream(); - outputStream = stream; + stream.Position = 0; + var record = new SecRecord() + { + ValueData = NSData.FromStream(stream) + }; - return new WriteTransaction(onCommit: () => Commit()); - void Commit() + var result = SecKeyChain.Update(_query, record); + if (result == SecStatusCode.ItemNotFound) { stream.Position = 0; - var record = new SecRecord() + record = new SecRecord(SecKind.GenericPassword) { + Account = _accountId, ValueData = NSData.FromStream(stream) }; + result = SecKeyChain.Add(record); + } - var result = SecKeyChain.Update(_query, record); - if (result == SecStatusCode.ItemNotFound) - { - stream.Position = 0; - record = new SecRecord(SecKind.GenericPassword) - { - Account = _accountId, - ValueData = NSData.FromStream(stream) - }; - - result = SecKeyChain.Add(record); - } - - if (result != SecStatusCode.Success) - { - CheckCommonStatusCodes(result); - throw new InvalidOperationException("Failed to persist the vault"); - } + if (result != SecStatusCode.Success) + { + CheckCommonStatusCodes(result); + throw new InvalidOperationException("Failed to persist the vault"); } } + } - private void CheckCommonStatusCodes(SecStatusCode code) + private void CheckCommonStatusCodes(SecStatusCode code) + { + if (code == SecStatusCode.MissingEntitlement) { - if (code == SecStatusCode.MissingEntitlement) - { - throw new InvalidOperationException("Your application is not allowed to use the keychain. Make sure that you have setup the KeyChain in the Entitlepements.plist of your application."); - } + throw new InvalidOperationException("Your application is not allowed to use the keychain. Make sure that you have setup the KeyChain in the Entitlepements.plist of your application."); } } } diff --git a/src/Uno.UWP/Security/Credentials/PasswordVault.others.cs b/src/Uno.UWP/Security/Credentials/PasswordVault.others.cs new file mode 100644 index 000000000000..f8a9f40526f2 --- /dev/null +++ b/src/Uno.UWP/Security/Credentials/PasswordVault.others.cs @@ -0,0 +1,25 @@ +#if !__ANDROID__ && !__IOS__ && !__MACOS__ +using System; +using Uno; + +namespace Windows.Security.Credentials; + +[NotImplemented("IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")] +// This class is ** NOT ** sealed in order to allow projects for which the security limit described bellow is not +// really a concern (for instance if they are only storing an OAuth token) to inherit and provide they own +// implementation of 'IPersister'. +partial class PasswordVault +{ + [NotImplemented("IS_UNIT_TESTS", "__WASM__", "__SKIA__", "__NETSTD_REFERENCE__")] + public PasswordVault() + { +#if !__WASM__ + throw new NotImplementedException(); +#else + throw new NotSupportedException(@"There is no way to properly persist secured content on WebAssembly. +At the opposite of other platforms, we cannot properly store a secret in a secured enclave which ensure that our secret +won't be accessed by any untrusted code (e.g. cross-site scripting)."); +#endif + } +} +#endif diff --git a/src/Uno.UWP/Security/Credentials/PasswordVault.reference.cs b/src/Uno.UWP/Security/Credentials/PasswordVault.reference.cs deleted file mode 100644 index 34bbf3011c36..000000000000 --- a/src/Uno.UWP/Security/Credentials/PasswordVault.reference.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using Uno; - -namespace Windows.Security.Credentials -{ - [NotImplemented] - /* sealed */ - partial class PasswordVault - { - [NotImplemented] - public PasswordVault() - { - throw new NotImplementedException(); - } - } -} diff --git a/src/Uno.UWP/Security/Credentials/PasswordVault.skia.cs b/src/Uno.UWP/Security/Credentials/PasswordVault.skia.cs deleted file mode 100644 index 34bbf3011c36..000000000000 --- a/src/Uno.UWP/Security/Credentials/PasswordVault.skia.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using Uno; - -namespace Windows.Security.Credentials -{ - [NotImplemented] - /* sealed */ - partial class PasswordVault - { - [NotImplemented] - public PasswordVault() - { - throw new NotImplementedException(); - } - } -} diff --git a/src/Uno.UWP/Security/Credentials/PasswordVault.wasm.cs b/src/Uno.UWP/Security/Credentials/PasswordVault.wasm.cs deleted file mode 100644 index 4169d38934ab..000000000000 --- a/src/Uno.UWP/Security/Credentials/PasswordVault.wasm.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using Uno; - -namespace Windows.Security.Credentials -{ - [NotImplemented] // Not really not implemented, but this will display an error directly in the IDE. - /* sealed */ - partial class PasswordVault - { - // This class is ** NOT ** sealed in order to allow projects for which the security limit described bellow is not - // really a concern (for instance if they are only storing an OAuth token) to inherit and provide they own - // implementation of 'IPersister'. - - private const string _notSupported = @"There is no way to properly persist secured content on WebAssembly. -At the opposite of other platforms, we cannot properly store a secret in a secured enclave which ensure that our secret -won't be accessed by any untrusted code (e.g. cross-site scripting)."; - - [NotImplemented] // Not really not implemented, but this will display an error directly in the IDE. - public PasswordVault() - { - throw new NotSupportedException(_notSupported); - } - } -}