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);
- }
- }
-}