From a51bfb102fda1f0f5a133b59eea3a807f296ceaa Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Wed, 5 Mar 2025 08:14:02 +0100 Subject: [PATCH 01/19] implements Samba support to close #82 --- src/Kp2aBusinessLogic/Io/SmbFileStorage.cs | 615 ++++++++++++++++++ .../Kp2aBusinessLogic.csproj | 1 + src/keepass2android-app/FileSelectHelper.cs | 47 +- .../drawable-xhdpi/ic_storage_smb.png | Bin 0 -> 5967 bytes .../Resources/layout/smbcredentials.axml | 51 ++ .../Resources/values/strings.xml | 6 + src/keepass2android-app/app/App.cs | 4 +- 7 files changed, 721 insertions(+), 3 deletions(-) create mode 100644 src/Kp2aBusinessLogic/Io/SmbFileStorage.cs create mode 100644 src/keepass2android-app/Resources/drawable-xhdpi/ic_storage_smb.png create mode 100644 src/keepass2android-app/Resources/layout/smbcredentials.axml diff --git a/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs b/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs new file mode 100644 index 00000000..b104ce10 --- /dev/null +++ b/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs @@ -0,0 +1,615 @@ +using System.Net; +using Android.Content; +using keepass2android; +using keepass2android.Io; +using KeePassLib.Serialization; +using SMBLibrary.Client; +using SMBLibrary; +using FileAttributes = SMBLibrary.FileAttributes; +using KeePassLib.Utility; +using Java.Nio.FileNio; + +namespace Kp2aBusinessLogic.Io +{ + public class SmbFileStorage : IFileStorage + { + public IEnumerable SupportedProtocols + { + get { yield return "smb"; } + } + + public bool UserShouldBackup + { + get { return false; } + } + + public void Delete(IOConnectionInfo ioc) + { + throw new NotImplementedException(); + } + + public bool CheckForFileChangeFast(IOConnectionInfo ioc, string previousFileVersion) + { + return false; + } + + public string GetCurrentFileVersionFast(IOConnectionInfo ioc) + { + return null; + } + + public struct SmbConnectionInfo + { + public string Host; + public string Username; + public string Password; + public string? Domain; + public string? Share; + public string? LocalPath; + + public static SmbConnectionInfo FromUrlAndCredentials(string url, string username, string password, string? domain) + { + string userDomain = username; + if (domain != null) + { + userDomain = domain + "\\" + username; + } + if (url.StartsWith("smb://")) + { + url = url.Substring(6); + } + + if (url.StartsWith("\\\\")) + { + url = url.Substring(2); + } + + url = url.Replace("\\", "/"); + + string fullPath = "smb://" + WebUtility.UrlEncode(userDomain) + ":" + WebUtility.UrlEncode(password) + "@" + url; + return new SmbConnectionInfo(new IOConnectionInfo() { Path = fullPath} ); + } + + + public SmbConnectionInfo(IOConnectionInfo ioc) + { + string fullpath = ioc.Path; + if (!fullpath.StartsWith("smb://")) + { + throw new Exception("Invalid smb path!"); + } + + fullpath = fullpath.Substring(6); + string[] authAndPath = fullpath.Split('@'); + if (authAndPath.Length != 2) + { + throw new Exception("Invalid smb path!"); + } + + string[] userAndPwd = authAndPath[0].Split(':'); + if (userAndPwd.Length != 2) + { + throw new Exception("Invalid smb path!"); + } + + string[] pathParts = authAndPath[1].Split('/'); + if (pathParts.Length < 1) + { + throw new Exception("Invalid smb path!"); + } + + Host = pathParts[0]; + if (pathParts.Length > 1) + { + Share = pathParts[1]; + } + LocalPath = String.Join("/", pathParts.Skip(2)); + if (LocalPath.EndsWith("/")) + { + LocalPath = LocalPath.Substring(0, LocalPath.Length - 1); + } + + Username = WebUtility.UrlDecode(userAndPwd[0]); + if (Username.Contains("\\")) + { + string[] domainAndUser = Username.Split('\\'); + Domain = domainAndUser[0]; + Username = domainAndUser[1]; + } + else Domain = null; + + Password = WebUtility.UrlDecode(userAndPwd[1]); + } + + public string ToPath() + { + string domainUser = Username; + if (Domain != null) + { + domainUser = Domain + "\\" + Username; + } + + return "smb://" + WebUtility.UrlEncode(domainUser) + ":" + WebUtility.UrlEncode(Password) + "@" + Host + + "/" + Share + "/" + LocalPath; + } + + public string GetPathWithoutCredentials() + { + return "smb://" + Host + "/" + Share + "/" + LocalPath; + } + + public string GetLocalSmbPath() + { + return LocalPath?.Replace("/", "\\") ?? ""; + } + + public SmbConnectionInfo GetParent() + { + SmbConnectionInfo parent = new SmbConnectionInfo + { + Host = Host, + Username = Username, + Password = Password, + Domain = Domain, + Share = Share + }; + string[] pathParts = LocalPath?.Split('/') ?? []; + if (pathParts.Length > 0) + { + parent.LocalPath = string.Join("/", pathParts.Take(pathParts.Length - 1)); + } + else + { + parent.LocalPath = ""; + parent.Share = ""; + } + + return parent; + } + + public string Stem() + { + return LocalPath?.Split('/').Last() ?? ""; + } + + + public SmbConnectionInfo GetChild(string childName) + { + SmbConnectionInfo child = new SmbConnectionInfo(); + child.Host = Host; + child.Username = Username; + child.Password = Password; + child.Domain = Domain; + if (string.IsNullOrEmpty(Share)) + { + child.Share = childName; + } + else + { + + child.Share = Share; + var pathPartsList = LocalPath?.Split('/').Where(p => !string.IsNullOrEmpty(p)).ToList() ?? []; + pathPartsList.Add(childName); + child.LocalPath = string.Join("/", pathPartsList); + } + + return child; + } + + public string ToDisplayString() + { + return "smb://" + Host + "/" + Share + "/" + LocalPath; + + } + } + + + class SmbConnection: IDisposable + { + public SmbConnection(SmbConnectionInfo info) + { + _isLoggedIn = false; + var isConnected = Client.Connect(info.Host, SMBTransportType.DirectTCPTransport); + if (!isConnected) + { + throw new Exception($"Failed to connect to SMB server {info.Host}"); + } + + var status = Client.Login(info.Domain ?? string.Empty, info.Username, info.Password); + if (status != NTStatus.STATUS_SUCCESS) + { + throw new Exception($"Failed to login to SMB as {info.Username}"); + } + + _isLoggedIn = true; + + if (!string.IsNullOrEmpty(info.Share)) + { + FileStore = Client.TreeConnect(info.Share, out status); + } + + } + + + + public readonly SMB2Client Client = new SMB2Client(); + + + public readonly ISMBFileStore? FileStore; + private readonly bool _isLoggedIn; + + public void Dispose() + { + FileStore?.Disconnect(); + + if (_isLoggedIn) + Client.Logoff(); + + if (!Client.IsConnected) return; + Client.Disconnect(); + + + } + } + + + + public Stream OpenFileForRead(IOConnectionInfo ioc) + { + + SmbConnectionInfo info = new SmbConnectionInfo(ioc); + using SmbConnection conn = new SmbConnection(info); + + if (conn.FileStore == null) + { + throw new Exception($"Failed to read to {info.GetPathWithoutCredentials()}"); + } + + + NTStatus status = conn.FileStore.CreateFile(out var fileHandle, out _, info.GetLocalSmbPath(), + AccessMask.GENERIC_READ | AccessMask.SYNCHRONIZE, FileAttributes.Normal, ShareAccess.Read, + CreateDisposition.FILE_OPEN, + CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT, null); + + if (status != NTStatus.STATUS_SUCCESS) + { + throw new Exception($"Failed to open file {info.LocalPath}"); + } + + var stream = new MemoryStream(); + long bytesRead = 0; + while (true) + { + status = conn.FileStore.ReadFile(out var data, fileHandle, bytesRead, (int)conn.Client.MaxReadSize); + if (status != NTStatus.STATUS_SUCCESS && status != NTStatus.STATUS_END_OF_FILE) + { + throw new Exception("Failed to read from file"); + } + + if (status == NTStatus.STATUS_END_OF_FILE || data.Length == 0) + { + break; + } + + bytesRead += data.Length; + stream.Write(data, 0, data.Length); + } + + stream.Seek(0, SeekOrigin.Begin); + return stream; + } + + + class SmbFileStorageWriteTransaction : IWriteTransaction + { + private bool UseFileTransaction { get; } + private readonly string _path; + private readonly string _uploadPath; + private readonly SmbFileStorage _fileStorage; + private MemoryStream? _memoryStream; + + public SmbFileStorageWriteTransaction(string path, SmbFileStorage fileStorage, bool useFileTransaction) + { + UseFileTransaction = useFileTransaction; + _path = path; + if (useFileTransaction) + { + _uploadPath = _path + Guid.NewGuid().ToString().Substring(0, 8) + ".tmp"; + } + else + { + _uploadPath = _path; + } + + + _fileStorage = fileStorage; + _memoryStream = null; + } + + public void Dispose() + { + _memoryStream?.Dispose(); + } + + public Stream OpenFile() + { + _memoryStream = new MemoryStream(); + return _memoryStream; + } + + public void CommitWrite() + { + _fileStorage.UploadData(new MemoryStream(_memoryStream!.ToArray()), new SmbConnectionInfo(new IOConnectionInfo() { Path = _uploadPath})); + if (UseFileTransaction) + { + SmbConnectionInfo uploadPath = new SmbConnectionInfo(new IOConnectionInfo() { Path = _uploadPath }); + SmbConnectionInfo finalPath = new SmbConnectionInfo(new IOConnectionInfo() { Path = _path }); + _fileStorage.RenameFile(uploadPath, finalPath); + } + + + } + } + + private void RenameFile(SmbConnectionInfo fromPath, SmbConnectionInfo toPath) + { + using var connection = new SmbConnection(fromPath); + + // Open existing file + var status = connection.FileStore!.CreateFile(out var handle, out _, fromPath.GetLocalSmbPath(), AccessMask.MAXIMUM_ALLOWED, 0, ShareAccess.Read, CreateDisposition.FILE_OPEN, CreateOptions.FILE_NON_DIRECTORY_FILE, null); + if (status != NTStatus.STATUS_SUCCESS) + throw new Exception($"Failed to open {fromPath.LocalPath} for renaming!"); + + FileRenameInformationType2 renameInfo = new FileRenameInformationType2 + { + FileName = toPath.GetLocalSmbPath(), + ReplaceIfExists = true + }; + connection.FileStore.SetFileInformation(handle, renameInfo); + connection.FileStore.CloseFile(handle); + + } + + private void UploadData(Stream data, SmbConnectionInfo uploadPath) + { + using var connection = new SmbConnection(uploadPath); + var status = connection.FileStore!.CreateFile(out var fileHandle, out _, uploadPath.GetLocalSmbPath(), AccessMask.GENERIC_WRITE | AccessMask.SYNCHRONIZE, FileAttributes.Normal, ShareAccess.None, CreateDisposition.FILE_CREATE, CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT, null); + if (status == NTStatus.STATUS_OBJECT_NAME_COLLISION) + status = connection.FileStore!.CreateFile(out fileHandle, out _, uploadPath.GetLocalSmbPath(), AccessMask.GENERIC_WRITE | AccessMask.SYNCHRONIZE, FileAttributes.Normal, ShareAccess.None, CreateDisposition.FILE_OVERWRITE, CreateOptions.FILE_NON_DIRECTORY_FILE | CreateOptions.FILE_SYNCHRONOUS_IO_ALERT, null); + if (status != NTStatus.STATUS_SUCCESS) + { + throw new Exception($"Failed to open {uploadPath.LocalPath} for writing!"); + } + + long writeOffset = 0; + while (data.Position < data.Length) + { + byte[] buffer = new byte[(int)connection.Client.MaxWriteSize]; + int bytesRead = data.Read(buffer, 0, buffer.Length); + if (bytesRead < (int)connection.Client.MaxWriteSize) + { + Array.Resize(ref buffer, bytesRead); + } + + status = connection.FileStore.WriteFile(out _, fileHandle, writeOffset, buffer); + if (status != NTStatus.STATUS_SUCCESS) + { + throw new Exception("Failed to write to file"); + } + writeOffset += bytesRead; + } + + } + + public IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction) + { + return new SmbFileStorageWriteTransaction(ioc.Path, this, useFileTransaction); + } + + public string GetFilenameWithoutPathAndExt(IOConnectionInfo ioc) + { + return UrlUtil.StripExtension( + UrlUtil.GetFileName(ioc.Path)); + + } + + public string GetFileExtension(IOConnectionInfo ioc) + { + return UrlUtil.GetExtension(ioc.Path); + } + + public bool RequiresCredentials(IOConnectionInfo ioc) + { + return false; + } + + public void CreateDirectory(IOConnectionInfo ioc, string newDirName) + { + throw new NotImplementedException(); + } + + private static IEnumerable ListShares(SmbConnection conn, SmbConnectionInfo parent) + { + foreach (string share in conn.Client.ListShares(out _)) + { + yield return new FileDescription() + { + CanRead = true, + CanWrite = true, + DisplayName = share, + IsDirectory = true, + Path = parent.GetChild(share).ToPath() + }; + } + + } + + public IEnumerable ListContents(IOConnectionInfo ioc) + { + List result = []; + SmbConnectionInfo info = new SmbConnectionInfo(ioc); + using SmbConnection conn = new SmbConnection(info); + if (string.IsNullOrEmpty(info.Share)) + { + var shares = ListShares(conn, info).ToList(); + return shares; + } + + NTStatus status = conn.FileStore!.CreateFile(out var directoryHandle, out _, info.GetLocalSmbPath(), AccessMask.GENERIC_READ, FileAttributes.Directory, ShareAccess.Read | ShareAccess.Write, CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null); + if (status == NTStatus.STATUS_SUCCESS) + { + conn.FileStore.QueryDirectory(out List fileList, directoryHandle, "*", FileInformationClass.FileDirectoryInformation); + foreach (var fi in fileList) + { + var fileDirectoryInformation = fi as FileDirectoryInformation; + if (fileDirectoryInformation == null) + continue; + + if (fileDirectoryInformation.FileName is "." or "..") + continue; + + var fileDescription = FileDescriptionConvert(ioc, fileDirectoryInformation); + + result.Add(fileDescription); + } + conn.FileStore.CloseFile(directoryHandle); + } + + return result; + } + + private FileDescription FileDescriptionConvert(IOConnectionInfo parentIoc, + FileDirectoryInformation fileDirectoryInformation) + { + FileDescription fileDescription = new FileDescription + { + CanRead = true, + CanWrite = true, + IsDirectory = (fileDirectoryInformation.FileAttributes & FileAttributes.Directory) != 0, + DisplayName = fileDirectoryInformation.FileName + }; + fileDescription.Path = CreateFilePath(parentIoc.Path, fileDescription.DisplayName); + fileDescription.LastModified = fileDirectoryInformation.LastWriteTime; + + fileDescription.SizeInBytes = fileDirectoryInformation.EndOfFile; + return fileDescription; + } + + public FileDescription GetFileDescription(IOConnectionInfo ioc) + { + SmbConnectionInfo info = new SmbConnectionInfo(ioc); + + if (string.IsNullOrEmpty(info.Share)) + { + return new FileDescription + { + CanRead = true, CanWrite = true, + DisplayName = info.Host, + IsDirectory = true, + Path = info.ToPath() + }; + } + + using SmbConnection conn = new SmbConnection(info); + NTStatus status = conn.FileStore!.CreateFile(out var directoryHandle, out _, info.GetParent().GetLocalSmbPath(), AccessMask.GENERIC_READ, FileAttributes.Directory, ShareAccess.Read | ShareAccess.Write, CreateDisposition.FILE_OPEN, CreateOptions.FILE_DIRECTORY_FILE, null); + if (status != NTStatus.STATUS_SUCCESS) throw new Exception($"Failed to query details for {info.LocalPath}"); + conn.FileStore.QueryDirectory(out List fileList, directoryHandle, info.Stem(), FileInformationClass.FileDirectoryInformation); + foreach (var fi in fileList) + { + var fileDirectoryInformation = fi as FileDirectoryInformation; + if (fileDirectoryInformation == null) + continue; + + if (fileDirectoryInformation.FileName is "." or "..") + continue; + + return FileDescriptionConvert(ioc, fileDirectoryInformation); + + + } + conn.FileStore.CloseFile(directoryHandle); + + throw new Exception($"Failed to query details for {info.LocalPath}"); + } + + public bool RequiresSetup(IOConnectionInfo ioConnection) + { + return false; + } + + public string IocToPath(IOConnectionInfo ioc) + { + return ioc.Path; + } + + public void StartSelectFile(IFileStorageSetupInitiatorActivity activity, bool isForSave, int requestCode, string protocolId) + { + activity.PerformManualFileSelect(isForSave, requestCode, protocolId); + + } + + public void PrepareFileUsage(IFileStorageSetupInitiatorActivity activity, IOConnectionInfo ioc, int requestCode, + bool alwaysReturnSuccess) + { + Intent intent = new Intent(); + activity.IocToIntent(intent, ioc); + activity.OnImmediateResult(requestCode, (int)FileStorageResults.FileUsagePrepared, intent); + } + + public void PrepareFileUsage(Context ctx, IOConnectionInfo ioc) + { + + } + + public void OnCreate(IFileStorageSetupActivity activity, Bundle savedInstanceState) + { + + } + + public void OnResume(IFileStorageSetupActivity activity) + { + + } + + public void OnStart(IFileStorageSetupActivity activity) + { + + } + + public void OnActivityResult(IFileStorageSetupActivity activity, int requestCode, int resultCode, Intent data) + { + + } + + public string GetDisplayName(IOConnectionInfo ioc) + { + return new SmbConnectionInfo(ioc).ToDisplayString(); + } + + public string CreateFilePath(string parent, string newFilename) + { + return new SmbConnectionInfo(new IOConnectionInfo() { Path = parent}).GetChild(newFilename).ToPath(); + } + + public IOConnectionInfo GetParentPath(IOConnectionInfo ioc) + { + SmbConnectionInfo connectionInfo = new SmbConnectionInfo(ioc); + return new IOConnectionInfo() { Path = connectionInfo.GetParent().ToPath() }; + } + + public IOConnectionInfo GetFilePath(IOConnectionInfo folderPath, string filename) + { + return new IOConnectionInfo() { Path = CreateFilePath(folderPath.Path, filename)}; + } + + public bool IsPermanentLocation(IOConnectionInfo ioc) + { + return true; + } + + public bool IsReadOnly(IOConnectionInfo ioc, OptionalOut reason = null) + { + return false; + } + } +} diff --git a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj index e518e764..7f880dae 100644 --- a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj +++ b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj @@ -13,6 +13,7 @@ + diff --git a/src/keepass2android-app/FileSelectHelper.cs b/src/keepass2android-app/FileSelectHelper.cs index 9e528808..249efd8c 100644 --- a/src/keepass2android-app/FileSelectHelper.cs +++ b/src/keepass2android-app/FileSelectHelper.cs @@ -22,6 +22,7 @@ using Keepass2android.Javafilestorage; #endif using KeePassLib.Serialization; using KeePassLib.Utility; +using static Kp2aBusinessLogic.Io.SmbFileStorage; namespace keepass2android { @@ -350,7 +351,47 @@ namespace keepass2android #endif } - private void ShowFtpDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath) + + private void ShowSmbDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath) + { +#if !EXCLUDE_JAVAFILESTORAGE && !NoNet + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity); + View dlgContents = activity.LayoutInflater.Inflate(Resource.Layout.smbcredentials, null); + if (!defaultPath.EndsWith(_schemeSeparator)) + { + SmbConnectionInfo ci = new SmbConnectionInfo(new IOConnectionInfo() { Path = defaultPath }); + + dlgContents.FindViewById(Resource.Id.smb_url).Text = ci.GetPathWithoutCredentials(); + dlgContents.FindViewById(Resource.Id.smb_domain).Text = ci.Domain; + dlgContents.FindViewById(Resource.Id.smb_user).Text = ci.Username; + dlgContents.FindViewById(Resource.Id.smb_password).Text = ci.Password; + + + } + builder.SetView(dlgContents); + builder.SetPositiveButton(Android.Resource.String.Ok, + (sender, args) => + { + string url = dlgContents.FindViewById(Resource.Id.smb_url).Text; + + string user = dlgContents.FindViewById(Resource.Id.smb_user).Text; + string password = dlgContents.FindViewById(Resource.Id.smb_password).Text; + string domain = dlgContents.FindViewById(Resource.Id.smb_domain).Text; + + string fullPath = SmbConnectionInfo.FromUrlAndCredentials(url, user, password, domain).ToPath(); + onStartBrowse(fullPath); + }); + EventHandler evtH = new EventHandler((sender, e) => onCancel()); + + builder.SetNegativeButton(Android.Resource.String.Cancel, evtH); + builder.SetTitle(activity.GetString(Resource.String.enter_smb_login_title)); + Dialog dialog = builder.Create(); + + dialog.Show(); +#endif + } + + private void ShowFtpDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath) { #if !NoNet MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity); @@ -476,7 +517,9 @@ namespace keepass2android ShowFtpDialog(_activity, ReturnFileOrStartFileChooser, ReturnCancel, defaultPath); else if ((defaultPath.StartsWith("http://")) || (defaultPath.StartsWith("https://"))) ShowHttpDialog(_activity, ReturnFileOrStartFileChooser, ReturnCancel, defaultPath); - else if (defaultPath.StartsWith("owncloud://")) + else if ((defaultPath.StartsWith("smb://"))) + ShowSmbDialog(_activity, ReturnFileOrStartFileChooser, ReturnCancel, defaultPath); + else if (defaultPath.StartsWith("owncloud://")) ShowOwncloudDialog(_activity, ReturnFileOrStartFileChooser, ReturnCancel, defaultPath, "owncloud"); else if (defaultPath.StartsWith("nextcloud://")) ShowOwncloudDialog(_activity, ReturnFileOrStartFileChooser, ReturnCancel, defaultPath, "nextcloud"); diff --git a/src/keepass2android-app/Resources/drawable-xhdpi/ic_storage_smb.png b/src/keepass2android-app/Resources/drawable-xhdpi/ic_storage_smb.png new file mode 100644 index 0000000000000000000000000000000000000000..40387b21b467a8595b2cd5a595c4650d9e971ab2 GIT binary patch literal 5967 zcmeHLc~lcu7Y~Rah$!HKh!O&#sFP%}CjtrqBuWt31Q$LhGZPq1GBFt;SW9t55Jjb6 zMX+^2q;(g?{(B&uPEE%sG=}-o5X4@BQ81dvDI9 zhlP&oW#eo^qtSZFf&;?AZ?N&|VFAA1-2Lbr_>@GKLCAlIU6;sU%c5ol4f?blgBF zX*5HvI&7-l3A$}_UEk6E$IQxS*VoDpA73%1@yl8wSL^RG&>?Bw4c4y-J>m)~RmGb< z7WIcVaNvPk3yvhz*-g(odilDSMeebsv-k0uCtv^E(mhi7ivp|mQYCGue_HCgsi{wG zmU;Ha&N;*)w#ArUKRQha-#u?IGWxFFpi_MRM>mSjJf6-iEgje2zTB+0TQ`AkYT`yI zzTm>D<>u&xXx&NY^qx(nMcsex-i*bLR68w?qtVP5gj5Nks)N|)>yeE#kaV{Gjf=LPMO6v{FxJeGJAzOXDy_YJY~`+4xRe)%mLO3Pj* zbSZO9)0aKIG4^a|dC4?MD(@?s%cEVUuh{X^g+c1vhWZDV$l71iihP&XHTFAOKcU*d zuWm_>-gv;auxC{N`+?nS%C4-Pd=Tk3wX`g0R7#(#lkx&@51TCRp_<((mPA7I~VqD+i zMiyNdG#nq5Rs7$O^1;R4@*(%%r0o1S|MI$;=ZxMNUugD4Z9iji!xcd-)>ZFHyuNbZ z%Vu>iZm3UVwJ^o(=DM&MKdg`4L8J94CP4RN*nE!Rr%1R{8@4v&}{8i~%Gg^Dr87+)K2g9HGmaSEXu z)GCc$V(?*@a3$csahS!Rn_Q?_K8zT77+p&0a5|64W3r(@1ChvK_}b9Db(m5T9uU+9 z0q%Sl2^6K3uvke+Nz5cJlhnntV6j-tVsls=4g@?PeX@o^43I|eZiHyX2*CBIj?hvB zsi7M&5d}Gi@?kK5o!*|GS}T`#z-#nvEC4=O21Lt(nQWF?&FVTsPX#6dkhX+=cZNO^ zEFxAot|#Z{P&_aZ*HG?VAuzOKzIKjIWhw_lS-1*U16MtW3cnT7xEwoXycN-CF(ug* zew<7m*5PhsES^wnO+i3!-U8MpkQAv;AhjLtZ=rXX-w8nx%0I}+XnDsS#QpsU?HrrZ z2VQcyB!EQc7-^6Nv`YsgQ38{6BqQO9#27~?;6i-9SOCE=&W8{VAA^)Uj4kFN7>0|* zT>xboJ%wmc+z1FlFbM$0hs6pZE=Ivbt_b3>Ib2A@Q3xTN!xN%B5l_q$i@HEe)Dd6} zAgZpp8KE!$g<@7Xg+W?!$mPhr`c6|M2oOOb#x}u19t`tE0zRL^=5j=WPN!&G*O~)|3A35p zR)RX)VyZw+VxR!7X_8S25h z#_vwl*gtgvt&vD|I6~n8-~!lem=CeV5Stgt;YzshNH#~(hSuRq>LN)}<-;&CMK=~` z3T8CR!M24V6w;2x;Y-+D37a3u7D-^XgwGwxhP%Q63J7KLUwIohDY`en2a*pa^c1N} zHVw7zToL%}*1OhQ6=B-4=ycQOBSFwsI#E-Nm?=|$)@mevNA7=Hqa7%EQb{EtI@~WF zbkB$3XFMC|VeQ)m9UQPmQt89nl;rhx4oL|(==LAb&iWgb>5)YIH+7*2h$bEf=TF-X z_Gk67VV+2#KsXSBaAAlo;wd17QVc^1F$WQgFa@6@=D(fsKU%Lak^@VCOnAaJrTrgD z#!#4xAutSK3LXye1OhQ6!eJi7#}H7n0#vrn?!&d|IJU^qWU*O(CHt7ycfS8==wm{ zdol1{&L7nEfv)#r;JutbsOul2%ck@F1FiuNeM#W$f*@x7bMR)uTrpu>0PRg$*14nL z&RQEBtEbU=IvOuCT3)^r7__2f@<6MGZr!bHgjKKL9yFQ-Bn$A1Ot|rI?Nn9YD3{7l z;uCA}!?|&r7jM~5EdOSB(B>fPC9D0vBr@E5-HtC!;9ar$l={)pT779z+M`dGIIU;f zy9RW3lm;$^S1-tT8J3x`vLiCmd!WB zB{}%dJ*;9Ld^Y`M^1|%zc0aO+u+9uOw|Qvy&HU!Cy<$Gsj@Z3s6rOXUY-(WlXu0Lx zTk!SAE<(sP{Fu}L?PP|eUA$E9aXx?FPc;w3!RfgFOvwq|ORtdAPwa19eSY3~=8r=U z6%HT-;x*;w3n$bwhk4xhs2YW4_OvaExl_1c+d;-t$7f!PPuWPk);ao1f;W2Uqj*tI zs7>dBilUQk7N;<$jvP8>@9K$9Av=dBjTSaJnK}xY=azo*i06km>N5A`R>ZbS>B1uk1(uKr`$78tA<dtt#e&XIk>i{H%N^w73B z)n&BqjO$rEE~S~4v0`{-#?-8WbsOuIR7*vaI?I!$T*)zOAEW71D+oX!7J zJ%yUNYXN!2YU8CRAAk8#Y_IRE16F*oj+<20goK9K9SbVERA8SlWRIi6lXGjfyiQvt ztE@}ESmCiKzxerxA?JOgye36Nm8?DNIwVf9aJ#N(X~q5QRdyRD=?|8VTp>HLuHo(m zw*s z`PcroUsqUb8X8KhN?!O}i+6(z!=;<%Pg~$}n?5=F;8*ERi08_=bCI2=)C2a@wLaIR zJ* + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/keepass2android-app/Resources/values/strings.xml b/src/keepass2android-app/Resources/values/strings.xml index f4b28c49..02de2d7b 100644 --- a/src/keepass2android-app/Resources/values/strings.xml +++ b/src/keepass2android-app/Resources/values/strings.xml @@ -504,7 +504,12 @@ Tell me more! No, I don\'t like it that much Enter WebDav login data: + Enter Samba login data: URL of folder or file (ex: mycloud.me.com/webdav/) + URL of folder or file (ex: 192.168.1.10/share/folder/) + Samba user\'s domain + Samba username + Enter OwnCloud login data: OwnCloud URL (ex: owncloud.me.com) Enter Nextcloud login data: @@ -542,6 +547,7 @@ FTP HTTP (WebDav) HTTPS (WebDav) + Samba (Windows Share) OwnCloud Nextcloud Dropbox diff --git a/src/keepass2android-app/app/App.cs b/src/keepass2android-app/app/App.cs index 00f73cf2..84b2d8a4 100644 --- a/src/keepass2android-app/app/App.cs +++ b/src/keepass2android-app/app/App.cs @@ -45,6 +45,7 @@ using keepass2android.database.edit; using keepass2android; using KeePassLib.Interfaces; using KeePassLib.Utility; +using Kp2aBusinessLogic.Io; #if !NoNet #if !EXCLUDE_JAVAFILESTORAGE using Android.Gms.Common; @@ -836,7 +837,8 @@ namespace keepass2android new SftpFileStorage(LocaleManager.LocalizedAppContext, this, IsFtpDebugEnabled()), new NetFtpFileStorage(LocaleManager.LocalizedAppContext, this, IsFtpDebugEnabled), new WebDavFileStorage(this), - new PCloudFileStorage(LocaleManager.LocalizedAppContext, this), + new SmbFileStorage(), + new PCloudFileStorage(LocaleManager.LocalizedAppContext, this), new PCloudFileStorageAll(LocaleManager.LocalizedAppContext, this), new MegaFileStorage(App.Context), //new LegacyWebDavStorage(this), From 5edf42254d7421fe1105c6cc39e65347f6ccd02c Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 1 Apr 2025 15:10:04 +0200 Subject: [PATCH 02/19] this is an experiment to use GnuTlsStream (the ftpcredentials.xml have some hardcoded credentials for a public FTP server for testing). Unfortunately, the app restarts when loading the native libraries for GnuTLS. --- src/Kp2aBusinessLogic/Io/NetFtpFileStorage.cs | 2 ++ src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj | 1 + src/keepass2android-app/Resources/layout/ftpcredentials.xml | 6 +++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Kp2aBusinessLogic/Io/NetFtpFileStorage.cs b/src/Kp2aBusinessLogic/Io/NetFtpFileStorage.cs index 06a2b115..b6226829 100644 --- a/src/Kp2aBusinessLogic/Io/NetFtpFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/NetFtpFileStorage.cs @@ -8,6 +8,7 @@ using Android.Content; using Android.OS; using FluentFTP; using FluentFTP.Exceptions; +using FluentFTP.GnuTLS; using KeePassLib; using KeePassLib.Serialization; using KeePassLib.Utility; @@ -139,6 +140,7 @@ namespace keepass2android.Io var settings = ConnectionSettings.FromIoc(ioc); FtpClient client = new FtpClient(); + client.Config.CustomStream = typeof(GnuTlsStream); client.Config.RetryAttempts = 3; if ((settings.Username.Length > 0) || (settings.Password.Length > 0)) client.Credentials = new NetworkCredential(settings.Username, settings.Password); diff --git a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj index e518e764..066e3729 100644 --- a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj +++ b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj @@ -10,6 +10,7 @@ + diff --git a/src/keepass2android-app/Resources/layout/ftpcredentials.xml b/src/keepass2android-app/Resources/layout/ftpcredentials.xml index 7a01e925..bc27b173 100644 --- a/src/keepass2android-app/Resources/layout/ftpcredentials.xml +++ b/src/keepass2android-app/Resources/layout/ftpcredentials.xml @@ -15,7 +15,7 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:singleLine="true" - android:text="" + android:text="ftp.dlptest.com" android:layout_weight="1" android:inputType="textWebEmailAddress" android:hint="@string/hint_sftp_host" /> @@ -46,7 +46,7 @@ android:layout_height="wrap_content" android:singleLine="true" android:inputType="textWebEmailAddress" - android:text="" + android:text="dlpuser" android:hint="@string/hint_username" /> From aec9441de4299475ae5849abd1239c16ed651175 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 8 Apr 2025 15:26:04 +0200 Subject: [PATCH 03/19] update FluentFTP --- src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj index 066e3729..af0e5d8b 100644 --- a/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj +++ b/src/Kp2aBusinessLogic/Kp2aBusinessLogic.csproj @@ -9,7 +9,7 @@ - + From ba7b02cd1ecd10033018ee4ef208b2b51a46e2f3 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 8 Apr 2025 15:46:38 +0200 Subject: [PATCH 04/19] remove testing credentials --- src/keepass2android-app/Resources/layout/ftpcredentials.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/keepass2android-app/Resources/layout/ftpcredentials.xml b/src/keepass2android-app/Resources/layout/ftpcredentials.xml index bc27b173..7a01e925 100644 --- a/src/keepass2android-app/Resources/layout/ftpcredentials.xml +++ b/src/keepass2android-app/Resources/layout/ftpcredentials.xml @@ -15,7 +15,7 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:singleLine="true" - android:text="ftp.dlptest.com" + android:text="" android:layout_weight="1" android:inputType="textWebEmailAddress" android:hint="@string/hint_sftp_host" /> @@ -46,7 +46,7 @@ android:layout_height="wrap_content" android:singleLine="true" android:inputType="textWebEmailAddress" - android:text="dlpuser" + android:text="" android:hint="@string/hint_username" /> From 48899ba9a0b15854c4ac7137fe65f62a5d214f43 Mon Sep 17 00:00:00 2001 From: PhilippC Date: Wed, 9 Jul 2025 01:34:39 +0200 Subject: [PATCH 05/19] New translations strings.xml (Portuguese, Brazilian) --- .../Resources/values-pt-rBR/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/keepass2android-app/Resources/values-pt-rBR/strings.xml b/src/keepass2android-app/Resources/values-pt-rBR/strings.xml index 0f52392e..df8d1a72 100644 --- a/src/keepass2android-app/Resources/values-pt-rBR/strings.xml +++ b/src/keepass2android-app/Resources/values-pt-rBR/strings.xml @@ -93,6 +93,8 @@ Desabilitar o Desbloqueio Biométrico Habilitar o Desbloqueio de Biométrico completo Habilitar o Desbloqueio de Biométrico para o QuickUnlock + Desbloqueio rápido baseado em senha não disponível + O desbloqueio rápido usando uma parte da sua senha está bloqueado porque o bloqueio da tela não está ativado no seu dispositivo. Esse comportamento é para protegê -lo caso alguém o veja entrando na sua chave de desbloqueio rápido. Desbloqueio por impressão digital falhou: Chave para desencriptação foi invalidada pelo sistema Android. Isto costuma acontecer se for adicionada uma nova impressão digital ao sistema ou se os parâmetros de segurança forem alterados. Desbloqueio da base de dados falhado: Chave composta inválida. O desbloqueio por impressão digital foi desativado porque aparentemente a chave mestra arquivada não é válida. Por favor, reative o Desbloquear com Impressão Digital para a nova senha mestre. @@ -321,6 +323,7 @@ Digite o código QuickUnlock: Desbloqueio Rápido! Fechar banco de dados + Ativar bloqueio de tela Habilitar Desbloqueio Rápido por padrão Define se o Desbloqueio Rápido está habilitado por padrão ou não. Proteger a exibição da base de dados @@ -722,6 +725,11 @@ Notificação para simplificar o acesso à entrada selecionada. Fechar banco de dados após três tentativas de desbloqueio biométrico falhadas. Alerta! Autenticação biométrica pode ser invalidada pelo Android, por exemplo: depois de adicionar uma nova digital nas configurações do seu dispositivo. Esteja certo de sempre saber como desbloquear com sua senha mestra! + + Estimativa aprimorada da qualidade da senha, considerando a maioria das senhas populares. + Block password-based QuickUnlock (for security reasons) if the device does not have a screen lock activated. + Update network security configuration to disable clear-text transfer. + Atualizado de Xamarin Android para .NET 8 Atualizado para o Target SDK 34 From 4b2d2ef7689aa9469ac179bdff43e4ea2906846c Mon Sep 17 00:00:00 2001 From: PhilippC Date: Wed, 9 Jul 2025 02:47:52 +0200 Subject: [PATCH 06/19] New translations strings.xml (Portuguese, Brazilian) --- src/keepass2android-app/Resources/values-pt-rBR/strings.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/keepass2android-app/Resources/values-pt-rBR/strings.xml b/src/keepass2android-app/Resources/values-pt-rBR/strings.xml index df8d1a72..bc8b05b7 100644 --- a/src/keepass2android-app/Resources/values-pt-rBR/strings.xml +++ b/src/keepass2android-app/Resources/values-pt-rBR/strings.xml @@ -727,8 +727,8 @@ Alerta! Autenticação biométrica pode ser invalidada pelo Android, por exemplo: depois de adicionar uma nova digital nas configurações do seu dispositivo. Esteja certo de sempre saber como desbloquear com sua senha mestra! Estimativa aprimorada da qualidade da senha, considerando a maioria das senhas populares. - Block password-based QuickUnlock (for security reasons) if the device does not have a screen lock activated. - Update network security configuration to disable clear-text transfer. + Bloqueia o desbloqueio rápido baseado em senha (por motivos de segurança) se o dispositivo não tiver uma trava de tela ativada. + Atualize a configuração de segurança de rede para desativar a transferência de texto não criptografado. Atualizado de Xamarin Android para .NET 8 From 60d8900473d0e8168847d6b919a247c9b25b5f0a Mon Sep 17 00:00:00 2001 From: PhilippC Date: Wed, 9 Jul 2025 11:40:00 +0200 Subject: [PATCH 07/19] New translations strings.xml (Greek) --- src/keepass2android-app/Resources/values-el/strings.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/keepass2android-app/Resources/values-el/strings.xml b/src/keepass2android-app/Resources/values-el/strings.xml index ba806ef4..46af533f 100644 --- a/src/keepass2android-app/Resources/values-el/strings.xml +++ b/src/keepass2android-app/Resources/values-el/strings.xml @@ -319,6 +319,7 @@ Εισάγετε τον κωδικό QuickUnlock: QuickUnlock! Κλείσιμο βάσης δεδομένων + Ενεργοποίηση κλειδώματος οθόνης Ενεργοποίηση QuickUnlock εξ ορισμού Ορίζει αν το QuickUnlock είναι ενεργό εξ ορισμού ή όχι. Προστασία προβολής βάσης δεδομένων From 0636f687aca69380d1a81a20b5b5d9eb0a421555 Mon Sep 17 00:00:00 2001 From: PhilippC Date: Wed, 9 Jul 2025 13:58:02 +0200 Subject: [PATCH 08/19] New translations strings.xml (Greek) --- .../Resources/values-el/strings.xml | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/keepass2android-app/Resources/values-el/strings.xml b/src/keepass2android-app/Resources/values-el/strings.xml index 46af533f..9600d615 100644 --- a/src/keepass2android-app/Resources/values-el/strings.xml +++ b/src/keepass2android-app/Resources/values-el/strings.xml @@ -93,6 +93,8 @@ Απενεργοποίηση βιομετρικού ξεκλειδώματος Ενεργοποίηση πλήρους βιομετρικού ξεκλειδώματος Ενεργοποίηση βιομετρικού ξεκλειδώματος για QuickUnlock + Το QuickUnlock βασισμένο σε συνθηματικό δεν είναι διαθέσιμο. + Το QuickUnlock που χρησιμοποιεί ένα τμήμα του συνθηματικού έχει αποκλειστεί, επειδή το κλείδωμα οθόνης είναι απενεργοποιημένο στη συσκευή σας. Αυτό σας προστατεύει σε περίπτωση που κάποιος σας παρακολουθεί όταν εισάγετε το QuickUnlock. Αποτυχία βιομετρικού ξεκλειδώματος. Το κλειδί αποκρυπτογράφησης ακυρώθηκε από το Android. Αυτό συμβαίνει συνήθως αν αλλάξει η βιομετρική αυθεντικοποίηση ή οι ρυθμίσεις ασφάλειας. Το ξεκλείδωμα της βάσης δεδομένων απέτυχε: άκυρο σύνθετο κλειδί. Το βιομετρικό ξεκλείδωμα απενεργοποιήθηκε επειδή το αποθηκευμένο πρωτεύον συνθηματικό δεν είναι πλέον έγκυρο. Ενεργοποιήστε ξανά το βιομετρικό ξεκλείδωμα για το νέο πρωτεύον συνθηματικό. @@ -251,7 +253,6 @@ Εξαίρεση παρόμοιων χαρακτήρων Προφίλ Εισάγετε το όνομα του προφίλ που θα αποθηκευτεί. Εισάγετε ένα υπάρχον όνομα για αντικατάσταση. - Αριθμός λέξεων συνθηματικής φράσης Διαχωριστικό λέξεων Συνθηματικό @@ -333,7 +334,7 @@ Απόκρυψη μήκους QuickUnlock Αν ενεργοποιηθεί, αποκρύπτει το μήκος του κωδικού QuickUnlock στη σχετική οθόνη. Κλειδί QuickUnlock από τη βάση δεδομένων - Εάν η ενεργή βάση δεδομένων περιέχει μια καταχώριση με τίτλο QuickUnlock στην ομάδα ρίζας της, ο κωδικός πρόσβασης αυτής της καταχώρισης χρησιμοποιείται ως κωδικός QuickUnlock. + Εάν η ενεργή βάση δεδομένων περιέχει μια καταχώριση με τίτλο QuickUnlock στην ομάδα ρίζας της, το συνθηματικό αυτής της καταχώρισης χρησιμοποιείται ως κωδικός QuickUnlock. Αποτυχία QuickUnlock: λανθασμένο συνθηματικό! Αποθήκευση συνημμένου Επιλέξτε πού θα αποθηκεύσετε το συνημμένο. @@ -428,7 +429,7 @@ Άνοιγμα ρυθμίσεων Το Keepass2Android μπορεί να εμφανίζει μια ειδοποίηση συστήματος ενόσω η βάση δεδομένων σας παραμένει ξεκλείδωτη. Για να λειτουργήσει αυτό, χορηγήστε την άδεια. Δεν με νοιάζει - Το αρχείο δεν είναι πλέον προσπελάσιμο στο Keepass2Android. Είτε διαγράφτηκε ή ανακηθηκαν τα δικαιώματα πρόσβασης. Δοκιμάστε να ξανα-ανοίξετε το αρχείο, πχ με Αλλαγή βάσης δεδομένων. + Το αρχείο δεν είναι πλέον προσπελάσιμο στο Keepass2Android. Είτε διαγράφτηκε ή ανακλήθηκαν τα δικαιώματα πρόσβασης. Δοκιμάστε να ξανανοίξετε το αρχείο, πχ με Αλλαγή βάσης δεδομένων. Προ-φόρτωση αρχείου βάσης δεδομένων Ξεκίνημα φόρτωσης στο παρασκήνιο ή λήψη του αρχείου της βάσης δεδομένων κατά την εισαγωγή του συνθηματικού. Συγχρονισμός μετά το QuickUnlock @@ -715,14 +716,19 @@ Ειδοποιήσεις Ειδοποίηση για απλοποιημένη πρόσβαση στην τρέχουσα καταχώριση. Κλείσιμο της βάσης δεδομένων μετά από 3 ανεπιτυχείς προσπάθειες βιομετρικού ξεκλειδώματος. - Προσοχή! Ο βιομετρικός έλεγχος ταυτότητας μπορεί να ακυρωθεί από το Android, π.χ. μετά την προσθήκη ενός νέου δακτυλικού αποτυπώματος στις ρυθμίσεις της συσκευής σας. Βεβαιωθείτε ότι ξέρετε πάντα πώς να ξεκλειδώσετε με τον κύριο κωδικό πρόσβασης! + Προσοχή! Ο βιομετρικός έλεγχος ταυτότητας μπορεί να ακυρωθεί από το Android, π.χ. μετά την προσθήκη ενός νέου δακτυλικού αποτυπώματος στις ρυθμίσεις της συσκευής σας. Βεβαιωθείτε ότι ξέρετε πάντα πώς να ξεκλειδώσετε με το κύριο συνθηματικό! + + Βελτιωμένη εκτίμηση της ποιότητας του συνθηματικού που λαμβάνει υπόψη τα πιο δημοφιλή συνθηματικά + Αποκλεισμός του QuickUnlock βασισμένου στο συνθηματικό (για λόγους ασφαλείας), εάν η συσκευή δεν έχει ενεργοποιημένο το κλείδωμα οθόνης. + Ενημερώστε τις ρυθμίσεις ασφαλείας δικτύου για να απενεργοποιήσετε τη μεταφορά ορατού κειμένου. + Αναβαθμίστηκε από Xamarin Android σε .ΝΕΤ 8 Αναβαθμίστηκε στοχεύοντας το SDK 34 Αναβαθμίστηκε σε διεπαφή χρήστη Material 3 Βελτιώστε την αυτόματη συμπλήρωση για να εργαστείτε με Compose Apps Διόρθωση ονόματος host στην αυτόματη συμπλήρωση και αναζήτηση - Διόρθωση προβλήματος με τη γεννήτρια κωδικού πρόσβασης + Διόρθωση προβλήματος με τη γεννήτρια συνθηματικών Αναβαθμίστηκε το OneDrive SDK στην έκδοση 5.68 @@ -748,12 +754,12 @@ Βελτίωση της υλοποίησης FTP και SFTP Προσθήκη πρόσβασης σε πλήρες pCloud Επιτρέπει την επιλογή γλώσσας συστήματος στο μενού της γλώσσας - Διόρθωση προβλήματος με την απομνημόνευση Keyfile + ερώτηση για τον τύπο του κωδικού πρόσβασης + Διόρθωση προβλήματος με την απομνημόνευση Keyfile + ερώτηση για το συνθηματικό Διόρθωση σφάλματος για απότομα κλεισίματα εφαρμογής και μη αναμενόμενες αποσυνδέσεις Μετάβαση σε νέα υλοποίηση SFTP, υποστηρίζοντας σύγχρονους αλγόριθμους δημόσιου κλειδιού όπως rsa-sha2-256 - Μαρκάρισμα κωδικών πρόσβασης ως ευαίσθητοι κατά την αντιγραφή στο πρόχειρο (Android 13) + Μαρκάρισμα των συνθηματικών ως ευαίσθητα κατά την αντιγραφή στο πρόχειρο (Android 13) Βελτιώσεις Autofill @@ -774,7 +780,7 @@ Προστέθηκε υποστήριξη για τη μορφή αρχείου KDBX 4.1 που εισήχθη στο KeePass 2.48 Προστέθηκε ο διάλογος ρύθμισης ρυθμίσεων TOTP για τις καταχωρίσεις - Βελτιωμένη γεννήτρια κωδικού πρόσβασης: Προστέθηκε υποστήριξη συνθηματικής φράσης, περισσότερες επιλογές, προφίλ και εκτίμηση ισχύος κωδικού πρόσβασης + Βελτιωμένη γεννήτρια συνθηματικών: Προστέθηκε υποστήριξη συνθηματικής φράσης, περισσότερες επιλογές, προφίλ και εκτίμηση ισχύος κωδικού πρόσβασης Βελτιώσεις στην αυτόματη συμπλήρωση (σταθερό αναδυόμενο παράθυρο δεν εμφανίζεται στο Chrome, καλύτερη υποστήριξη υποτομέα) Βελτιώσεις στην υλοποίηση του OneDrive: δεν υπάρχει πλέον όριο μεγέθους, ούτε περιττές αιτήσεις ελέγχου ταυτότητας Προστέθηκε επιλογή για να επιλέξετε το φωτεινό/σκούρο θέμα από τις ρυθμίσεις του συστήματος, συμπεριλαμβανομένων των νυχτερινών πλάνων, απαιτεί Android 10+ @@ -884,7 +890,7 @@ Δεν σχετίζεται το web domain %1$s με την εφαρμογή %2$s Το Keepass2Android ανίχνευσε βιομετρικό εξοπλισμό. Θέλετε να ενεργοποιήσετε βιομετρικό ξεκλείδωμα για αυτή τη βάση δεδομένων; Να επιτρέπονται οι ειδοποιήσεις - Το Keepass2Android μπορεί να εμφανίσει ειδοποιήσεις με κουμπιά για να αντιγράψετε τιμές, όπως κωδικούς πρόσβασης και TOTP στο πρόχειρο, ή για να εμφανιστεί το ενσωματωμένο πληκτρολόγιο. Αυτό είναι χρήσιμο για να μεταφέρετε τιμές σε άλλες εφαρμογές, χωρίς να μεταβείτε σε Keepass2Android επανειλημμένα. Θέλετε να ενεργοποιήσετε αυτές τις ειδοποιήσεις; + Το Keepass2Android μπορεί να εμφανίσει ειδοποιήσεις με κουμπιά για να αντιγράψετε τιμές, όπως συνθηματικά και TOTP στο πρόχειρο, ή για να εμφανιστεί το ενσωματωμένο πληκτρολόγιο. Αυτό είναι χρήσιμο για να μεταφέρετε τιμές σε άλλες εφαρμογές, χωρίς να μεταβείτε σε Keepass2Android επανειλημμένα. Θέλετε να ενεργοποιήσετε αυτές τις ειδοποιήσεις; Να επιτρέπονται οι ειδοποιήσεις Απενεργοποιήστε αυτό το χαρακτηριστικό Όχι τώρα @@ -910,4 +916,8 @@ Εναλλαγή πίσω όταν πατήσετε αποστολή / λήψη / ολοκλήρωση Η σάρωση QR κώδικα απαιτεί Google Play Services. Παρακαλώ εγκαταστήστε ή ενημερώστε τις Google Play Services στη συσκευή σας. Ρυθμίσεις πληκτρολογίου Android + Σημείωση: Έχετε ενεργοποιήσει στις Ρυθμίσεις - Εφαρμογή - Πρόσβαση στα συνθηματικά - Εναλλαγή πληκτρολογίου - Αυτόματη εναλλαγή πληκτρολογίου, αλλά φαίνεται ότι δεν έχει ρυθμιστεί σωστά. + Σημείωση: Έχετε ενεργοποιήσει στις Ρυθμίσεις - Εφαρμογή - Πρόσβαση στα συνθηματικά - Λειτουργία αυτόματης συμπλήρωσης - Αυτόματη συμπλήρωση για καταχωρίσεις TOTP. Αυτό μπορεί να προκαλέσει την εμφάνιση αυτού του παραθύρου όταν ανοίγετε μια καταχώριση με TOTP. + Σημείωση: Έχετε ενεργοποιήσει στις Ρυθμίσεις - Εφαρμογή - Ασφάλεια - Χρήση του ενσωματωμένου στο Keepass2Android πληκτρολογίου. Αυτό μπορεί να προκαλέσει την εμφάνιση αυτού του παραθύρου όταν ανοίγετε την εφαρμογή ή επεξεργάζεστε μια καταχώριση. + Σημείωση: Έχετε ενεργοποιήσει στις Ρυθμίσεις - Εφαρμογή - Πρόσβαση στα συνθηματικά - Εναλλαγή πληκτρολογίου - Εναλλαγή πληκτρολογίου. Αυτό μπορεί να προκαλέσει την εμφάνιση αυτού του παραθύρου κατά την αναζήτηση μιας καταχώρισης από το πρόγραμμα περιήγησης. From 16ff81cf8121dad1d0bb2a7257d0b240c682e3fc Mon Sep 17 00:00:00 2001 From: PhilippC Date: Mon, 14 Jul 2025 10:57:07 +0200 Subject: [PATCH 09/19] New translations strings.xml (Czech) --- src/keepass2android-app/Resources/values-cs/strings.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/keepass2android-app/Resources/values-cs/strings.xml b/src/keepass2android-app/Resources/values-cs/strings.xml index 4b2dc52c..553f382d 100644 --- a/src/keepass2android-app/Resources/values-cs/strings.xml +++ b/src/keepass2android-app/Resources/values-cs/strings.xml @@ -93,6 +93,8 @@ Zakázat Biometrické odemknutí Povolit plné Biometrické odemknutí Povolit Biometrické odemknutí pro Rychlé odemknutí + Rychlé odemknutí pomocí hesla není k dispozici + Funkce Rychlé odemknutí pomocí části hesla je blokována, protože na vašem zařízení není aktivováno zamykání obrazovky. Toto opatření slouží k vaší ochraně pro případ, že by někdo sledoval zadávání klíče pro Rychlé odemknutí. Biometrické odemknutí selhalo. Dešifrovací klíč byl zneplatněn systémem Android. To se obvykle stává, pokud došlo ke změně biometrického ověření nebo bylo změněno bezpečnostní nastavení. Odemknutí databáze se nezdařilo: neplatný složený klíč. Biometrické odemknutí bylo zakázáno z důvodu pravděpodobného vypršení platnosti hlavního hesla. Prosím, povolte znovu Biometrické odemknutí pro nové hlavní heslo. @@ -319,6 +321,7 @@ Vložte kód Rychlého odemknutí: Rychlé odemknutí Zavřít databázi + Povolit zámek obrazovky Ve výchozím nastavení povolit Rychlé odemknutí Určuje, zda je Rychlé odemknutí ve výchozím nastavení povoleno nebo ne. Chránit zobrazení databáze @@ -718,6 +721,11 @@ Upozornění pro usnadnění přístupu k momentálně zvolené položce. Zavřít databázi po třech neúspěšných pokusech o odemknutí. Varování! Biometrické ověření může být zneplatněno Androidem, např. po přidání nového otisku prstu do nastavení zařízení. Ujistěte se, že vždy víte, jak odemknout pomocí hlavního hesla! + + Vylepšené hodnocení kvality hesel založené na nejčastěji používaných heslech. + Pokud zařízení nemá aktivovaný zámek obrazovky, blokuje se funkce Rychlé odemknutí pomocí hesla (z bezpečnostních důvodů). + Aktualizovat konfiguraci zabezpečení sítě pro vypnutí přenosu textu v nešifrované podobě. + Aktualizováno z Xamarin Android na .net 8 Aktualizováno na Target SDK 34 From 3e6d86c206e52bbae6b83d50d06fddc57ba6be46 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 15 Jul 2025 09:10:41 +0200 Subject: [PATCH 10/19] correctly check if an item is a folder or file. closes https://github.com/PhilippC/keepass2android/issues/2589 --- .../javafilestorage/WebDavStorage.java | 5 ++- .../webdav/PropfindXmlParser.java | 38 +++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/java/JavaFileStorage/app/src/main/java/keepass2android/javafilestorage/WebDavStorage.java b/src/java/JavaFileStorage/app/src/main/java/keepass2android/javafilestorage/WebDavStorage.java index 26710fdf..fd3b6386 100644 --- a/src/java/JavaFileStorage/app/src/main/java/keepass2android/javafilestorage/WebDavStorage.java +++ b/src/java/JavaFileStorage/app/src/main/java/keepass2android/javafilestorage/WebDavStorage.java @@ -290,7 +290,10 @@ public class WebDavStorage extends JavaFileStorageBase { e.sizeInBytes = -1; } } - e.isDirectory = r.href.endsWith("/"); + + e.isDirectory = r.href.endsWith("/") || okprop.IsCollection; + + e.displayName = okprop.DisplayName; if (e.displayName == null) diff --git a/src/java/JavaFileStorage/app/src/main/java/keepass2android/javafilestorage/webdav/PropfindXmlParser.java b/src/java/JavaFileStorage/app/src/main/java/keepass2android/javafilestorage/webdav/PropfindXmlParser.java index 59ed2034..f424fb27 100644 --- a/src/java/JavaFileStorage/app/src/main/java/keepass2android/javafilestorage/webdav/PropfindXmlParser.java +++ b/src/java/JavaFileStorage/app/src/main/java/keepass2android/javafilestorage/webdav/PropfindXmlParser.java @@ -57,6 +57,8 @@ public class PropfindXmlParser public String DisplayName; public String LastModified; public String ContentLength; + + public boolean IsCollection; } public String status; public Prop prop; @@ -191,6 +193,8 @@ public class PropfindXmlParser continue; } String name = parser.getName(); + String namespace = parser.getNamespace(); + android.util.Log.d("PARSE", "4name = " + name); if (name.equals("getcontentlength")) @@ -200,6 +204,9 @@ public class PropfindXmlParser prop.LastModified = readText(parser); } else if (name.equals("displayname")) { prop.DisplayName = readText(parser); + } else if (name.equals("resourcetype") && namespace.equals(ns)) { + // We found the tag + prop.IsCollection = readResourceType(parser); } else { skip(parser); } @@ -208,6 +215,37 @@ public class PropfindXmlParser return prop; } + private boolean readResourceType(XmlPullParser parser) throws IOException, XmlPullParserException { + boolean isCollection = false; + parser.require(XmlPullParser.START_TAG, ns, "resourcetype"); + + while (parser.next() != XmlPullParser.END_TAG) { + + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + String name = parser.getName(); + String namespace = parser.getNamespace(); + + if (name.equals("collection") && namespace.equals(ns)) { + // We found , so it's a folder + isCollection = true; + // Since is usually an empty tag, just consume it. + // It might contain text if there's whitespace, so consume text then end tag. + if (parser.next() == XmlPullParser.TEXT) { + parser.nextTag(); // Move to the end tag + } + parser.require(XmlPullParser.END_TAG, ns, "collection"); + } else { + // Skip any other unexpected tags within + skip(parser); + } + } + // After reading all children of , ensure we are at its END_TAG + parser.require(XmlPullParser.END_TAG, ns, "resourcetype"); + return isCollection; + } + private void skip(XmlPullParser parser) throws XmlPullParserException, IOException { android.util.Log.d("PARSE", "skipping " + parser.getName()); From 913222d7cb799274f3ddf9ef569f92aa3c01787b Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 15 Jul 2025 11:07:40 +0200 Subject: [PATCH 11/19] allow chunked uploads, closes https://github.com/PhilippC/keepass2android/issues/2777 --- src/Kp2aBusinessLogic/IKp2aApp.cs | 4 ++ .../Io/DropboxFileStorage.cs | 5 +- src/Kp2aBusinessLogic/Io/JavaFileStorage.cs | 2 +- src/Kp2aBusinessLogic/Io/WebDavFileStorage.cs | 25 +++++++-- .../javafilestorage/WebDavStorage.java | 56 ++++++++++++++++++- src/keepass2android-app/FileSelectHelper.cs | 15 +++-- .../Resources/values/config.xml | 1 + .../Resources/values/strings.xml | 3 + .../Resources/xml/pref_app_file_handling.xml | 9 +++ src/keepass2android-app/app/App.cs | 23 ++++++-- 10 files changed, 125 insertions(+), 18 deletions(-) diff --git a/src/Kp2aBusinessLogic/IKp2aApp.cs b/src/Kp2aBusinessLogic/IKp2aApp.cs index 9a8357f8..6e4e9c1f 100644 --- a/src/Kp2aBusinessLogic/IKp2aApp.cs +++ b/src/Kp2aBusinessLogic/IKp2aApp.cs @@ -140,6 +140,10 @@ namespace keepass2android #endif + int WebDavChunkedUploadSize + { + get; + } } } \ No newline at end of file diff --git a/src/Kp2aBusinessLogic/Io/DropboxFileStorage.cs b/src/Kp2aBusinessLogic/Io/DropboxFileStorage.cs index 4ad64472..0f354a5c 100644 --- a/src/Kp2aBusinessLogic/Io/DropboxFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/DropboxFileStorage.cs @@ -15,7 +15,9 @@ namespace keepass2android.Io { get { return false; } } - } + + static public bool IsConfigured => !string.IsNullOrEmpty(AppKey) && !string.IsNullOrEmpty(AppSecret); + } public partial class DropboxAppFolderFileStorage: JavaFileStorage { @@ -29,6 +31,7 @@ namespace keepass2android.Io get { return false; } } + static public bool IsConfigured => !string.IsNullOrEmpty(AppKey) && !string.IsNullOrEmpty(AppSecret); } } diff --git a/src/Kp2aBusinessLogic/Io/JavaFileStorage.cs b/src/Kp2aBusinessLogic/Io/JavaFileStorage.cs index e526928e..2c39e594 100644 --- a/src/Kp2aBusinessLogic/Io/JavaFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/JavaFileStorage.cs @@ -123,7 +123,7 @@ namespace keepass2android.Io } - public IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction) + public virtual IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction) { return new JavaFileStorageWriteTransaction(IocToPath(ioc), useFileTransaction, this); } diff --git a/src/Kp2aBusinessLogic/Io/WebDavFileStorage.cs b/src/Kp2aBusinessLogic/Io/WebDavFileStorage.cs index 6cce6119..2d80e18a 100644 --- a/src/Kp2aBusinessLogic/Io/WebDavFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/WebDavFileStorage.cs @@ -6,10 +6,12 @@ using System.Text; using Android.App; using Android.Content; using Android.OS; +using Android.Preferences; using Android.Runtime; using Android.Views; using Android.Widget; #if !NoNet && !EXCLUDE_JAVAFILESTORAGE + using Keepass2android.Javafilestorage; #endif using KeePassLib.Serialization; @@ -19,9 +21,15 @@ namespace keepass2android.Io #if !NoNet && !EXCLUDE_JAVAFILESTORAGE public class WebDavFileStorage: JavaFileStorage { - public WebDavFileStorage(IKp2aApp app) : base(new Keepass2android.Javafilestorage.WebDavStorage(app.CertificateErrorHandler), app) - { - } + private readonly IKp2aApp _app; + private readonly WebDavStorage baseWebdavStorage; + + public WebDavFileStorage(IKp2aApp app, int chunkSize) : base(new Keepass2android.Javafilestorage.WebDavStorage(app.CertificateErrorHandler, chunkSize), app) + { + _app = app; + baseWebdavStorage = (WebDavStorage)Jfs; + + } public override IEnumerable SupportedProtocols { @@ -75,6 +83,15 @@ namespace keepass2android.Io } return base.IocToPath(ioc); } - } + + + public override IWriteTransaction OpenWriteTransaction(IOConnectionInfo ioc, bool useFileTransaction) + { + baseWebdavStorage.SetUploadChunkSize(_app.WebDavChunkedUploadSize); + return base.OpenWriteTransaction(ioc, useFileTransaction); + } + } + + #endif } \ No newline at end of file diff --git a/src/java/JavaFileStorage/app/src/main/java/keepass2android/javafilestorage/WebDavStorage.java b/src/java/JavaFileStorage/app/src/main/java/keepass2android/javafilestorage/WebDavStorage.java index fd3b6386..37960f2d 100644 --- a/src/java/JavaFileStorage/app/src/main/java/keepass2android/javafilestorage/WebDavStorage.java +++ b/src/java/JavaFileStorage/app/src/main/java/keepass2android/javafilestorage/WebDavStorage.java @@ -15,7 +15,10 @@ import com.burgstaller.okhttp.basic.BasicAuthenticator; import com.burgstaller.okhttp.digest.CachingAuthenticator; import com.burgstaller.okhttp.digest.DigestAuthenticator; + +import java.io.ByteArrayInputStream; import java.io.FileNotFoundException; +import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.io.UnsupportedEncodingException; @@ -44,23 +47,33 @@ import keepass2android.javafilestorage.webdav.DecoratedTrustManager; import keepass2android.javafilestorage.webdav.PropfindXmlParser; import keepass2android.javafilestorage.webdav.WebDavUtil; import okhttp3.MediaType; +import okhttp3.MultipartBody; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; import okhttp3.internal.tls.OkHostnameVerifier; +import okio.BufferedSink; public class WebDavStorage extends JavaFileStorageBase { private final ICertificateErrorHandler mCertificateErrorHandler; private Context appContext; - public WebDavStorage(ICertificateErrorHandler certificateErrorHandler) + int chunkSize; + + public WebDavStorage(ICertificateErrorHandler certificateErrorHandler, int chunkSize) { + this.chunkSize = chunkSize; mCertificateErrorHandler = certificateErrorHandler; } + public void setUploadChunkSize(int chunkSize) + { + this.chunkSize = chunkSize; + } + public String buildFullPath(String url, String username, String password) throws UnsupportedEncodingException { String scheme = url.substring(0, url.indexOf("://")); url = url.substring(scheme.length() + 3); @@ -189,11 +202,49 @@ public class WebDavStorage extends JavaFileStorageBase { try { ConnectionInfo ci = splitStringToConnectionInfo(path); + + RequestBody requestBody; + if (chunkSize > 0) + { + // use chunked upload + requestBody = new RequestBody() { + @Override + public MediaType contentType() { + return MediaType.parse("application/binary"); + } + + @Override + public void writeTo(BufferedSink sink) throws IOException { + try (InputStream in = new ByteArrayInputStream(data)) { + byte[] buffer = new byte[chunkSize]; + int read; + while ((read = in.read(buffer)) != -1) { + sink.write(buffer, 0, read); + sink.flush(); + } + } + } + + @Override + public long contentLength() { + return -1; // use chunked upload + } + }; + } + else + { + requestBody = new MultipartBody.Builder() + .addPart(RequestBody.create(data, MediaType.parse("application/binary"))) + .build(); + } + + Request request = new Request.Builder() .url(new URL(ci.URL)) - .put(RequestBody.create(MediaType.parse("application/binary"), data)) + .put(requestBody) .build(); + //TODO consider writeTransactional //TODO check for error @@ -522,3 +573,4 @@ public class WebDavStorage extends JavaFileStorageBase { } } + diff --git a/src/keepass2android-app/FileSelectHelper.cs b/src/keepass2android-app/FileSelectHelper.cs index 911b5300..3b480b7a 100644 --- a/src/keepass2android-app/FileSelectHelper.cs +++ b/src/keepass2android-app/FileSelectHelper.cs @@ -9,7 +9,9 @@ using System.Text; using Android.App; using Android.Content; +using Android.Content.Res; using Android.OS; +using Android.Preferences; using Android.Runtime; using Android.Views; using Android.Widget; @@ -319,7 +321,7 @@ namespace keepass2android View dlgContents = activity.LayoutInflater.Inflate(Resource.Layout.httpcredentials, null); if (!defaultPath.EndsWith(_schemeSeparator)) { - var webdavStorage = new Keepass2android.Javafilestorage.WebDavStorage(App.Kp2a.CertificateErrorHandler); + var webdavStorage = CreateWebdavStorage(activity); var connInfo = webdavStorage.SplitStringToConnectionInfo(defaultPath); dlgContents.FindViewById(Resource.Id.http_url).Text = connInfo.Url; dlgContents.FindViewById(Resource.Id.http_user).Text = connInfo.Username; @@ -339,7 +341,7 @@ namespace keepass2android string scheme = defaultPath.Substring(0, defaultPath.IndexOf(_schemeSeparator, StringComparison.Ordinal)); if (host.Contains(_schemeSeparator) == false) host = scheme + _schemeSeparator + host; - string httpPath = new Keepass2android.Javafilestorage.WebDavStorage(null).BuildFullPath(host, user, + string httpPath = CreateWebdavStorage(activity).BuildFullPath(host, user, password); onStartBrowse(httpPath); }); @@ -353,7 +355,12 @@ namespace keepass2android #endif } - private void ShowFtpDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath) + private static WebDavStorage CreateWebdavStorage(Activity activity) + { + return new WebDavStorage(App.Kp2a.CertificateErrorHandler, App.Kp2a.WebDavChunkedUploadSize); + } + + private void ShowFtpDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath) { #if !NoNet MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(activity); @@ -518,7 +525,7 @@ namespace keepass2android string scheme = defaultPath.Substring(0,defaultPath.IndexOf(_schemeSeparator, StringComparison.Ordinal)); if (host.Contains(_schemeSeparator) == false) host = scheme + _schemeSeparator + host; - string httpPath = new Keepass2android.Javafilestorage.WebDavStorage(null).BuildFullPath(WebDavFileStorage.Owncloud2Webdav(host, subtype == "owncloud" ? WebDavFileStorage.owncloudPrefix : WebDavFileStorage.nextcloudPrefix), user, + string httpPath = CreateWebdavStorage(activity).BuildFullPath(WebDavFileStorage.Owncloud2Webdav(host, subtype == "owncloud" ? WebDavFileStorage.owncloudPrefix : WebDavFileStorage.nextcloudPrefix), user, password); onStartBrowse(httpPath); }); diff --git a/src/keepass2android-app/Resources/values/config.xml b/src/keepass2android-app/Resources/values/config.xml index e5c3a97c..db3582ec 100644 --- a/src/keepass2android-app/Resources/values/config.xml +++ b/src/keepass2android-app/Resources/values/config.xml @@ -209,6 +209,7 @@ ShowUnlockedNotification true + 65536 PreloadDatabaseEnabled true diff --git a/src/keepass2android-app/Resources/values/strings.xml b/src/keepass2android-app/Resources/values/strings.xml index 6c4f3c87..70a93d18 100644 --- a/src/keepass2android-app/Resources/values/strings.xml +++ b/src/keepass2android-app/Resources/values/strings.xml @@ -729,6 +729,9 @@ Notification to simplify access to the currently selected entry. Close database after three failed biometric unlock attempts. Warning! Biometric authentication can be invalidated by Android, e.g. after adding a new fingerprint in your device settings. Make sure you always know how to unlock with your master password! + Chunk size for WebDav upload + Size of chunks when uploading to WebDav servers in bytes. Use 0 to disable chunked upload. + Improved password quality estimation by considering most popular passwords. diff --git a/src/keepass2android-app/Resources/xml/pref_app_file_handling.xml b/src/keepass2android-app/Resources/xml/pref_app_file_handling.xml index 98b85e90..196ab621 100644 --- a/src/keepass2android-app/Resources/xml/pref_app_file_handling.xml +++ b/src/keepass2android-app/Resources/xml/pref_app_file_handling.xml @@ -45,6 +45,14 @@ android:title="@string/UseFileTransactions_title" android:key="@string/UseFileTransactions_key" /> + + + \ No newline at end of file diff --git a/src/keepass2android-app/app/App.cs b/src/keepass2android-app/app/App.cs index db78a794..77c4df72 100644 --- a/src/keepass2android-app/app/App.cs +++ b/src/keepass2android-app/app/App.cs @@ -836,8 +836,8 @@ namespace keepass2android new AndroidContentStorage(LocaleManager.LocalizedAppContext), #if !EXCLUDE_JAVAFILESTORAGE #if !NoNet - new DropboxFileStorage(LocaleManager.LocalizedAppContext, this), - new DropboxAppFolderFileStorage(LocaleManager.LocalizedAppContext, this), + DropboxFileStorage.IsConfigured ? new DropboxFileStorage(LocaleManager.LocalizedAppContext, this) : null, + DropboxAppFolderFileStorage.IsConfigured ? new DropboxAppFolderFileStorage(LocaleManager.LocalizedAppContext, this): null, GoogleApiAvailability.Instance.IsGooglePlayServicesAvailable(LocaleManager.LocalizedAppContext)==ConnectionResult.Success ? new GoogleDriveFileStorage(LocaleManager.LocalizedAppContext, this) : null, GoogleApiAvailability.Instance.IsGooglePlayServicesAvailable(LocaleManager.LocalizedAppContext)==ConnectionResult.Success ? new GoogleDriveAppDataFileStorage(LocaleManager.LocalizedAppContext, this) : null, new OneDriveFileStorage(this), @@ -846,8 +846,8 @@ namespace keepass2android new OneDrive2AppFolderFileStorage(), new SftpFileStorage(LocaleManager.LocalizedAppContext, this, IsFtpDebugEnabled()), new NetFtpFileStorage(LocaleManager.LocalizedAppContext, this, IsFtpDebugEnabled), - new WebDavFileStorage(this), - new PCloudFileStorage(LocaleManager.LocalizedAppContext, this), + new WebDavFileStorage(this, WebDavChunkedUploadSize), + new PCloudFileStorage(LocaleManager.LocalizedAppContext, this), new PCloudFileStorageAll(LocaleManager.LocalizedAppContext, this), new MegaFileStorage(App.Context), //new LegacyWebDavStorage(this), @@ -1333,6 +1333,18 @@ namespace keepass2android } } + + + public int WebDavChunkedUploadSize + { + get + { + return int.Parse(PreferenceManager.GetDefaultSharedPreferences(LocaleManager.LocalizedAppContext) + .GetString("WebDavChunkedUploadSize_str", + LocaleManager.LocalizedAppContext.Resources + .GetInteger(Resource.Integer.WebDavChunkedUploadSize_default).ToString())); + } + } } @@ -1458,8 +1470,7 @@ namespace keepass2android { Kp2aLog.LogUnexpectedError(e.Exception); } - - } + } } From b83c4b377209753525c614a51bbc71b9ee0eb7e4 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 15 Jul 2025 11:51:15 +0200 Subject: [PATCH 12/19] improve Samba dialog. fix NoNet build --- src/Kp2aBusinessLogic/Io/SmbFileStorage.cs | 3 ++- src/keepass2android-app/FileSelectHelper.cs | 5 ++++- src/keepass2android-app/Resources/layout/smbcredentials.axml | 4 ++++ src/keepass2android-app/Resources/values/strings.xml | 1 + 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs b/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs index b104ce10..fb23d112 100644 --- a/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs @@ -8,7 +8,7 @@ using SMBLibrary; using FileAttributes = SMBLibrary.FileAttributes; using KeePassLib.Utility; using Java.Nio.FileNio; - +#if !NoNet namespace Kp2aBusinessLogic.Io { public class SmbFileStorage : IFileStorage @@ -613,3 +613,4 @@ namespace Kp2aBusinessLogic.Io } } } +#endif \ No newline at end of file diff --git a/src/keepass2android-app/FileSelectHelper.cs b/src/keepass2android-app/FileSelectHelper.cs index 542c3183..be3e4d57 100644 --- a/src/keepass2android-app/FileSelectHelper.cs +++ b/src/keepass2android-app/FileSelectHelper.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Net; #if !NoNet using FluentFTP; +using static Kp2aBusinessLogic.Io.SmbFileStorage; #endif using System.Text; @@ -22,7 +23,7 @@ using Keepass2android.Javafilestorage; #endif using KeePassLib.Serialization; using KeePassLib.Utility; -using static Kp2aBusinessLogic.Io.SmbFileStorage; + namespace keepass2android { @@ -384,6 +385,8 @@ namespace keepass2android string fullPath = SmbConnectionInfo.FromUrlAndCredentials(url, user, password, domain).ToPath(); onStartBrowse(fullPath); }); + builder.SetCancelable(false); + EventHandler evtH = new EventHandler((sender, e) => onCancel()); builder.SetNegativeButton(Android.Resource.String.Cancel, evtH); diff --git a/src/keepass2android-app/Resources/layout/smbcredentials.axml b/src/keepass2android-app/Resources/layout/smbcredentials.axml index 99c1445c..cbde2d3c 100644 --- a/src/keepass2android-app/Resources/layout/smbcredentials.axml +++ b/src/keepass2android-app/Resources/layout/smbcredentials.axml @@ -3,8 +3,12 @@ android:orientation="vertical" android:layout_width="fill_parent" android:layout_height="wrap_content" + android:padding="12dp" android:layout_margin="12dip" > + Entry notifications Notification to simplify access to the currently selected entry. Close database after three failed biometric unlock attempts. + If you want to access files from your personal Windows computer, use the computer name or local IP address as host URL; enter the computer name as domain and the Windows login (often the same as your Microsoft account) as username and password. Warning! Biometric authentication can be invalidated by Android, e.g. after adding a new fingerprint in your device settings. Make sure you always know how to unlock with your master password! From cfb5098b38bda05c1110a91684d048ead8b9ba0a Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 15 Jul 2025 12:18:45 +0200 Subject: [PATCH 13/19] add support for transactional upload --- .../javafilestorage/WebDavStorage.java | 66 ++++++++++++++++++- .../javafilestoragetest2/MainActivity.java | 8 +-- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/java/JavaFileStorage/app/src/main/java/keepass2android/javafilestorage/WebDavStorage.java b/src/java/JavaFileStorage/app/src/main/java/keepass2android/javafilestorage/WebDavStorage.java index 37960f2d..fb8ec597 100644 --- a/src/java/JavaFileStorage/app/src/main/java/keepass2android/javafilestorage/WebDavStorage.java +++ b/src/java/JavaFileStorage/app/src/main/java/keepass2android/javafilestorage/WebDavStorage.java @@ -1,6 +1,7 @@ package keepass2android.javafilestorage; import android.content.Context; +import java.math.BigInteger; import android.content.Intent; import android.net.Uri; @@ -27,6 +28,7 @@ import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; @@ -194,11 +196,73 @@ public class WebDavStorage extends JavaFileStorageBase { return client; } + public void renameOrMoveWebDavResource(String sourcePath, String destinationPath, boolean overwrite) throws Exception { + + ConnectionInfo sourceCi = splitStringToConnectionInfo(sourcePath); + ConnectionInfo destinationCi = splitStringToConnectionInfo(destinationPath); + + Request.Builder requestBuilder = new Request.Builder() + .url(new URL(sourceCi.URL)) + .method("MOVE", null) // "MOVE" is the HTTP method + .header("Destination", destinationCi.URL); // New URI for the resource + + // Add Overwrite header + if (overwrite) { + requestBuilder.header("Overwrite", "T"); // 'T' for true + } else { + requestBuilder.header("Overwrite", "F"); // 'F' for false + } + + Request request = requestBuilder.build(); + + Response response = getClient(sourceCi).newCall(request).execute(); + + // Check the status code + if (response.isSuccessful()) { + // WebDAV MOVE can return 201 (Created) if a new resource was created at dest, + // or 204 (No Content) if moved to a pre-existing destination (e.g., just renamed). + // A 200 OK might also be returned by some servers, though 201/204 are more common. + + } + else + { + throw new Exception("Rename/Move failed for " + sourceCi.URL + " to " + destinationCi.URL + ": " + response.code() + " " + response.message()); + } + } + + public static String generateRandomHexString(int length) { + SecureRandom secureRandom = new SecureRandom(); + // Generate enough bytes to ensure we can get the desired number of hex characters. + // Each byte converts to two hex characters. + // For 8 hex characters, we need 4 bytes. + int numBytes = (int) Math.ceil(length / 2.0); + byte[] randomBytes = new byte[numBytes]; + secureRandom.nextBytes(randomBytes); + + // Convert the byte array to a hexadecimal string + // BigInteger(1, randomBytes) treats the byte array as a positive number. + // toString(16) converts it to a hexadecimal string. + String hexString = new BigInteger(1, randomBytes).toString(16); + + // Pad with leading zeros if necessary (e.g., if the generated number is small) + // and then take the first 'length' characters. + // Using String.format to ensure leading zeros if the hexString is shorter. + return String.format("%0" + length + "d", new BigInteger(hexString, 16)).substring(0, length); + } @Override public void uploadFile(String path, byte[] data, boolean writeTransactional) throws Exception { + if (writeTransactional) + { + String randomSuffix = ".tmp." + generateRandomHexString(8); + uploadFile(path + randomSuffix, data, false); + renameOrMoveWebDavResource(path+randomSuffix, path, true); + return; + } + + try { ConnectionInfo ci = splitStringToConnectionInfo(path); @@ -245,8 +309,6 @@ public class WebDavStorage extends JavaFileStorageBase { .build(); - //TODO consider writeTransactional - //TODO check for error Response response = getClient(ci).newCall(request).execute(); checkStatus(response); diff --git a/src/java/JavaFileStorageTest-AS/app/src/main/java/com/crocoapps/javafilestoragetest2/MainActivity.java b/src/java/JavaFileStorageTest-AS/app/src/main/java/com/crocoapps/javafilestoragetest2/MainActivity.java index 1c81a604..5e8fcc35 100644 --- a/src/java/JavaFileStorageTest-AS/app/src/main/java/com/crocoapps/javafilestoragetest2/MainActivity.java +++ b/src/java/JavaFileStorageTest-AS/app/src/main/java/com/crocoapps/javafilestoragetest2/MainActivity.java @@ -548,7 +548,7 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag //storageToTest = new GoogleDriveAppDataFileStorage(); - /*storageToTest = new WebDavStorage(new ICertificateErrorHandler() { + storageToTest = new WebDavStorage(new ICertificateErrorHandler() { @Override public boolean onValidationError(String error) { return false; @@ -558,12 +558,12 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag public boolean alwaysFailOnValidationError() { return false; } - }); -*/ + }, 64*1024); + //storageToTest = new DropboxV2Storage(ctx,"4ybka4p4a1027n6", "1z5lv528un9nre8", !simulateRestart); //storageToTest = new DropboxFileStorage(ctx,"4ybka4p4a1027n6", "1z5lv528un9nre8", !simulateRestart); //storageToTest = new DropboxAppFolderFileStorage(ctx,"ax0268uydp1ya57", "3s86datjhkihwyc", true); - storageToTest = new GoogleDriveFullFileStorage(); + // storageToTest = new GoogleDriveFullFileStorage(); return storageToTest; From 13306a9076d74627d865b86920c7b261745796b3 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 15 Jul 2025 12:20:25 +0200 Subject: [PATCH 14/19] fix another build issue in NoNet --- src/Kp2aBusinessLogic/Io/SmbFileStorage.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs b/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs index fb23d112..11eec514 100644 --- a/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs +++ b/src/Kp2aBusinessLogic/Io/SmbFileStorage.cs @@ -1,4 +1,5 @@ -using System.Net; +#if !NoNet +using System.Net; using Android.Content; using keepass2android; using keepass2android.Io; @@ -8,7 +9,7 @@ using SMBLibrary; using FileAttributes = SMBLibrary.FileAttributes; using KeePassLib.Utility; using Java.Nio.FileNio; -#if !NoNet + namespace Kp2aBusinessLogic.Io { public class SmbFileStorage : IFileStorage From 057a7e2f7afd4dfeae0108860683c12dd1839978 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 15 Jul 2025 13:12:18 +0200 Subject: [PATCH 15/19] another nonet fix --- src/keepass2android-app/FileSelectHelper.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/keepass2android-app/FileSelectHelper.cs b/src/keepass2android-app/FileSelectHelper.cs index 3b480b7a..176f780c 100644 --- a/src/keepass2android-app/FileSelectHelper.cs +++ b/src/keepass2android-app/FileSelectHelper.cs @@ -354,12 +354,12 @@ namespace keepass2android dialog.Show(); #endif } - +#if !NoNet private static WebDavStorage CreateWebdavStorage(Activity activity) { return new WebDavStorage(App.Kp2a.CertificateErrorHandler, App.Kp2a.WebDavChunkedUploadSize); } - +#endif private void ShowFtpDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath) { #if !NoNet From d40b3dc15c2d36d86fe47e6e4c447940f320bcbe Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 15 Jul 2025 13:16:44 +0200 Subject: [PATCH 16/19] fix build issue with NoNet --- src/keepass2android-app/app/App.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/keepass2android-app/app/App.cs b/src/keepass2android-app/app/App.cs index da7499bd..e39cd2ae 100644 --- a/src/keepass2android-app/app/App.cs +++ b/src/keepass2android-app/app/App.cs @@ -46,10 +46,11 @@ using keepass2android; using keepass2android.Utils; using KeePassLib.Interfaces; using KeePassLib.Utility; -using Kp2aBusinessLogic.Io; + using Message = keepass2android.Utils.Message; #if !NoNet #if !EXCLUDE_JAVAFILESTORAGE +using Kp2aBusinessLogic.Io; using Android.Gms.Common; using Keepass2android.Javafilestorage; using GoogleDriveFileStorage = keepass2android.Io.GoogleDriveFileStorage; From 0d6f83757874d04b51a9d2b1111df5cacb35d133 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 15 Jul 2025 14:24:10 +0200 Subject: [PATCH 17/19] manifest and changelog for 1.14-pre0 --- src/keepass2android-app/ChangeLog.cs | 4 ++++ src/keepass2android-app/Manifests/AndroidManifest_net.xml | 4 ++-- src/keepass2android-app/Manifests/AndroidManifest_nonet.xml | 4 ++-- src/keepass2android-app/Resources/values/strings.xml | 6 ++++++ 4 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/keepass2android-app/ChangeLog.cs b/src/keepass2android-app/ChangeLog.cs index 1c1a3723..7fa17098 100644 --- a/src/keepass2android-app/ChangeLog.cs +++ b/src/keepass2android-app/ChangeLog.cs @@ -31,6 +31,10 @@ namespace keepass2android MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(ctx); builder.SetTitle(ctx.GetString(Resource.String.ChangeLog_title)); List changeLog = new List{ + #if !NoNet + BuildChangelogString(ctx, new List{Resource.Array.ChangeLog_1_14_net}, "1.14"), + #endif + BuildChangelogString(ctx, new List{Resource.Array.ChangeLog_1_13}, "1.13"), BuildChangelogString(ctx, new List{Resource.Array.ChangeLog_1_12 diff --git a/src/keepass2android-app/Manifests/AndroidManifest_net.xml b/src/keepass2android-app/Manifests/AndroidManifest_net.xml index 4cd481a7..3ea77407 100644 --- a/src/keepass2android-app/Manifests/AndroidManifest_net.xml +++ b/src/keepass2android-app/Manifests/AndroidManifest_net.xml @@ -1,7 +1,7 @@  diff --git a/src/keepass2android-app/Manifests/AndroidManifest_nonet.xml b/src/keepass2android-app/Manifests/AndroidManifest_nonet.xml index da6ad16c..1b734e8c 100644 --- a/src/keepass2android-app/Manifests/AndroidManifest_nonet.xml +++ b/src/keepass2android-app/Manifests/AndroidManifest_nonet.xml @@ -1,7 +1,7 @@  diff --git a/src/keepass2android-app/Resources/values/strings.xml b/src/keepass2android-app/Resources/values/strings.xml index 319e3a57..9ee59c17 100644 --- a/src/keepass2android-app/Resources/values/strings.xml +++ b/src/keepass2android-app/Resources/values/strings.xml @@ -739,6 +739,12 @@ Chunk size for WebDav upload Size of chunks when uploading to WebDav servers in bytes. Use 0 to disable chunked upload. + + WebDav improvements: Bug fix for listing folders; support for chunked uploads and transactions + Added support for Samba/Windows network shares + Updated FluentFTP and enabled support for GnuTLS stream, supporting TLS 1.3 and solving other FTPS issues + + Improved password quality estimation by considering most popular passwords. From f655a89be027ac4f82d26513d0adc5e1dd823d1e Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 15 Jul 2025 14:26:12 +0200 Subject: [PATCH 18/19] don't run the release workflow when creating a tag --- .github/workflows/release.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d7f73ce2..e2866a43 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,10 +3,9 @@ env: NAME: 'Release' on: - push: - tags: - - "v1.*" - workflow_dispatch: # Allows manual triggering of the workflow + # the workflow is always triggered manually. This allows to test the apks + # before publishing the release and not having a broken tag in the repo if that test fails. + workflow_dispatch: jobs: build-release: From 74ceea562bb98a3b482216aa938eb67df721c9d7 Mon Sep 17 00:00:00 2001 From: Philipp Crocoll Date: Tue, 22 Jul 2025 08:40:52 +0200 Subject: [PATCH 19/19] Make dialogs for SFTP, FTP, Mega, WebDav and Nextcloud not "cancelable", i.e. they don't close when tapping outside. --- src/keepass2android-app/FileSelectHelper.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/keepass2android-app/FileSelectHelper.cs b/src/keepass2android-app/FileSelectHelper.cs index 4766150c..d3937911 100644 --- a/src/keepass2android-app/FileSelectHelper.cs +++ b/src/keepass2android-app/FileSelectHelper.cs @@ -278,7 +278,8 @@ namespace keepass2android builder.SetNegativeButton(Android.Resource.String.Cancel, evtH); builder.SetTitle(activity.GetString(Resource.String.enter_sftp_login_title)); - Dialog dialog = builder.Create(); + builder.SetCancelable(false); + Dialog dialog = builder.Create(); dialog.Show(); #endif @@ -351,7 +352,8 @@ namespace keepass2android builder.SetNegativeButton(Android.Resource.String.Cancel, evtH); builder.SetTitle(activity.GetString(Resource.String.enter_http_login_title)); - Dialog dialog = builder.Create(); + builder.SetCancelable(false); + Dialog dialog = builder.Create(); dialog.Show(); #endif @@ -454,7 +456,8 @@ namespace keepass2android builder.SetNegativeButton(Android.Resource.String.Cancel, evtH); builder.SetTitle(activity.GetString(Resource.String.enter_ftp_login_title)); - Dialog dialog = builder.Create(); + builder.SetCancelable(false); + Dialog dialog = builder.Create(); dialog.Show(); #endif @@ -514,7 +517,8 @@ namespace keepass2android builder.SetNegativeButton(Android.Resource.String.Cancel, evtH); builder.SetTitle(activity.GetString(Resource.String.enter_mega_login_title)); - Dialog dialog = builder.Create(); + builder.SetCancelable(false); + Dialog dialog = builder.Create(); dialog.Show(); #endif @@ -579,7 +583,8 @@ namespace keepass2android builder.SetNegativeButton(Android.Resource.String.Cancel, evtH); builder.SetTitle(activity.GetString(subtype == "owncloud" ? Resource.String.enter_owncloud_login_title : Resource.String.enter_nextcloud_login_title)); - Dialog dialog = builder.Create(); + builder.SetCancelable(false); + Dialog dialog = builder.Create(); dlgContents.FindViewById(Resource.Id.owncloud_url).SetHint(subtype == "owncloud" ? Resource.String.hint_owncloud_url : Resource.String.hint_nextcloud_url); dialog.Show(); #endif