diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e3bc4d..61f010a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ ## Change log +### Release 1.3.7 +**KPCLib** + - Added `GetOtpUrl()` + ### Release 1.3.6 **PassXYZLib** - Added LogFilePath diff --git a/KPCLib.nuspec b/KPCLib.nuspec index 18ce45d..14f70e2 100644 --- a/KPCLib.nuspec +++ b/KPCLib.nuspec @@ -2,7 +2,7 @@ KPCLib - 1.3.6.0 + 1.3.7.0 Roger Ye Roger Ye false diff --git a/KPCLib.xunit/PureOtpTests.cs b/KPCLib.xunit/PureOtpTests.cs new file mode 100644 index 0000000..771008a --- /dev/null +++ b/KPCLib.xunit/PureOtpTests.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using Xunit; + +using PureOtp; + +namespace KPCLib.xunit +{ + public class PureOtpTests + { + [Theory] + [InlineData("/kpclibpy/Database/Oracle")] + [InlineData("http://www.google.com/test?secret=JBSWY3DPEHPK3PXP")] + [InlineData("otpauth://totp/test01%3Abad_url_test%40gmail.com?secret=098")] + [InlineData("otpauth://totp/Google%3Apxentry_test%40gmail.com")] + [InlineData("otpauth://totp/Google%3Apxentry_test%40gmail.com?secret=JBSWY3DPEHPK3PXP")] + [InlineData("otpauth://totp/Google%3Apxentry_test%40gmail.com?secret=JBSWY3DPEHPK3PXP&issuer=Google")] + public void RawUrlTest(string rawUrl) + { + try + { + var otp = KeyUrl.FromUrl(rawUrl); + if (otp is Totp totp) + { + var url = new Uri(rawUrl); + Assert.True(true); + Debug.WriteLine($"{rawUrl} is a valid URL."); + } + else + { + Debug.WriteLine($"{rawUrl} is an invalid URL."); + } + } + catch (Exception ex) + { + Debug.WriteLine($"{ex}"); + Assert.False(false); + } + } + } +} diff --git a/KPCLib/KPCLib.csproj b/KPCLib/KPCLib.csproj index 8a78f93..5ff8350 100644 --- a/KPCLib/KPCLib.csproj +++ b/KPCLib/KPCLib.csproj @@ -6,7 +6,7 @@ Library true - 1.3.6 + 1.3.7 https://github.com/passxyz/KPCLib https://github.com/passxyz/KPCLib This is the build of KeePassLib in Xamarin Portable Class Library. Three platforms, UWP, Android and iOS, are supported and tested. @@ -15,8 +15,8 @@ PassXYZ Inc. en-US - 1.3.6.0 - 1.3.6.0 + 1.3.7.0 + 1.3.7.0 diff --git a/KPCLib/PwEntry.cs b/KPCLib/PwEntry.cs index e430b79..a4e201e 100644 --- a/KPCLib/PwEntry.cs +++ b/KPCLib/PwEntry.cs @@ -398,6 +398,11 @@ public void UpdateToken() } } + public string GetOtpUrl() + { + return CustomData.Get(PassXYZLib.PxDefs.PxCustomDataOtpUrl); + } + /// /// Update the OTP Url. /// If it is new, create a PxOtpUrl key in CustomData. diff --git a/PassXYZLib.nuspec b/PassXYZLib.nuspec index fe97a2f..7917c5d 100644 --- a/PassXYZLib.nuspec +++ b/PassXYZLib.nuspec @@ -2,7 +2,7 @@ PassXYZLib - 1.3.6.0 + 1.3.7.0 Roger Ye Roger Ye false diff --git a/PassXYZLib/PxDatabase.cs b/PassXYZLib/PxDatabase.cs index 1b8082d..4b420da 100644 --- a/PassXYZLib/PxDatabase.cs +++ b/PassXYZLib/PxDatabase.cs @@ -172,6 +172,8 @@ public class PxDatabase : PwDatabase public PwGroup CurrentGroup { get { + if (!IsOpen) { return null; } + if(RootGroup.Uuid == LastSelectedGroup || LastSelectedGroup.Equals(PwUuid.Zero)) { LastSelectedGroup = RootGroup.Uuid; @@ -203,9 +205,9 @@ public PwGroup CurrentGroup public string CurrentPath { get { - if(CurrentGroup == null) + if(CurrentGroup == null) { - return null; + return string.Empty; } else { @@ -262,9 +264,9 @@ public PxDatabase() : base() /// The password of data file public void Open(string filename, string password) { - if (filename == null || filename == String.Empty) + if (filename == null || filename == String.Empty) { Debug.Assert(false); throw new ArgumentNullException("filename"); } - if (password == null || password == String.Empty) + if (password == null || password == String.Empty) { Debug.Assert(false); throw new ArgumentNullException("password"); } var logger = new KPCLibLogger(); @@ -302,6 +304,8 @@ public void Open(PassXYZLib.User user) { if (user == null) { Debug.Assert(false); throw new ArgumentNullException("PassXYZLib.User"); } + if (user.Password == null || user.Password == String.Empty) + { Debug.Assert(false); throw new ArgumentNullException("Password"); } var logger = new KPCLibLogger(); @@ -328,7 +332,7 @@ public void Open(PassXYZLib.User user) } catch (PassXYZ.Services.InvalidDeviceLockException ex) { - try { cmpKey.AddUserKey(new KcpKeyFile(user.KeFilePath)); } + try { cmpKey.AddUserKey(new KcpKeyFile(user.KeyFilePath)); } catch (Exception exFile) { Debug.Write($"{exFile} in {ex}"); @@ -392,7 +396,7 @@ public bool ChangeMasterPassword(string newPassword, PassXYZLib.User user) } catch (PassXYZ.Services.InvalidDeviceLockException ex) { - try { cmpKey.AddUserKey(new KcpKeyFile(user.KeFilePath)); } + try { cmpKey.AddUserKey(new KcpKeyFile(user.KeyFilePath)); } catch (Exception exFile) { Debug.Write($"{exFile} in {ex}"); @@ -527,6 +531,34 @@ private void EnsureRecycleBin(ref PwGroup pgRecycleBin) else { Debug.Assert(pgRecycleBin.Uuid.Equals(this.RecycleBinUuid)); } } + /// + /// Remove RecycleBin before merge. RecycleBin should be kept locally and should not be merged. + /// + /// + /// + private bool DeleteRecycleBin(PwDatabase pwDb) + { + if (pwDb == null) { return false; } + + PwGroup pgRecycleBin = pwDb.RootGroup.FindGroup(pwDb.RecycleBinUuid, true); + + if (pgRecycleBin != null) + { + pwDb.RootGroup.Groups.Remove(pgRecycleBin); + pgRecycleBin.DeleteAllObjects(pwDb); + PwDeletedObject pdo = new PwDeletedObject(pgRecycleBin.Uuid, DateTime.UtcNow); + pwDb.DeletedObjects.Add(pdo); + Debug.WriteLine("DeleteRecycleBin successfully."); + return true; + } + else + { + Debug.WriteLine("DeleteRecycleBin failure."); + return false; + } + } + + /// /// Find an entry or a group. /// @@ -962,6 +994,58 @@ orderby e.LastModificationTime descending return resultsList; } + public bool Merge(string path, PwMergeMethod mm) + { + var pwImp = new PwDatabase(); + var ioInfo = IOConnectionInfo.FromPath(path); + + var compositeKey = MasterKey; + + KPCLibLogger swLogger = new KPCLibLogger(); + try + { + swLogger.StartLogging("Merge: Opening database ...", true); + pwImp.Open(ioInfo, compositeKey, swLogger); + swLogger.EndLogging(); + } + catch (Exception e) + { + Debug.WriteLine($"$Failed to open database: {e.Message}."); + return false; + } + + // We only merge, if these are the same database with different versions. + if (RootGroup.EqualsGroup(pwImp.RootGroup, (PwCompareOptions.IgnoreLastBackup | + PwCompareOptions.IgnoreHistory | PwCompareOptions.IgnoreParentGroup | + PwCompareOptions.IgnoreTimes | PwCompareOptions.PropertiesOnly), MemProtCmpMode.None)) + { + Debug.WriteLine($"Merge: Root group are the same. Merge method is {mm}."); + } + else + { + Debug.WriteLine($"Merge: Root group are different DBase={RootGroup}, pwImp={pwImp.RootGroup}."); + pwImp.Close(); + return false; + } + + try + { + // Need to remove RecycleBin first before merge. + DeleteRecycleBin(pwImp); + + MergeIn(pwImp, mm, swLogger); + DescriptionChanged = DateTime.UtcNow; + Save(swLogger); + pwImp.Close(); + } + catch (Exception exMerge) + { + Debug.WriteLine($"Merge failed {exMerge}"); + return false; + } + return true; + } + // The end of PxDatabase } diff --git a/PassXYZLib/User.cs b/PassXYZLib/User.cs index bdb81eb..4bbd56d 100644 --- a/PassXYZLib/User.cs +++ b/PassXYZLib/User.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Diagnostics; using System.IO; +using System.Runtime.CompilerServices; namespace PassXYZLib { @@ -80,6 +82,54 @@ public static string KeyFilePath } } + /// + /// The temporary file path. + /// + public static string TmpFilePath + { + get + { + string tmpPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "tmp"); + if (!Directory.Exists(tmpPath)) + { + Directory.CreateDirectory(tmpPath); + } + return tmpPath; + } + } + + /// + /// The backup file path. + /// + public static string BakFilePath + { + get + { + string bakPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "bak"); + if (!Directory.Exists(bakPath)) + { + Directory.CreateDirectory(bakPath); + } + return bakPath; + } + } + + /// + /// The icon file path. + /// + public static string IconFilePath + { + get + { + string iconPath = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "icons"); + if (!Directory.Exists(iconPath)) + { + _ = Directory.CreateDirectory(iconPath); + } + return iconPath; + } + } + /// /// Decode the username from filename /// @@ -119,22 +169,22 @@ public static string GetUserName(string fileName) } - public class User + public class User : INotifyPropertyChanged { - private string _username; + private string _username = string.Empty; /// /// PassXYZ uses the concept of user instead of data file to manage password database. /// This is because it is difficult to manage data file in mobile devices. The actual data file is encoded /// using base58 encoding with information such as key file or device lock enabled. /// - virtual public string Username + public virtual string Username { get => _username; set { _username = value; - if(_username == null) + if(string.IsNullOrEmpty(_username)) { IsDeviceLockEnabled = false; } @@ -169,7 +219,7 @@ public bool IsUserExist { get { - if (_username == null) + if (string.IsNullOrEmpty(_username)) { return false; } @@ -196,7 +246,7 @@ public bool IsKeyFileExist { get { - if (_username == null) + if (string.IsNullOrEmpty(_username)) { return false; } @@ -222,10 +272,7 @@ public bool IsKeyFileExist /// /// The date/time when this user was last accessed (read). /// - public DateTime LastAccessTime - { - get { return File.GetLastAccessTime(this.Path); } - } + public DateTime LastAccessTime => File.GetLastWriteTime(this.Path); /// /// Data file name. Converted Username to file name @@ -239,9 +286,9 @@ public string Path { get { - if (_username == null) + if (string.IsNullOrEmpty(_username)) { - return null; + return string.Empty; } return System.IO.Path.Combine(PxDataFile.DataFilePath, FileName); } @@ -255,7 +302,7 @@ public string KeyFileName { get { - if (_username == null) + if (string.IsNullOrEmpty(_username)) { return string.Empty; } @@ -274,13 +321,13 @@ public string KeyFileName /// /// Key file path /// - public string KeFilePath + public string KeyFilePath { get { - if (_username == null) + if (string.IsNullOrEmpty(_username)) { - return null; + return string.Empty; } return System.IO.Path.Combine(PxDataFile.KeyFilePath, KeyFileName); } @@ -289,12 +336,12 @@ public string KeFilePath /// /// Delete the current user /// - public void Delete() + public void Delete() { File.Delete(Path); - if (IsDeviceLockEnabled) + if (IsDeviceLockEnabled) { - File.Delete(KeFilePath); + File.Delete(KeyFilePath); } } @@ -306,9 +353,9 @@ public void Delete() /// Data file name private string GetFileName(bool isDeviceLockEnabled = false) { - if (_username == null) + if (string.IsNullOrEmpty(_username)) { - return null; + return string.Empty; } if (isDeviceLockEnabled) @@ -346,5 +393,30 @@ public User() { IsDeviceLockEnabled = false; } + + #region INotifyPropertyChanged + protected bool SetProperty(ref T backingStore, T value, + [CallerMemberName] string propertyName = "", + Action onChanged = null) + { + if (EqualityComparer.Default.Equals(backingStore, value)) + return false; + + backingStore = value; + onChanged?.Invoke(); + OnPropertyChanged(propertyName); + return true; + } + + public event PropertyChangedEventHandler PropertyChanged; + protected void OnPropertyChanged([CallerMemberName] string propertyName = "") + { + var changed = PropertyChanged; + if (changed == null) + return; + + changed.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + #endregion } }