Compare commits

...

271 Commits

Author SHA1 Message Date
Philipp Crocoll
f4b5eee171 avoid potentiall null hints in autofill (#2488) 2024-01-02 16:26:02 +01:00
Philipp Crocoll
2e9400cf4d Manifest and changelog for 1.10-pre 2023-11-21 08:43:54 +01:00
Philipp Crocoll
d761f07fc9 change app-id of pCloud because the previous app couldn't be modified anymore (https://github.com/PhilippC/keepass2android/pull/2388#issuecomment-1799771771) but is not compatible with the latest sdk version 2023-11-21 08:07:39 +01:00
PhilippC
b18515dd8c Merge pull request #2456 from PhilippC/l10n_master2
New Crowdin updates
2023-11-21 08:05:10 +01:00
PhilippC
2677cae5e6 Merge pull request #2457 from hyproman/persist-ftp-debug-perf
Persist ftp debug preference
2023-11-21 07:11:33 +01:00
Philipp Crocoll
cb832c412f improve some source strings (removing superfluous characters) 2023-11-21 07:08:41 +01:00
PhilippC
97018b15f7 New translations strings.xml (Norwegian Bokmal) 2023-11-21 07:04:00 +01:00
PhilippC
6b06d4ba8d New translations strings.xml (Malayalam) 2023-11-21 07:03:58 +01:00
PhilippC
902fc6f6d3 New translations strings.xml (Azerbaijani) 2023-11-21 07:03:57 +01:00
PhilippC
d4fd8db455 New translations strings.xml (Norwegian Nynorsk) 2023-11-21 07:03:56 +01:00
PhilippC
9f1be03dc4 New translations strings.xml (Croatian) 2023-11-21 07:03:55 +01:00
PhilippC
7b863e115f New translations strings.xml (Persian) 2023-11-21 07:03:54 +01:00
PhilippC
06fa5a5fcd New translations strings.xml (Indonesian) 2023-11-21 07:03:53 +01:00
PhilippC
b1837468d7 New translations strings.xml (Galician) 2023-11-21 07:03:52 +01:00
PhilippC
0ffe6cda16 New translations strings.xml (Chinese Traditional) 2023-11-21 07:03:51 +01:00
PhilippC
0ba1e946d1 New translations strings.xml (Ukrainian) 2023-11-21 07:03:50 +01:00
PhilippC
ba2890cc80 New translations strings.xml (Turkish) 2023-11-21 07:03:49 +01:00
PhilippC
ee9750e689 New translations strings.xml (Swedish) 2023-11-21 07:03:47 +01:00
PhilippC
3f358fed38 New translations strings.xml (Serbian (Cyrillic)) 2023-11-21 07:03:46 +01:00
PhilippC
4eb7b4519e New translations strings.xml (Slovenian) 2023-11-21 07:03:45 +01:00
PhilippC
9b61f651c4 New translations strings.xml (Slovak) 2023-11-21 07:03:44 +01:00
PhilippC
c716fa0c12 New translations strings.xml (Russian) 2023-11-21 07:03:43 +01:00
PhilippC
013d69b520 New translations strings.xml (Portuguese) 2023-11-21 07:03:42 +01:00
PhilippC
fec2875e6a New translations strings.xml (Polish) 2023-11-21 07:03:41 +01:00
PhilippC
1a1036f7b8 New translations strings.xml (Dutch) 2023-11-21 07:03:40 +01:00
PhilippC
b9fcf8deda New translations strings.xml (Korean) 2023-11-21 07:03:39 +01:00
PhilippC
a9a88dbdbe New translations strings.xml (Japanese) 2023-11-21 07:03:37 +01:00
PhilippC
d3f505fb55 New translations strings.xml (Italian) 2023-11-21 07:03:36 +01:00
PhilippC
da10ebd2f4 New translations strings.xml (Hungarian) 2023-11-21 07:03:35 +01:00
PhilippC
daeee50e09 New translations strings.xml (Hebrew) 2023-11-21 07:03:34 +01:00
PhilippC
f602367a6c New translations strings.xml (Finnish) 2023-11-21 07:03:33 +01:00
PhilippC
db2ad49f36 New translations strings.xml (Basque) 2023-11-21 07:03:32 +01:00
PhilippC
a782843b29 New translations strings.xml (Greek) 2023-11-21 07:03:31 +01:00
PhilippC
e35babb8eb New translations strings.xml (German) 2023-11-21 07:03:30 +01:00
PhilippC
e6b296c0b9 New translations strings.xml (Danish) 2023-11-21 07:03:28 +01:00
PhilippC
44692afa98 New translations strings.xml (Catalan) 2023-11-21 07:03:27 +01:00
PhilippC
491912a6ab New translations strings.xml (Bulgarian) 2023-11-21 07:03:26 +01:00
PhilippC
5cb02e88bf New translations strings.xml (Arabic) 2023-11-21 07:03:25 +01:00
PhilippC
69ce92a7b7 New translations strings.xml (Spanish) 2023-11-21 07:03:24 +01:00
PhilippC
a09e2656be New translations strings.xml (Romanian) 2023-11-21 07:03:23 +01:00
PhilippC
445923e12c New translations strings.xml (Portuguese, Brazilian) 2023-11-21 07:03:21 +01:00
PhilippC
6499c97206 New translations strings.xml (Vietnamese) 2023-11-21 07:03:20 +01:00
PhilippC
41ef1900a1 New translations strings.xml (Chinese Simplified) 2023-11-21 07:03:19 +01:00
PhilippC
290f61d114 New translations strings.xml (Czech) 2023-11-21 07:03:18 +01:00
PhilippC
11f45c61e8 New translations strings.xml (Belarusian) 2023-11-21 07:03:16 +01:00
PhilippC
ac6df5d10f New translations strings.xml (French) 2023-11-21 07:03:15 +01:00
Philipp Crocoll
206ab3ac42 Merge branch 'master' of https://github.com/PhilippC/keepass2android 2023-11-13 09:55:58 +01:00
PhilippC
33847deb00 Merge pull request #2455 from PhilippC/l10n_master2
New Crowdin updates
2023-11-13 09:54:48 +01:00
PhilippC
baf9a29646 update links from codeplex to github, closes 2454 2023-11-13 09:52:42 +01:00
Philipp Crocoll
30d45e086c refactor SftpFileStorage creation 2023-11-13 09:18:39 +01:00
PhilippC
66166e44a0 New translations strings.xml (Czech) 2023-11-10 15:42:42 +01:00
PhilippC
8ace491d84 New translations strings.xml (French) 2023-11-10 13:52:04 +01:00
PhilippC
39deef4053 New translations strings.xml (Belarusian) 2023-11-08 10:39:24 +01:00
PhilippC
1faa0b06bd New translations strings.xml (French) 2023-11-07 19:30:59 +01:00
PhilippC
1eb1e1cb2b New translations strings.xml (Vietnamese) 2023-11-07 17:23:46 +01:00
PhilippC
d551969b04 New translations strings.xml (Vietnamese) 2023-11-07 14:30:33 +01:00
PhilippC
0f0c1ddbfd New translations strings.xml (Portuguese, Brazilian) 2023-11-07 11:06:02 +01:00
PhilippC
b46f2984a3 New translations strings.xml (Spanish) 2023-11-07 07:00:55 +01:00
PhilippC
cce1e2794e New translations strings.xml (Chinese Simplified) 2023-11-07 02:44:46 +01:00
Rick Brown
c19b8d2238 Fix nonet compilation 2023-11-06 18:09:19 -05:00
Rick Brown
141d2f3ddb Persist FTP/SFTP debug log preference
-Commit/apply FtpDebug_key state to persistent preferences
-Configure SFTP/JSch logging based on FtpDebug_key/LogFilename
 state on app startup
2023-11-06 16:55:36 -05:00
PhilippC
3d2ae980b7 New translations strings.xml (Slovenian) 2023-11-06 19:56:59 +01:00
PhilippC
a8f4fcde7b New translations strings.xml (Portuguese, Brazilian) 2023-11-06 11:41:33 +01:00
PhilippC
add8b2dad6 New translations strings.xml (Chinese Simplified) 2023-11-06 11:41:29 +01:00
PhilippC
50074d547f New translations strings.xml (Slovenian) 2023-11-06 11:41:25 +01:00
PhilippC
e6b425a30e New translations strings.xml (Slovak) 2023-11-06 11:41:23 +01:00
PhilippC
9af9d34d87 New translations strings.xml (Czech) 2023-11-06 11:41:10 +01:00
PhilippC
4fee92f591 New translations strings.xml (Spanish) 2023-11-06 11:41:05 +01:00
PhilippC
1b658f1c39 New translations strings.xml (French) 2023-11-06 11:41:04 +01:00
PhilippC
4dbd33ba97 Merge pull request #2455 from PhilippC/l10n_master2
New Crowdin updates
2023-11-06 10:34:07 +01:00
PhilippC
f8f18152c3 Merge pull request #2435 from hyproman/upgrade-fluentftp
Update to latest FluentFTP version
2023-11-06 10:33:41 +01:00
PhilippC
dbf5e46e94 New translations strings.xml (Croatian) 2023-11-06 09:24:19 +01:00
PhilippC
a23c1a2360 New translations strings.xml (Persian) 2023-11-06 09:24:18 +01:00
PhilippC
d2bd91ba6a New translations strings.xml (Portuguese, Brazilian) 2023-11-06 09:24:16 +01:00
PhilippC
95352ef0ee New translations strings.xml (Chinese Traditional) 2023-11-06 09:24:14 +01:00
PhilippC
24b8c27d26 New translations strings.xml (Chinese Simplified) 2023-11-06 09:24:13 +01:00
PhilippC
191b90d974 New translations strings.xml (Ukrainian) 2023-11-06 09:24:12 +01:00
PhilippC
21db4b612b New translations strings.xml (Turkish) 2023-11-06 09:24:10 +01:00
PhilippC
a23101b812 New translations strings.xml (Swedish) 2023-11-06 09:24:09 +01:00
PhilippC
8042470488 New translations strings.xml (Slovenian) 2023-11-06 09:24:07 +01:00
PhilippC
4bbec4367f New translations strings.xml (Slovak) 2023-11-06 09:24:06 +01:00
PhilippC
4b583cc0c0 New translations strings.xml (Russian) 2023-11-06 09:24:05 +01:00
PhilippC
cd07de56df New translations strings.xml (Portuguese) 2023-11-06 09:24:04 +01:00
PhilippC
962c4dbf63 New translations strings.xml (Polish) 2023-11-06 09:24:03 +01:00
PhilippC
09b56d85cf New translations strings.xml (Dutch) 2023-11-06 09:24:01 +01:00
PhilippC
8281888608 New translations strings.xml (Korean) 2023-11-06 09:24:00 +01:00
PhilippC
44d9456e20 New translations strings.xml (Japanese) 2023-11-06 09:23:59 +01:00
PhilippC
7b01e4494f New translations strings.xml (Italian) 2023-11-06 09:23:58 +01:00
PhilippC
fda68a1114 New translations strings.xml (Hungarian) 2023-11-06 09:23:57 +01:00
PhilippC
4849c089b3 New translations strings.xml (Finnish) 2023-11-06 09:23:55 +01:00
PhilippC
580668c5cb New translations strings.xml (Basque) 2023-11-06 09:23:54 +01:00
PhilippC
da8f1122e8 New translations strings.xml (Greek) 2023-11-06 09:23:53 +01:00
PhilippC
7d44518ac7 New translations strings.xml (German) 2023-11-06 09:23:52 +01:00
PhilippC
1a2c1267c4 New translations strings.xml (Danish) 2023-11-06 09:23:50 +01:00
PhilippC
85e0fe487f New translations strings.xml (Czech) 2023-11-06 09:23:49 +01:00
PhilippC
fa9a9f2602 New translations strings.xml (Catalan) 2023-11-06 09:23:48 +01:00
PhilippC
5c7d626f4b New translations strings.xml (Belarusian) 2023-11-06 09:23:46 +01:00
PhilippC
b3ce9c64b1 New translations strings.xml (Arabic) 2023-11-06 09:23:45 +01:00
PhilippC
dc809941e8 New translations strings.xml (Spanish) 2023-11-06 09:23:43 +01:00
PhilippC
b7d69c33c8 New translations strings.xml (French) 2023-11-06 09:23:42 +01:00
PhilippC
de765f3451 New translations strings.xml (Romanian) 2023-11-06 09:23:41 +01:00
Philipp Crocoll
1581d79666 Merge branch 'master' of https://github.com/PhilippC/keepass2android into iansw246/master 2023-11-06 09:02:01 +01:00
Philipp Crocoll
297fa267e5 fix bug in shared preference handling
fix issue with receiving meta data: previous implementation was repeatedly listing the full contents of pCloud recursively which is slow and might not work for large contents (https://github.com/pCloud/pcloud-sdk-java/issues/42)
2023-11-06 09:01:13 +01:00
PhilippC
77e2d67b6c Merge pull request #2299 from PhilippC/l10n_master2
New Crowdin updates
2023-11-04 18:23:55 +01:00
PhilippC
a3ba2d8367 Merge pull request #2344 from iansw246/master
Hide progress dialogs when user input dialog is showing
2023-11-04 18:23:44 +01:00
PhilippC
48d59aa0f6 New translations strings.xml (Spanish) 2023-11-02 21:37:26 +01:00
PhilippC
cff6595b79 New translations strings.xml (Spanish) 2023-11-02 20:29:56 +01:00
Rick Brown
798f633af7 Merge branch 'master' into upgrade-fluentftp 2023-11-02 15:19:28 -04:00
Rick Brown
f5681c4e62 Integrate FTP debug logging into UI toggle 2023-11-02 15:11:54 -04:00
PhilippC
690de2761c New translations strings.xml (Slovenian) 2023-11-02 18:29:48 +01:00
PhilippC
92238436d5 New translations strings.xml (Slovak) 2023-11-02 17:10:41 +01:00
PhilippC
9282e80938 New translations strings.xml (French) 2023-11-02 17:10:40 +01:00
PhilippC
588e203442 New translations strings.xml (Czech) 2023-11-02 15:24:59 +01:00
PhilippC
7e96055e0b New translations strings.xml (Portuguese, Brazilian) 2023-11-02 12:19:16 +01:00
Philipp Crocoll
c90d623d15 Merge branch 'bug-2378-pcloud-sdk-upgrade' into iansw246/master 2023-11-01 18:06:16 +01:00
PhilippC
86a03d8b9a New translations strings.xml (Czech) 2023-11-01 16:18:25 +01:00
Philipp Crocoll
17f7d1b8eb Merge branch 'target-sdk33' into iansw246/master 2023-11-01 12:24:34 +01:00
Philipp Crocoll
d3fecaf4e3 Merge branch 'master' into iansw246/master 2023-11-01 12:09:04 +01:00
PhilippC
03dee4f262 New translations strings.xml (Chinese Simplified) 2023-11-01 02:26:14 +01:00
PhilippC
2b502df566 Merge pull request #2434 from hyproman/bug-2423-ftp-contents-invisible
Bugfix for issue #2423 - FTP contents invisible
2023-10-31 09:14:42 +01:00
PhilippC
9ea064108c Merge pull request #2451 from PhilippC/target-sdk33
Update to target sdk33
2023-10-31 09:13:18 +01:00
Philipp Crocoll
682736d119 implement info text for notifications permission in GroupBaseActivity 2023-10-31 07:27:27 +01:00
Philipp Crocoll
150bd336d8 update to targetSdkVersion=33 again, this time with handling of notification permissions (https://developer.android.com/develop/ui/views/notifications/notification-permission)
For now, this is only implemented for entry activity, unlocked/QuickUnlock notifications not tested/implemented yet.
2023-10-31 06:46:13 +01:00
PhilippC
d13ee3d2ca New translations strings.xml (Slovenian) 2023-10-26 18:51:08 +02:00
PhilippC
99b5df4c94 New translations strings.xml (Portuguese, Brazilian) 2023-10-24 11:30:43 +02:00
PhilippC
9a70442d69 New translations strings.xml (Chinese Simplified) 2023-10-24 02:23:38 +02:00
Rick Brown
cd04050e57 Merge branch 'master' into upgrade-fluentftp 2023-10-23 17:54:59 -04:00
PhilippC
c13bb15fc0 New translations strings.xml (Norwegian Bokmal) 2023-10-23 11:20:35 +02:00
PhilippC
84939c70e1 New translations strings.xml (Malayalam) 2023-10-23 11:20:33 +02:00
PhilippC
2b108d9818 New translations strings.xml (Azerbaijani) 2023-10-23 11:20:31 +02:00
PhilippC
06f338fdd5 New translations strings.xml (Norwegian Nynorsk) 2023-10-23 11:20:30 +02:00
PhilippC
fa5e8c1656 New translations strings.xml (Croatian) 2023-10-23 11:20:29 +02:00
PhilippC
3652e2ee25 New translations strings.xml (Persian) 2023-10-23 11:20:28 +02:00
PhilippC
e8f3eb1bc8 New translations strings.xml (Indonesian) 2023-10-23 11:20:27 +02:00
PhilippC
233f612479 New translations strings.xml (Portuguese, Brazilian) 2023-10-23 11:20:25 +02:00
PhilippC
dc1e790ab5 New translations strings.xml (Galician) 2023-10-23 11:20:24 +02:00
PhilippC
bab77538c9 New translations strings.xml (Vietnamese) 2023-10-23 11:20:22 +02:00
PhilippC
09165af0a8 New translations strings.xml (Chinese Traditional) 2023-10-23 11:20:21 +02:00
PhilippC
4502d3d2bf New translations strings.xml (Chinese Simplified) 2023-10-23 11:20:20 +02:00
PhilippC
eb03d448d8 New translations strings.xml (Ukrainian) 2023-10-23 11:20:19 +02:00
PhilippC
7798ec8454 New translations strings.xml (Turkish) 2023-10-23 11:20:17 +02:00
PhilippC
7424bb324f New translations strings.xml (Swedish) 2023-10-23 11:20:16 +02:00
PhilippC
b18432add6 New translations strings.xml (Serbian (Cyrillic)) 2023-10-23 11:20:15 +02:00
PhilippC
e9a66d688c New translations strings.xml (Slovenian) 2023-10-23 11:20:14 +02:00
PhilippC
d9add0d5f6 New translations strings.xml (Slovak) 2023-10-23 11:20:12 +02:00
PhilippC
aed00420fc New translations strings.xml (Russian) 2023-10-23 11:20:11 +02:00
PhilippC
8dc546e640 New translations strings.xml (Portuguese) 2023-10-23 11:20:09 +02:00
PhilippC
c8f3d5f3e2 New translations strings.xml (Polish) 2023-10-23 11:20:08 +02:00
PhilippC
1f3786189b New translations strings.xml (Dutch) 2023-10-23 11:20:07 +02:00
PhilippC
d7bdde0585 New translations strings.xml (Korean) 2023-10-23 11:20:05 +02:00
PhilippC
f213f05477 New translations strings.xml (Japanese) 2023-10-23 11:20:04 +02:00
PhilippC
cb73144da7 New translations strings.xml (Italian) 2023-10-23 11:20:02 +02:00
PhilippC
689a1710c4 New translations strings.xml (Hungarian) 2023-10-23 11:20:01 +02:00
PhilippC
da116bbb4d New translations strings.xml (Hebrew) 2023-10-23 11:20:00 +02:00
PhilippC
2fd76ad28f New translations strings.xml (Finnish) 2023-10-23 11:19:58 +02:00
PhilippC
bdc7bf9cf6 New translations strings.xml (Basque) 2023-10-23 11:19:57 +02:00
PhilippC
b3ef4f817a New translations strings.xml (Greek) 2023-10-23 11:19:56 +02:00
PhilippC
3fb2f2e858 New translations strings.xml (German) 2023-10-23 11:19:54 +02:00
PhilippC
d8f60aa7f1 New translations strings.xml (Danish) 2023-10-23 11:19:53 +02:00
PhilippC
31f3a30a54 New translations strings.xml (Czech) 2023-10-23 11:19:52 +02:00
PhilippC
474b90f331 New translations strings.xml (Catalan) 2023-10-23 11:19:51 +02:00
PhilippC
fa0a52b328 New translations strings.xml (Bulgarian) 2023-10-23 11:19:50 +02:00
PhilippC
ccb6ece463 New translations strings.xml (Belarusian) 2023-10-23 11:19:48 +02:00
PhilippC
ffa33ed190 New translations strings.xml (Arabic) 2023-10-23 11:19:47 +02:00
PhilippC
c4923c57bf New translations strings.xml (Spanish) 2023-10-23 11:19:46 +02:00
PhilippC
2602bf7bee New translations strings.xml (French) 2023-10-23 11:19:45 +02:00
PhilippC
7df86fd134 New translations strings.xml (Romanian) 2023-10-23 11:19:43 +02:00
PhilippC
cf2f57b372 Merge pull request #2345 from AlexCherrypi/patch-1
remember keyprovider for "Password + Key file + Challenge-Response for KeePass XC"
2023-10-23 11:18:12 +02:00
Philipp Crocoll
7c2500af63 Merge branch 'master' of https://github.com/PhilippC/keepass2android 2023-10-23 11:14:28 +02:00
PhilippC
748a71bc03 Merge pull request #2386 from hyproman/bug-2366-WIP-ssh-custom-alg-cfg
Bug 2366 SSH Custom Algorithms Configuration
2023-10-23 11:03:02 +02:00
Philipp Crocoll
e3ae3233fe introduce file storage for pcloud with access to all files. current implementation doesn't work in my tests. 2023-10-23 09:46:43 +02:00
Philipp Crocoll
bc464b0eba Merge remote-tracking branch 'remotes/origin/master' into bug-2378-pcloud-sdk-upgrade 2023-10-17 08:14:38 +02:00
PhilippC
9b3d7250ec Merge pull request #2348 from lockland/master
Update pt-BR strings.xml
2023-10-17 08:08:31 +02:00
PhilippC
b04f7f6c81 Merge pull request #2358 from anthonyryan1/master
Losslessly compress PNG images
2023-10-17 08:07:42 +02:00
PhilippC
41151a184b Merge pull request #2409 from schlotter/fix-de-translation-keytransform
Update strings.xml (German)
2023-10-17 08:02:12 +02:00
PhilippC
9d9b24cb98 Merge pull request #2439 from hyproman/bug-2426-set-system-language
Bug 2426 Add "System language" as language option
2023-10-17 07:16:20 +02:00
Rick Brown
087e3f5931 Allow "System language" to be set as language option
Populate "System language" as a first class item in the language
preference list so that users can select it in scenarios where they
have previously selected a specific language, and wish to go back to
the default.
2023-10-11 17:23:09 -04:00
Rick Brown
c8abb4d76a Update to latest FluentFTP version 2023-10-08 18:23:11 -04:00
PhilippC
18f81e6927 New translations strings.xml (Chinese Traditional) 2023-10-07 23:45:25 +02:00
PhilippC
b8c094554a New translations strings.xml (Chinese Simplified) 2023-10-07 23:45:22 +02:00
PhilippC
1c6831bb78 New translations strings.xml (Greek) 2023-10-07 23:44:46 +02:00
PhilippC
a5e7bbc081 New translations strings.xml (Danish) 2023-10-07 23:44:42 +02:00
Rick Brown
be2218afcc Bugfix for issue #2423 - FTP contents invisible
Change working directory to target path and call GetListing on
current directory instead of calling GetListing on explicit path.

For some reason GetListing(path) does not always return the contents of
the directory. However, calling SetWorkingDirectory(path) followed by
GetListing(null, options) to list the contents of the working directory
does consistently work.

Similar behavior was confirmed using ncftp client. I suspect this is a
strange bug/nuance in the server's implementation of the LIST command?
2023-10-07 17:30:32 -04:00
PhilippC
32c1d2a379 New translations strings.xml (Finnish) 2023-09-15 21:45:50 +02:00
PhilippC
9c7182f85a New translations strings.xml (German) 2023-08-27 16:49:51 +02:00
Christian Schlotter
100ed6e58e Fix grammar 2023-08-27 15:47:03 +02:00
PhilippC
31abf68031 New translations strings.xml (German) 2023-08-27 15:46:35 +02:00
Christian Schlotter
a5bce53a12 Update strings.xml (German) 2023-08-26 23:28:21 +02:00
Rick Brown
489ed8e2b4 Convert literals to constants, add javadoc to resolver class 2023-08-09 20:58:06 -04:00
Rick Brown
d63e11b307 Add SFTP credentials documentation 2023-08-09 17:38:37 -04:00
Rick Brown
c9be806b01 Bump pcloud sdk to v1.8.1 (latest available in maven)
NOTE: pCloud auth does NOT currently work. The redirect_uri needs
      updating in the Kp2A pCloud app configuration.

See here for more info:
      https://github.com/pCloud/pcloud-sdk-java/issues/33
2023-07-25 17:12:17 -04:00
Rick Brown
0e9da69f47 Minor ssh debug logging changes
-Refactor the logger implementation to make creation more intuitive
-Remove SSH debug logging preference persistence (didn't work properly
 anyway, and probably not worth trying to fix)
2023-07-23 22:29:13 -04:00
Rick Brown
18ecfd5396 Integrate KEx/SHK functionality into JavaFileStorageTest-AS
-Re-organize SFTP Credentials dialog to be more space-efficient
-Add KEX and SHK algorithm spec fields (these get used to build the SFTP
 URI when connecting)
-Add CSV test fields/buttons for standalone testing of spec/config
 resolution
2023-07-23 22:29:13 -04:00
PhilippC
0fef5f0f8c New translations strings.xml (Spanish) 2023-07-22 08:25:42 +02:00
Rick Brown
83529dd3b5 Modify/specify KEX/SHK algorithms
-Implemented ability to manipulate server_host_key (SHK) via SFTP
 Credentials dialog (like KEX)
-Implemented a few basic wildcard/relative algorithm list manipulation
features:
   - Prepend to existing list: +alg_name
   - Append to end of existing list: alg_name+
   - Remove a specific value: -alg_name
   - Remove values matching prefix: -alg_prefix*
   - Remove values matching suffix: -*alg_suffix
   - Remove values matching substring: -*alg_substring*
   - Remove values matching prefix and suffix: -alg*name
   - Otherwise CSV of values completely replace original config values
2023-07-20 19:48:49 -04:00
Rick Brown
9204c4ca8f Add ssh config options to display URI 2023-07-19 22:11:34 -04:00
Rick Brown
46fdba1bfa SSH/SFTP: Allow kex algorithms to be explicitly set
-kex config overload, set via database connection settings
2023-07-19 19:38:00 -04:00
Rick Brown
006f5497e5 Merge branch 'bug-2366-ssh-debug-logging_master' into custom-sftp-private-key_patches 2023-07-19 17:12:53 -04:00
Rick Brown
da3665c25b Fix NoNet compilation error 2023-07-12 18:40:57 -04:00
Rick Brown
464fe43323 Add JSch (SFTP) debug logging
-App Settings->Log-File for Debugging->SFTP debug logging
-Logs to android log (logcat) if log file is not enabled
-Logs to Kp2a log file if it is enabled
-Logs are tagged as "KP2AJFS[JSch]"
-When enabled, logs ALL levels (DEBUG+).

NOTE: Sensitive SSH connection information may be logged!!
2023-07-12 17:03:39 -04:00
PhilippC
bded2394bb New translations strings.xml (Russian) 2023-07-10 00:19:43 +02:00
PhilippC
0fe2ca8238 New translations strings.xml (Russian) 2023-07-09 23:15:32 +02:00
PhilippC
ae33ca219f New translations strings.xml (Hungarian) 2023-07-08 15:24:04 +02:00
PhilippC
c16eeff130 Merge pull request #2365 from hyproman/bugfix-2350-sftp-fails-ipv6
Bugfix for issue #2350 - SFTP fails to connect to IPv6 address
2023-06-26 11:13:15 +02:00
AlexCherrypi
fb0f83c37a Update PasswordActivity.cs 2023-06-18 21:49:26 +02:00
Rick Brown
da5533ef3b Modified impl of bugfix #2350
URL encode/decode host parameter in SFTP URI

This version is slightly different than the original PR, given
this branch's changes to SftpStorage.buildFullPath().
2023-06-16 19:40:01 -04:00
Rick Brown
03ea073426 Bugfix for issue #2350 - SFTP fails to connect to IPv6 address
Since IPv6 addresses contain colons, they break the host:port URI
parsing logic, since "host" will have colons in it.

This fix adds URL encoding/decoding of the "host" parameter, thus
removing any possible colons in that parameter that could conflict
with the host:port separator.
2023-06-16 19:18:47 -04:00
PhilippC
681dfb6ded New translations strings.xml (Russian) 2023-06-10 23:46:19 +02:00
Anthony Ryan
cde5d31845 Losslessly compress PNG images
By using Efficient-Compression-Tool we were able to save 804 KB of 9.8 MB (8.2%)
without changing the visual appearance.
2023-06-10 09:15:03 -04:00
PhilippC
20f334f0d3 New translations strings.xml (Danish) 2023-06-09 10:00:23 +02:00
PhilippC
d8268d4f0f New translations strings.xml (Russian) 2023-06-02 23:03:27 +02:00
PhilippC
325e8a8e32 New translations strings.xml (Russian) 2023-06-02 22:05:45 +02:00
PhilippC
7e9e91da05 New translations strings.xml (Russian) 2023-06-02 20:52:43 +02:00
PhilippC
80eaf39f04 New translations strings.xml (Russian) 2023-06-02 19:56:15 +02:00
PhilippC
ddffdb48aa New translations strings.xml (Czech) 2023-06-01 11:20:16 +02:00
Sidney Souza
85709e4058 Update strings.xml
add some brazilian portuguese translation fixes
2023-05-31 10:50:51 -03:00
PhilippC
5a406fe5df New translations strings.xml (German) 2023-05-31 14:05:45 +02:00
AlexCherrypi
4e2603ae27 remember keyprovider for "Password + Key file + Challenge-Response for KeePass XC" "
extended "SetKeyProviderFromString()" to set _keyFile for "ChallengeXCKeyFile";
extended "InitializePasswordModeSpinner()" case 7 to remember key file location
2023-05-21 15:53:31 +02:00
ianjazz246
bcf980eed5 Make _activeProgressDialogs readonly 2023-05-20 10:58:19 -07:00
ianjazz246
05c94a3af8 Fix a few more tabs 2023-05-20 10:48:32 -07:00
ianjazz246
3526aa1889 Fix more tabs 2023-05-20 10:46:29 -07:00
ianjazz246
72a3b55341 Fix tab indentation 2023-05-20 10:42:09 -07:00
ianjazz246
b11d5e667e Hide progress dialogs when dialog requesting user input is showing 2023-05-20 10:19:14 -07:00
PhilippC
93a4529fe9 New translations strings.xml (Indonesian) 2023-05-10 14:15:38 +02:00
PhilippC
7582274903 New translations strings.xml (Catalan) 2023-05-08 21:28:00 +02:00
Philipp Crocoll
158349c005 mark camera as optional feature to make the app compatible with non-camera devices again, closes https://github.com/PhilippC/keepass2android/issues/2316 2023-04-21 04:44:08 +02:00
PhilippC
2fffe5988c New translations strings.xml (German) 2023-04-16 11:34:09 +02:00
PhilippC
3f6e51b126 Update Privacy-Policy.md 2023-04-13 04:36:57 +02:00
PhilippC
c0345d1309 Merge pull request #2303 from robellegate/add/pr-template
Add issue templates for bug report, feature request, and question
2023-04-11 06:07:31 +02:00
PhilippC
14f7e17fa4 New translations strings.xml (Czech) 2023-04-10 17:51:09 +02:00
PhilippC
05acba4309 New translations strings.xml (Czech) 2023-04-10 16:55:36 +02:00
Robert Ellegate
746dcd4c6b 🎨 style(issue_template): improve readability of bug report template
This commit improves the readability of the bug report template by changing the label of the "Version" information to "provide it below" instead of "provide it here". Additionally, the "Which version of Android are you on?" question is now a separate input field instead of a textarea, which makes it easier to answer. Finally, the markdown formatting of the instructions for finding the Android version is improved for better readability.
2023-04-09 11:20:44 -04:00
Robert Ellegate
37a6da5a3b 🎨 style(issue_template): add prefixes to issue titles
Add prefixes to issue titles to improve consistency and make it easier to identify the type of issue.

- `[FEAT]` for feature requests
- `[QUESTION]` for questions

Also, update the bug report template to move the instructions for finding the app version to a markdown section and remove the placeholder text from the version input field. This makes it clearer and easier to follow the instructions.
2023-04-09 11:16:01 -04:00
Robert Ellegate
1efe2e16a5 📝 chore(github): add issue templates for bug report, feature request, and question
The issue templates for bug report, feature request, and question have been added to the `.github/ISSUE_TEMPLATE` directory. These templates will help standardize the information provided in issues and make it easier for contributors to provide the necessary information. The bug report template includes checkboxes to ensure that the FAQ has been checked and open issues have been searched before submitting a new bug report. The feature request template is a simple template for suggesting new ideas for the project. The question template asks for the version of Keepass2Android being used to help with troubleshooting.
2023-04-08 13:37:07 -04:00
PhilippC
542984ca2f New translations strings.xml (Japanese) 2023-04-08 18:26:37 +02:00
PhilippC
f8746f69f8 New translations strings.xml (Slovak) 2023-04-08 11:16:05 +02:00
Philipp Crocoll
5cbddb4fcc explicitly remove READ_PHONE_STATE permission to close #2300; manifest for 1.09e-r7 2023-04-08 08:30:36 +02:00
Philipp Crocoll
b3a73f20d4 fix to potential crash when reloading the database. related to 4910c73a5e 2023-04-08 08:25:09 +02:00
PhilippC
53913e66ab New translations strings.xml (Polish) 2023-04-07 16:17:52 +02:00
Philipp Crocoll
badf99c20d Manifest for 1.09e-r6 2023-04-07 10:05:22 +02:00
Philipp Crocoll
b8318f7fa5 Merge branch 'master' of https://github.com/PhilippC/keepass2android 2023-04-07 09:19:43 +02:00
PhilippC
f0e30459a2 Merge pull request #2294 from PhilippC/l10n_master2
New Crowdin updates
2023-04-07 08:10:29 +02:00
Philipp Crocoll
e101ffb01e add regression test for the crash fixed in 9933fa1f9d 2023-04-07 08:10:10 +02:00
PhilippC
a05ef51d44 New translations strings.xml (Japanese) 2023-04-03 10:08:32 +02:00
PhilippC
8aacdf683b New translations strings.xml (Dutch) 2023-04-02 10:10:05 +02:00
PhilippC
0502efde14 New translations strings.xml (Dutch) 2023-04-02 09:00:39 +02:00
Philipp Crocoll
94ede3a696 output current Configuration during build 2023-03-31 08:01:25 +02:00
Philipp Crocoll
9933fa1f9d fix to potential crash in Autofill. Couldn't add a test yet, still waiting for corresponding Autofill structure. 2023-03-31 08:01:10 +02:00
Philipp Crocoll
4910c73a5e fix to potential crash when reloading database 2023-03-31 08:00:44 +02:00
Philipp Crocoll
cf222a2db1 remove Flavor from build-properties, adjust Manifest for debug build 2023-03-31 07:59:34 +02:00
PhilippC
a9ad3725dc Merge pull request #2265 from PhilippC/l10n_master2
New Crowdin updates
2023-03-31 07:58:27 +02:00
PhilippC
40d3fe1cd9 New translations strings.xml (Ukrainian) 2023-03-22 22:56:58 +01:00
PhilippC
1e90a52275 New translations strings.xml (Ukrainian) 2023-03-22 21:15:28 +01:00
PhilippC
8596edaa67 New translations strings.xml (Greek) 2023-03-22 10:59:35 +01:00
PhilippC
38f1aa4d3d New translations strings.xml (Greek) 2023-03-22 10:59:34 +01:00
PhilippC
984da3fd3b New translations strings.xml (Greek) 2023-03-22 09:53:14 +01:00
PhilippC
ed7138991d New translations strings.xml (German) 2023-03-21 21:45:15 +01:00
PhilippC
1e78527164 New translations strings.xml (Italian) 2023-03-21 00:17:53 +01:00
PhilippC
a6540b4462 New translations strings.xml (Japanese) 2023-03-20 13:56:48 +01:00
PhilippC
230b3941e8 New translations strings.xml (Japanese) 2023-03-20 12:42:13 +01:00
PhilippC
a76c43a800 New translations strings.xml (Chinese Traditional) 2023-03-19 11:40:16 +01:00
PhilippC
eedeeafd80 New translations strings.xml (Chinese Simplified) 2023-03-16 03:34:05 +01:00
PhilippC
ad3b1500bb New translations strings.xml (Finnish) 2023-03-12 23:25:28 +01:00
PhilippC
5f2a976fde New translations strings.xml (Finnish) 2023-03-12 22:20:54 +01:00
PhilippC
dd0becdfd8 New translations strings.xml (Slovenian) 2023-03-12 20:04:54 +01:00
PhilippC
cacd204ac2 New translations strings.xml (Chinese Simplified) 2023-03-10 04:06:30 +01:00
PhilippC
728fd2f8ae New translations strings.xml (Portuguese, Brazilian) 2023-03-09 21:38:11 +01:00
Rick Brown
5e265d1816 Backend:
-Generalize SFTP query param option map building
-Add "key" and "phrase" as SFTP query params
  key: custom private key name
  phrase: passphrase used to unlock key
-Add CRUD support for custom private keys
  Key files are stored in "user_keys" subdirectory
  File names are constructed by (sanitized) key name
  Basic support for private key content validation
-Existing and new key-related functionality moved into
  SftpPublicPrivateKeyUtils class

UI:
-Add custom private key support to SFTP Credentials dialog
  Add a new auth mode item (authModeSpinner)
  Add Spinner showing saved private key names, with an option
   to create a new one (top).
  Add Delete Private Key button; deletes the selected key
   in Spinner

Testing:
-Add custom private key CRUD support to JavaFileStorageTest app
 via file chooser SFTP Credentials panel
2023-02-19 20:26:39 -05:00
Rick Brown
83e77b2a31 Bugfix for #2223 - crash after import database by SFTP
Add FLAG_MUTABLE flag to PendingIntent call for API >= 31 to fix an
issue where trying to open an SFTP database (transition to choose a
remote database file) crashes and returns to the Open/New database
screen.
2023-02-19 19:52:05 -05:00
Rick Brown
893cf2b3c8 Get JavaFileStorage working in Android Studio
Resolve issue where AS would fail to import Android API jar
2023-02-19 19:52:05 -05:00
Rick Brown
15b3b76b27 Squashed commit of not-yet-approved PR #2038
https://github.com/PhilippC/keepass2android/pull/2038
User-defined SFTP connection timeout

The addition of SFTP query parameter options are needed
to support custom private key functionality.

Squashed commits from hyproman:sftp-conn-timeout:

commit 9c6b96e8198f1b912acdc1248af775f8fed58e1c
commit ebe59d9bc337a46bf0646677eb38b13ddde21f14
commit 69eb0bfd1a7010a2e442c36d10a16d1710c958de
commit 9394947c12bedb8667b7b94d0b1457f9e0451e18
2023-02-15 19:08:14 -05:00
1244 changed files with 4313 additions and 8269 deletions

53
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: Bug Report
description: Report a bug.
title: "[BUG] "
labels: bug
body:
- type: markdown
attributes:
value: |
Please check out the [FAQ section](https://github.com/PhilippC/keepass2android/blob/master/docs/Documentation.md#faq) and [search for open issues](https://github.com/PhilippC/keepass2android/issues?q=is%3Aopen+is%3Aissue+label%3Abug) first.
- type: checkboxes
attributes:
label: Checks
options:
- label: I have read the FAQ section, searched the open issues, and still think this is a new bug.
required: true
- type: textarea
id: bug
attributes:
label: "Describe the bug you encountered:"
validations:
required: true
- type: textarea
id: expected
attributes:
label: "Describe what you expected to happen:"
- type: markdown
attributes:
value: |
Please follow these steps to find your app version:
1. Click the **⁝** icon in the top right corner
2. Select **Settings**
3. Click **About**
4. Find the "Version" information and provide it below
- type: input
id: version
attributes:
label: "What version of Keepass2Android are you using?"
validations:
required: true
- type: markdown
attributes:
value: |
Please follow these steps to find your Android version:
1. Open your device's **Settings** app
2. Scroll down and select **About phone** or **About tablet**
3. Find the **Android version** section and provide it below
- type: input
id: os
attributes:
label: "Which version of Android are you on?"
validations:
required: true

View File

@@ -0,0 +1,8 @@
---
name: Feature Request
about: Suggest an idea for this project.
title: '[FEAT] '
labels: enhancement
assignees: ''
---

16
.github/ISSUE_TEMPLATE/question.md vendored Normal file
View File

@@ -0,0 +1,16 @@
---
name: Question
about: Ask a question about 'Keepass2Android'.
title: '[QUESTION] '
labels: question
assignees: ''
---
**What version of Keepass2Android are you using?**
Please follow these steps to find your app version:
1. Click the **⁝** icon in the top right corner
2. Select **Settings**
3. Click **About**
4. Find the "Version" information and provide it here:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

After

Width:  |  Height:  |  Size: 87 KiB

View File

@@ -3,13 +3,13 @@
Creating a plug-in for Keepass2Android or enabling your app to query credentials from Keepass2Android is pretty simple. Please follow the steps below to get started. In case you have any questions, please contact me.
## Preparations
First check out the source code and import the Keepass2AndroidPluginSDK from [https://keepass2android.codeplex.com/SourceControl/latest#src/java/Keepass2AndroidPluginSDK/](https://keepass2android.codeplex.com/SourceControl/latest#src/java/Keepass2AndroidPluginSDK/) into your workspace. You should be able to build this library project.
First check out the source code and import the Keepass2AndroidPluginSDK from [https://github.com/PhilippC/keepass2android/tree/master/src/java/Keepass2AndroidPluginSDK2](https://github.com/PhilippC/keepass2android/tree/master/src/java/Keepass2AndroidPluginSDK2/) into your workspace. You should be able to build this library project.
Now add a reference to the PluginSDK library from your existing app or add a new plug-in app and then add the reference.
## Authorization
Keepass2Android stores very sensitive user data and therefore implements a plug-in authorization scheme based on broadcasts sent between the plug-in and the host app (=Keepass2Android or Keepass2Android Offline). Before your app/plug-in gets any information from KP2A, the user will have to grant your app/plug-in access to KP2A. As not every app/plug-in requires access to all information, you must specify which scopes are required by your app. The implemented scopes can be found in [https://keepass2android.codeplex.com/SourceControl/latest#src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Strings.java](https://keepass2android.codeplex.com/SourceControl/latest#src/java/Keepass2AndroidPluginSDK/src/keepass2android/pluginsdk/Strings.java).
Keepass2Android stores very sensitive user data and therefore implements a plug-in authorization scheme based on broadcasts sent between the plug-in and the host app (=Keepass2Android or Keepass2Android Offline). Before your app/plug-in gets any information from KP2A, the user will have to grant your app/plug-in access to KP2A. As not every app/plug-in requires access to all information, you must specify which scopes are required by your app. The implemented scopes can be found in [https://github.com/PhilippC/keepass2android/tree/master/src/java/Keepass2AndroidPluginSDK2/src/keepass2android/pluginsdk/Strings.java](https://github.com/PhilippC/keepass2android/tree/master/src/java/Keepass2AndroidPluginSDK2/src/keepass2android/pluginsdk/Strings.java).
To tell Kp2a that you're a plug-in, you need to add a simple BroadcastReceiver like this:
@@ -55,8 +55,8 @@ These strings will be displayed to the user when KP2A asks if access should be g
## Modifying the entry view
You can add menu options for the full entry or for individual fields of the entry when displayed to the user. This is done, for example, by the QR plugin ([https://play.google.com/store/apps/details?id=keepass2android.plugin.qr](https://play.google.com/store/apps/details?id=keepass2android.plugin.qr)).
In addition, it is even possible to add new fields or modify existing fields. Please see the sample plugin "PluginA" in the KP2A repository for a simple example on how to do this:
[https://keepass2android.codeplex.com/SourceControl/latest#src/java/PluginA/src/keepass2android/plugina/PluginAActionReceiver.java](https://keepass2android.codeplex.com/SourceControl/latest#src/java/PluginA/src/keepass2android/plugina/PluginAActionReceiver.java)
In addition, it is even possible to add new fields or modify existing fields. Please see the sample plugin "PluginA" for a simple example on how to do this:
[https://github.com/PhilippC/keepass2android-sampleplugin/blob/main/src/keepass2android/plugina/PluginAAccessReceiver.java](https://github.com/PhilippC/keepass2android-sampleplugin/blob/main/src/keepass2android/plugina/PluginAAccessReceiver.java)
## Querying credentials
KP2A 0.9.4 adds a great opportunity for third party apps: Instead of prompting the user to enter credentials or a passphrase, the app should try to get the data from KP2A if it is installed: If the user grants (or previously granted) access for the app, KP2A will automatically retrieve the matching entry. User action is only required if the KP2A database is locked (user will usually unlock it with the short QuickUnlock code) or if no matching entry is found (user can then create a new entry or select an existing one. in the latter case KP2A will offer to add entry information so that the entry will be found automatically next time).

View File

@@ -18,6 +18,8 @@ Keepass2Android does not collect personal identifiable information. For debuggin
* **Internet** (Keepass2Android regular only): Required to allow the user to read/store password databases or key files on remote locations, e.g. Dropbox or via WebDav.
* **Contacts/Accounts** (Keepass2Android regular only): Required by the Google Drive SDK. If you want to access files on Google Drive, you are prompted to select one of the Google Accounts on your phone to use. The permission is required to query the list of Google accounts on the device. Keepass2Android does not access your personal contacts.
* **Storage**: Required to allow the user to read/store password databases or key files on the device locally.
* **Fingerprint**: Required if you want to use fingerprint unlock.
* **Fingerprint/Biometric**: Required if you want to use biometric unlock.
* **Vibrate**: Required by the built-in keyboard (vibrate on key press)
* **Camera**: Required for scanning OTP QR Codes
* **Foreground service**: Required to keep the app alive for QuickUnlock (so you don't need to enter your full master password repeatedly)

72
docs/SFTP-Credentials.md Normal file
View File

@@ -0,0 +1,72 @@
# SFTP Open/Create Database Credentials Documentation
## Basic Settings
* **Host** -- the hostname or IP address of the SFTP server to connect to
* **Port** -- the listening TCP port of the SFTP server to connect to (default: 22)
* **Username** -- the user/account name on the SFTP server that has access to the database
* **Initial directory** -- The path on the SFTP server that will be used as a starting point when choosing the remote database file
### Authentication Modes
#### Password
Authenticate using a password
* **Password** -- the password associated with **username** used to log into the SFTP server
#### K2A Private/Public Key
Authenticate using a private/public key pair that is generated internally by KP2A
* **SEND PUBLIC KEY...** -- Opens a standard Android "Share" screen containing the KP2A public key content. This allows for the public key to be sent via email, SMS, etc. This public key will need to be added to the SFTP server's user's "authorized keys" to allow private/public key authentication.
#### Custom Private Key
Authenticate using an existing private/public key pair. Use this option instead of *K2A Private/Public Key* if you wish to use a key pair that is already set up for this **username** on the SFTP server.
* **Selected private key** -- a combo-box containing a list of custom private keys that KP2A knows about, and a special `[Add new...]` option.
##### Add A New Private Key
* Select `[Add new...]`
* Enter a name for the new key in **New key name**
* Enter the private key contents (text) into **New key content**. **TIP:** The easiest way to accomplish this is to open the private key file in a text editor on the device, **Select All**, **Copy** to the clipboard, and paste it into **New key content**.
* Tap **SAVE PRIVATE KEY** to add the new key to the known list.
##### Use An Existing Private Key
* To use a private key that has already been imported into KP2A, simply select it from the list of keys.
##### Remove An Existing Key
* To remove a private that has been imported into KP2A, select it from the list and tap **DELETE PRIVATE KEY**.
A **key passphrase** can be supplied (if the key pair requires it)
## Advanced Settings
* **Connection timeout seconds** -- the number of seconds to wait for a connection to the server before giving up and considering the server as unavailable/unreachable
### Key Algorithm Manipulation
**NOTE: It is very rare that these fields need to be (or should be) specified. Use at your own risk!**
* **Key Exchange (KEX) Algorithm(s)** -- Explicitly set or modify the ordered list of Key Exchange algorithms that the SSH/SFTP client library will try to use
* **Server Host Key Algorithm(s)** -- Explicitly set or modify the ordered list of Server Host Key algorithms that the SSH/SFTP client library will try to use
#### How It Works
The SSH/SFTP client has a pre-defined ordered list of algorithm names that it will use to negotiate with the server to handle key exchange. In rare cases there are compatibility issues where Android OS has not properly implemented full support for algorithms listed. This can result in a connection failure, even if there is a suitable algorithm available (of lesser priority in the list).
The fields listed above allow these lists to be manipulated in the following ways to overcome/workaround such problems. The value is a comma-separated list of "algorithm spec" entries. Specs can be one of:
* Direct replacement of values -- Ex: `primary_alg,secondary_alg`
* Prepend to values -- Ex: `+try_first_alg`
* Append to values -- Ex: `try_last_alg+`
* Remove a specific value -- Ex: `-bad_alg`
* Remove values matching prefix -- Ex: `-bad_starting_with*`
* Remove values matching suffix -- Ex: `-*bad_ending_with`
* Remove values matching substring -- Ex: `-*bad_middle*`
* Remove values matching prefix and suffix -- Ex: `-alg_begin*end`
For example, assume the system's KEX algorithm list is:
`ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256`
These are various outcomes (user KEX field -> result):
* Prefix removal: `-ec*` --> `diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256`
* Suffix removal, appending: `-*256,+first_alg,almost_last_alg+,last_alg+` --> `first_alg,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,almost_last_alg,last_alg`
* Direct replacement: `first_alg,middle_alg,last_alg` --> `first_alg,middle_alg,last_alg`
## Selecting A Database
Once all applicable fields have been entered and/or options selected, tapping **OK** will attempt to connect to the SFTP server. First time connections may pop up a dialog window asking to accept the host's authenticity (tap **yes** if the host is trusted), as well as potentially creating a new `known_hosts` file (tap **yes** to do so). If the connection is successful, a remote file browser screen will open. Navigate and select the Keepass database to open.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@@ -58,12 +58,12 @@ namespace keepass2android
}
private static string LogFilename
public static string LogFilename
{
get { return Application.Context.FilesDir.CanonicalPath +"/keepass2android.log"; }
}
private static bool LogToFile
public static bool LogToFile
{
get
{

View File

@@ -434,7 +434,7 @@ namespace Kp2aAutofillParser
public static List<string> ConvertToCanonicalLowerCaseHints(string[] supportedHints)
{
List<string> result = new List<string>();
foreach (string hint in supportedHints)
foreach (string hint in supportedHints.Where(h => h != null))
{
var canonicalHint = ToCanonicalHint(hint);
result.Add(canonicalHint.ToLower());
@@ -829,7 +829,7 @@ namespace Kp2aAutofillParser
// * if there is no such autofill hint, we use IsPassword to
HashSet<string> autofillHintsOfAllFields = autofillView.InputFields.Where(f => f.AutofillHints != null)
.SelectMany(f => f.AutofillHints).Select(AutofillHintsHelper.ToCanonicalHint).ToHashSet();
.SelectMany(f => f.AutofillHints).Where(x => x != null).Select(AutofillHintsHelper.ToCanonicalHint).ToHashSet();
bool hasLoginAutofillHints = autofillHintsOfAllFields.Intersect(_autofillHintsForLogin).Any();
if (hasLoginAutofillHints)
@@ -839,9 +839,9 @@ namespace Kp2aAutofillParser
string[] viewHints = viewNode.AutofillHints;
if (viewHints == null)
continue;
if (viewHints.Select(AutofillHintsHelper.ToCanonicalHint).Intersect(_autofillHintsForLogin).Any())
if (viewHints.Where(h => h != null).Select(AutofillHintsHelper.ToCanonicalHint).Intersect(_autofillHintsForLogin).Any())
{
AddFieldToHintMap(viewNode, viewHints.Select(AutofillHintsHelper.ToCanonicalHint).ToHashSet().ToArray());
AddFieldToHintMap(viewNode, viewHints.Where(h => h != null).Select(AutofillHintsHelper.ToCanonicalHint).ToHashSet().ToArray());
}
}
@@ -973,7 +973,7 @@ namespace Kp2aAutofillParser
|| IsInputTypeVariation(inputType, InputTypes.TextVariationWebPassword)
)
)
|| (f.AutofillHints != null && f.AutofillHints.First() == "passwordAuto")
|| (f.AutofillHints != null && f.AutofillHints.FirstOrDefault() == "passwordAuto")
|| (f.HtmlInfoTypeAttribute == "password")
);
}

View File

@@ -30,11 +30,18 @@ namespace Kp2aAutofillParserTest
RunTestFromAutofillInput(resourceName, "com.servicenet.mobile");
}
[Fact]
public void TestCrashRegressionEmptySequence()
{
var resourceName = "Kp2aAutofillParserTest.imdb.json";
RunTestFromAutofillInput(resourceName, "com.vivaldi.browser", "m.imdb.com");
}
[Fact]
public void TestFocusedPasswordAutoIsFilled()
{
var resourceName = "Kp2aAutofillParserTest.com-servicenet-mobile-focused.json";
RunTestFromAutofillInput(resourceName, "com.servicenet.mobile" );
RunTestFromAutofillInput(resourceName, "com.servicenet.mobile");
}
[Fact]

View File

@@ -15,6 +15,7 @@
<None Remove="com-servicenet-mobile-focused.json" />
<None Remove="com-servicenet-mobile-no-focus.json" />
<None Remove="firefox-amazon-it.json" />
<None Remove="imdb.json" />
</ItemGroup>
<ItemGroup>
@@ -53,6 +54,9 @@
<EmbeddedResource Include="com-servicenet-mobile-no-focus.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Include="imdb.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</EmbeddedResource>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,728 @@
{
"InputFields": [
{
"IdEntry": null,
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "action_bar_root",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "custom_tabs_handle_view_stub",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "coordinator",
"Hint": null,
"ClassName": "android.view.ViewGroup",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "compositor_view_holder",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": null,
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": true,
"InputType": 0,
"HtmlInfoTag": "form",
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": null,
"Hint": "",
"ClassName": null,
"AutofillHints": [],
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": "input",
"HtmlInfoTypeAttribute": "checkbox"
},
{
"IdEntry": null,
"Hint": "Search IMDb",
"ClassName": null,
"AutofillHints": [
"off"
],
"IsFocused": true,
"InputType": 0,
"HtmlInfoTag": "input",
"HtmlInfoTypeAttribute": "text"
},
{
"IdEntry": "main_tab_switcher",
"Hint": null,
"ClassName": "android.widget.RelativeLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "ar_view_holder",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "capture_overlay",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "overview_list_layout_holder",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "bottom_container",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "keyboard_accessory_sheet_stub",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "bottombar_stub",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "tab_modal_dialog_container_stub",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "tab_modal_dialog_container_sibling_view",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "omnibox_results_container_stub",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "panel_stub",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "search_engine_suggestion_stub",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "action_bar_black_background",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "bottom_controls",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "bottom_controls_wrapper",
"Hint": null,
"ClassName": "android.widget.LinearLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "tab_group_ui_bottom_container",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "tab_group_ui_toolbar_view",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "bottom_container_slot",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "bottom_toolbar",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "bottom_toolbar_browsing",
"Hint": null,
"ClassName": "android.widget.LinearLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "tab_switcher_tab_layout_toggle",
"Hint": null,
"ClassName": "android.widget.RelativeLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "tab_switcher_tab_layout",
"Hint": null,
"ClassName": "android.widget.HorizontalScrollView",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": null,
"Hint": null,
"ClassName": "android.widget.TextView",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": null,
"Hint": null,
"ClassName": "android.widget.TextView",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": null,
"Hint": null,
"ClassName": "android.widget.TextView",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": null,
"Hint": null,
"ClassName": "android.widget.TextView",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "control_container",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "find_toolbar_stub",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "find_toolbar_tablet_stub",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "toolbar_container",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "tab_group_ui_top_container",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "toolbar",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "location_bar",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "location_bar_status_view_left_space",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "location_bar_status",
"Hint": null,
"ClassName": "android.widget.LinearLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "location_bar_incognito_badge_stub",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "location_bar_status_icon_view",
"Hint": null,
"ClassName": "android.widget.LinearLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "location_bar_status_icon_frame",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "location_bar_status_icon_bg",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "location_bar_status_icon_holding_space",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "location_bar_verbose_status",
"Hint": null,
"ClassName": "android.widget.TextView",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "location_bar_verbose_status_separator",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "location_bar_verbose_status_extra_space",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "location_bar_status_view_right_space",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "url_action_container",
"Hint": null,
"ClassName": "android.widget.LinearLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "toolbar_buttons",
"Hint": null,
"ClassName": "android.widget.LinearLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "optional_button_stub",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "menu_button_wrapper",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "tab_switcher_toolbar_stub",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "bottom_toolbar_tab_switcher_mode",
"Hint": null,
"ClassName": "android.widget.LinearLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "grid_tab_switcher_view_holder_stub",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "message_container",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "status_indicator_stub",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "empty_container_stub",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "sheet_container",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "survey_container",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "page_zoom_container",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "dialog_parent_view",
"Hint": null,
"ClassName": "android.widget.FrameLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "keyboard_accessory",
"Hint": null,
"ClassName": "android.widget.LinearLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "accessory_bar_contents",
"Hint": null,
"ClassName": "android.widget.LinearLayout",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "tabs",
"Hint": null,
"ClassName": "android.widget.HorizontalScrollView",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "menu_anchor_stub",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "navigation_popup_anchor_stub",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
},
{
"IdEntry": "action_mode_bar_stub",
"Hint": null,
"ClassName": "android.view.View",
"AutofillHints": null,
"IsFocused": false,
"InputType": 0,
"HtmlInfoTag": null,
"HtmlInfoTypeAttribute": null
}
],
"PackageId": "com.vivaldi.browser",
"WebDomain": "m.imdb.com"
}

View File

@@ -25,7 +25,7 @@ namespace keepass2android.Io
public abstract bool UserShouldBackup { get; }
private readonly IJavaFileStorage _jfs;
protected readonly IJavaFileStorage _jfs;
private readonly IKp2aApp _app;
public JavaFileStorage(IJavaFileStorage jfs, IKp2aApp app)

View File

@@ -1,18 +1,18 @@
#if !NoNet
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Reflection;
using System.Threading;
using Android.Content;
using Android.OS;
using Android.Preferences;
using FluentFTP;
using FluentFTP.Exceptions;
using KeePassLib;
using KeePassLib.Serialization;
using KeePassLib.Utility;
namespace keepass2android.Io
{
public class NetFtpFileStorage: IFileStorage
@@ -75,14 +75,15 @@ namespace keepass2android.Io
}
private readonly ICertificateValidationHandler _app;
private readonly Func<bool> _debugLogPrefGetter;
public MemoryStream traceStream;
public NetFtpFileStorage(Context context, ICertificateValidationHandler app)
public NetFtpFileStorage(Context context, ICertificateValidationHandler app, Func<bool> debugLogPrefGetter)
{
_app = app;
traceStream = new MemoryStream();
_debugLogPrefGetter = debugLogPrefGetter;
traceStream = new MemoryStream();
}
public IEnumerable<string> SupportedProtocols
@@ -138,7 +139,7 @@ namespace keepass2android.Io
var settings = ConnectionSettings.FromIoc(ioc);
FtpClient client = new FtpClient();
client.RetryAttempts = 3;
client.Config.RetryAttempts = 3;
if ((settings.Username.Length > 0) || (settings.Password.Length > 0))
client.Credentials = new NetworkCredential(settings.Username, settings.Password);
else
@@ -154,9 +155,12 @@ namespace keepass2android.Io
args.Accept = _app.CertificateValidationCallback(control, args.Certificate, args.Chain, args.PolicyErrors);
};
client.EncryptionMode = settings.EncryptionMode;
client.Connect();
client.Config.EncryptionMode = settings.EncryptionMode;
if (_debugLogPrefGetter())
client.Logger = new Kp2aLogFTPLogger();
client.Connect();
return client;
}
@@ -284,42 +288,55 @@ namespace keepass2android.Io
public IEnumerable<FileDescription> ListContents(IOConnectionInfo ioc)
{
try
try
{
using (var client = GetClient(ioc))
{
/*
* For some reason GetListing(path) does not always return the contents of the directory.
* However, calling SetWorkingDirectory(path) followed by GetListing(null, options) to
* list the contents of the working directory does consistently work.
*
* Similar behavior was confirmed using ncftp client. I suspect this is a strange
* bug/nuance in the server's implementation of the LIST command?
*
* [bug #2423]
*/
client.SetWorkingDirectory(IocToLocalPath(ioc));
List<FileDescription> files = new List<FileDescription>();
foreach (FtpListItem item in client.GetListing(IocToLocalPath(ioc),
FtpListOption.Modify | FtpListOption.Size | FtpListOption.DerefLinks))
foreach (FtpListItem item in client.GetListing(null,
FtpListOption.SizeModify | FtpListOption.AllFiles))
{
switch (item.Type)
switch (item.Type)
{
case FtpFileSystemObjectType.Directory:
case FtpObjectType.Directory:
files.Add(new FileDescription()
{
CanRead = true,
CanWrite = true,
DisplayName = item.Name,
IsDirectory = true,
LastModified = item.Modified,
Path = IocPathFromUri(ioc, item.FullName)
});
break;
case FtpFileSystemObjectType.File:
{
CanRead = true,
CanWrite = true,
DisplayName = item.Name,
IsDirectory = true,
LastModified = item.Modified,
Path = IocPathFromUri(ioc, item.FullName)
});
break;
case FtpObjectType.File:
files.Add(new FileDescription()
{
CanRead = true,
CanWrite = true,
DisplayName = item.Name,
IsDirectory = false,
LastModified = item.Modified,
Path = IocPathFromUri(ioc, item.FullName),
SizeInBytes = item.Size
});
{
CanRead = true,
CanWrite = true,
DisplayName = item.Name,
IsDirectory = false,
LastModified = item.Modified,
Path = IocPathFromUri(ioc, item.FullName),
SizeInBytes = item.Size
});
break;
default:
Kp2aLog.Log("FTP: ListContents item skipped: " + IocToUri(ioc) + ": " + item.FullName + ", type=" + item.Type);
break;
}
}
}
return files;
}
@@ -329,7 +346,6 @@ namespace keepass2android.Io
throw ConvertException(ex);
}
}
public FileDescription GetFileDescription(IOConnectionInfo ioc)
{
@@ -466,7 +482,9 @@ namespace keepass2android.Io
public static int GetDefaultPort(FtpEncryptionMode encryption)
{
return new FtpClient() { EncryptionMode = encryption}.Port;
var client = new FtpClient();
client.Config.EncryptionMode = encryption;
return client.Port;
}
public string BuildFullPath(string host, int port, string initialPath, string user, string password, FtpEncryptionMode encryption)
@@ -582,5 +600,13 @@ namespace keepass2android.Io
_stream.Close();
}
}
class Kp2aLogFTPLogger : IFtpLogger
{
public void Log(FtpLogEntry entry)
{
Kp2aLog.Log("[FluentFTP] " + entry.Message);
}
}
}
#endif

View File

@@ -3,14 +3,15 @@ using Android.Content;
namespace keepass2android.Io
{
public partial class PCloudFileStorage: JavaFileStorage
public class PCloudFileStorage: JavaFileStorage
{
private const string ClientId = "CkRWTQXY6Lm";
private const string ClientId = "yCeH59Ffgtm";
public PCloudFileStorage(Context ctx, IKp2aApp app) :
base(new Keepass2android.Javafilestorage.PCloudFileStorage(ctx, ClientId), app)
base(new Keepass2android.Javafilestorage.PCloudFileStorage(ctx, ClientId, "pcloud", ""), app)
{
}
}
public override bool UserShouldBackup
@@ -18,6 +19,23 @@ namespace keepass2android.Io
get { return false; }
}
}
public class PCloudFileStorageAll : JavaFileStorage
{
private const string ClientId = "FLm22de7bdS";
public PCloudFileStorageAll(Context ctx, IKp2aApp app) :
base(new Keepass2android.Javafilestorage.PCloudFileStorage(ctx, ClientId, "pcloudall", "PCLOUDALL_"), app)
{
}
public override bool UserShouldBackup
{
get { return false; }
}
}
}
#endif

View File

@@ -1,17 +1,39 @@
using Android.Content;
using Java.Nio.FileNio;
#if !EXCLUDE_JAVAFILESTORAGE
namespace keepass2android.Io
{
public class SftpFileStorage: JavaFileStorage
{
public SftpFileStorage(Context ctx, IKp2aApp app) :
public SftpFileStorage(Context ctx, IKp2aApp app, bool debugEnabled) :
base(new Keepass2android.Javafilestorage.SftpStorage(ctx.ApplicationContext), app)
{
}
var storage = BaseStorage;
if (debugEnabled)
{
string? logFilename = null;
if (Kp2aLog.LogToFile)
{
logFilename = Kp2aLog.LogFilename;
}
storage.SetJschLogging(true, logFilename);
}
else
{
storage.SetJschLogging(false, null);
}
}
private Keepass2android.Javafilestorage.SftpStorage BaseStorage
{
get
{
return _jfs as Keepass2android.Javafilestorage.SftpStorage;
}
}
public override bool UserShouldBackup
public override bool UserShouldBackup
{
get { return true; }
}

View File

@@ -182,7 +182,7 @@
</ItemGroup>
<ItemGroup Condition=" '$(Flavor)'!='NoNet' ">
<PackageReference Include="FluentFTP">
<Version>31.3.1</Version>
<Version>48.0.0</Version>
</PackageReference>
<PackageReference Include="MegaApiClient">
<Version>1.10.3</Version>
@@ -312,4 +312,4 @@
<Target Name="AfterBuild">
</Target>
-->
</Project>
</Project>

Binary file not shown.

Binary file not shown.

View File

@@ -56,7 +56,7 @@
<ItemGroup>
<None Include="Jars\AboutJars.txt" />
<None Include="Additions\AboutAdditions.txt" />
<LibraryProjectZip Include="Jars\pcloud-sdk-android-1.2.0.aar" />
<LibraryProjectZip Include="Jars\pcloud-sdk-android-1.8.1.aar" />
</ItemGroup>
<ItemGroup>
<TransformFile Include="Transforms\Metadata.xml" />
@@ -72,6 +72,6 @@
</Target>
-->
<ItemGroup>
<EmbeddedReferenceJar Include="Jars\pcloud-sdk-java-core-1.2.0.jar" />
<EmbeddedReferenceJar Include="Jars\pcloud-sdk-java-core-1.8.1.jar" />
</ItemGroup>
</Project>

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<Flavor>NoNet</Flavor>
<xFlavor>Net</xFlavor>
</PropertyGroup>

View File

@@ -47,8 +47,8 @@ dependencies {
implementation('com.onedrive.sdk:onedrive-sdk-android:1.2.0') {
transitive = false
}
implementation 'com.pcloud.sdk:java-core:1.2.0'
implementation 'com.pcloud.sdk:android:1.2.0'
implementation 'com.pcloud.sdk:java-core:1.8.1'
implementation 'com.pcloud.sdk:android:1.8.1'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.microsoft.services.msa:msa-auth:0.8.6'
implementation 'com.microsoft.aad:adal:1.14.0'

View File

@@ -97,7 +97,28 @@ public class FileEntry {
return false;
return true;
}
@Override
public String toString() {
StringBuilder s = new StringBuilder("JavaFileStorage.FileEntry{").append(displayName).append("|")
.append("path=").append(path).append(",sz=").append(sizeInBytes)
.append(",").append(isDirectory ? "dir" : "file")
.append(",lastMod=").append(lastModifiedTime);
StringBuilder perms = new StringBuilder();
if (canRead)
perms.append("r");
if (canWrite)
perms.append("w");
if (perms.length() > 0) {
s.append(",").append(perms);
}
if (userData != null && userData.length() > 0)
s.append(",userData=").append(userData);
return s.append("}").toString();
}
}

View File

@@ -0,0 +1,101 @@
package keepass2android.javafilestorage;
import android.util.Log;
import com.jcraft.jsch.Logger;
import java.io.FileWriter;
import java.io.PrintWriter;
import java.util.Map;
public class Kp2aJSchLogger implements Logger {
private static final String PREFIX = "KP2AJFS[JSch]";
private interface ILogger {
void log(String message);
}
private interface EntryToLogFactory {
ILogger create(LogEntry e);
}
private static final EntryToLogFactory ANDROID_FACTORY = e -> e.logger;
private static final class LogEntry {
private final String levelTag;
private final ILogger logger;
LogEntry(String levelTag, ILogger logger) {
this.levelTag = levelTag;
this.logger = logger;
}
}
private static final ILogger DEBUG = msg -> Log.d(PREFIX, msg);
private static final LogEntry DEBUG_ENTRY = new LogEntry("D", DEBUG);
private static final ILogger ERROR = msg -> Log.e(PREFIX, msg);
private static final LogEntry DEFAULT_ENTRY = DEBUG_ENTRY;
private static final Map<Integer, LogEntry> loggers = Map.of(
Logger.DEBUG, DEBUG_ENTRY,
Logger.INFO, new LogEntry("I", msg -> Log.i(PREFIX, msg)),
Logger.WARN, new LogEntry("W", msg -> Log.w(PREFIX, msg)),
Logger.ERROR, new LogEntry("E", ERROR),
Logger.FATAL, new LogEntry("F", msg -> Log.wtf(PREFIX, msg))
);
private final EntryToLogFactory logFactory;
static Kp2aJSchLogger createAndroidLogger() {
return new Kp2aJSchLogger(ANDROID_FACTORY);
}
static Kp2aJSchLogger createFileLogger(String logFilename) {
final String fName = logFilename;
return new Kp2aJSchLogger(e -> createFileLogger(e, fName));
}
private Kp2aJSchLogger(EntryToLogFactory logFactory) {
this.logFactory = logFactory;
}
@Override
public boolean isEnabled(int level) {
return true;
}
@Override
public void log(int level, String message) {
if (isEnabled(level))
getLogger(level).log(message);
}
private ILogger getLogger(int level) {
LogEntry entry = loggers.get(level);
if (entry == null)
entry = DEFAULT_ENTRY;
return logFactory.create(entry);
}
private static ILogger createFileLogger(LogEntry entry, String fName) {
try {
final PrintWriter p = new PrintWriter(new FileWriter(fName, true));
return msg -> {
try {
String fullMsg = String.join(" ", entry.levelTag, PREFIX, msg);
p.println(fullMsg);
} catch (Exception e) {
ERROR.log(e.getMessage());
} finally {
p.close();
}
};
} catch (Exception e) {
ERROR.log(e.getMessage());
return entry.logger;
}
}
}

View File

@@ -22,6 +22,7 @@ import com.pcloud.sdk.ApiError;
import com.pcloud.sdk.Authenticators;
import com.pcloud.sdk.AuthorizationActivity;
import com.pcloud.sdk.AuthorizationData;
import com.pcloud.sdk.AuthorizationRequest;
import com.pcloud.sdk.AuthorizationResult;
import com.pcloud.sdk.Call;
import com.pcloud.sdk.DataSource;
@@ -47,11 +48,19 @@ public class PCloudFileStorage extends JavaFileStorageBase
private ApiClient apiClient;
private String clientId;
private String protocolId;
public PCloudFileStorage(Context ctx, String clientId) {
///prefix for SHARED_PREF keys so we can distinguish between different instances
private String sharedPrefPrefix;
public PCloudFileStorage(Context ctx, String clientId, String protocolId, String sharedPrefPrefix) {
this.ctx = ctx;
this.clientId = clientId;
this.protocolId = protocolId;
this.sharedPrefPrefix = sharedPrefPrefix;
this.apiClient = createApiClientFromSharedPrefs();
android.util.Log.d("KP2A", "Init pcloud with protocol " + protocolId + ", prefix=" + sharedPrefPrefix + ", clientId=" + clientId);
}
@Override
@@ -86,7 +95,8 @@ public class PCloudFileStorage extends JavaFileStorageBase
@Override
public String getProtocolId() {
return "pcloud";
return protocolId;
}
@Override
@@ -136,7 +146,7 @@ public class PCloudFileStorage extends JavaFileStorageBase
DataSource dataSource = DataSource.create(data);
String filename = path.substring(path.lastIndexOf("/") + 1);
String filePath = path.substring(0, path.lastIndexOf("/") + 1);
String filePath = path.substring(0, path.lastIndexOf("/"));
RemoteFolder remoteFolder = this.getRemoteFolderByPath(filePath);
try {
@@ -165,11 +175,14 @@ public class PCloudFileStorage extends JavaFileStorageBase
@Override
public String createFilePath(String parentPath, String newFileName) throws Exception {
String cleanpath = this.cleanPath(parentPath);
String filepath = this.getProtocolId() + "://";
return (
this.getProtocolId() + "://" +
this.cleanPath(parentPath) +
("".equals(newFileName) ? "" : "/") +
newFileName
filepath
+cleanpath
+("".equals(newFileName) || "/".equals(cleanpath) ? "" : "/") +newFileName
);
}
@@ -191,7 +204,7 @@ public class PCloudFileStorage extends JavaFileStorageBase
@Override
public FileEntry getFileEntry(String path) throws Exception {
path = this.cleanPath(path);
//do not call getRemoteFileByPath because path could represent a file or folder, we don't know here
RemoteEntry remoteEntry = this.getRemoteEntryByPath(path);
return this.convertRemoteEntryToFileEntry(
@@ -204,10 +217,13 @@ public class PCloudFileStorage extends JavaFileStorageBase
public void delete(String path) throws Exception {
path = this.cleanPath(path);
RemoteEntry remoteEntry = this.getRemoteFileByPath(path);
RemoteEntry remoteEntry = this.getRemoteEntryByPath(path);
try {
this.apiClient.delete(remoteEntry).execute();
if (remoteEntry.isFolder())
this.apiClient.deleteFolder(remoteEntry.asFolder(), true).execute();
else
this.apiClient.delete(remoteEntry).execute();
} catch (ApiError e) {
throw convertApiError(e);
}
@@ -228,11 +244,17 @@ public class PCloudFileStorage extends JavaFileStorageBase
finishActivityWithSuccess(activity);
} else if (!activity.getState().getBoolean("hasStartedAuth", false)) {
Activity castedActivity = (Activity)activity;
Intent authIntent = AuthorizationActivity.createIntent(castedActivity, this.clientId);
AuthorizationRequest req = AuthorizationRequest.create()
.setClientId(this.clientId)
.setType(AuthorizationRequest.Type.TOKEN)
.setForceAccessApproval(true)
.build();
Intent authIntent = AuthorizationActivity.createIntent(castedActivity, req);
castedActivity.startActivityForResult(authIntent, PCLOUD_AUTHORIZATION_REQUEST_CODE);
activity.getState().putBoolean("hasStartedAuth", true);
}
}
@Override
@@ -273,7 +295,7 @@ public class PCloudFileStorage extends JavaFileStorageBase
}
private ApiClient createApiClientFromSharedPrefs() {
SharedPreferences prefs = this.ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
SharedPreferences prefs = getPrefs();
String authToken = prefs.getString(SHARED_PREF_AUTH_TOKEN, null);
String apiHost = prefs.getString(SHARED_PREF_API_HOST, null);
return this.createApiClient(authToken, apiHost);
@@ -297,15 +319,20 @@ public class PCloudFileStorage extends JavaFileStorageBase
private void clearAuthToken() {
this.apiClient = null;
SharedPreferences prefs = this.ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
SharedPreferences prefs = getPrefs();
SharedPreferences.Editor edit = prefs.edit();
edit.clear();
edit.apply();
}
private SharedPreferences getPrefs()
{
return this.ctx.getSharedPreferences(sharedPrefPrefix + SHARED_PREF_NAME, Context.MODE_PRIVATE);
}
private void setAuthToken(String authToken, String apiHost) {
this.apiClient = this.createApiClient(authToken, apiHost);
SharedPreferences prefs = this.ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
SharedPreferences prefs = getPrefs();
SharedPreferences.Editor edit = prefs.edit();
edit.putString(SHARED_PREF_AUTH_TOKEN, authToken);
edit.putString(SHARED_PREF_API_HOST, apiHost);
@@ -319,27 +346,47 @@ public class PCloudFileStorage extends JavaFileStorageBase
}
private RemoteFile getRemoteFileByPath(String path) throws Exception {
RemoteEntry remoteEntry = this.getRemoteEntryByPath(path);
Call<RemoteFile> call = this.apiClient.loadFile(path);
try {
return remoteEntry.asFile();
} catch (IllegalStateException e) {
throw new FileNotFoundException(e.toString());
return call.execute();
} catch (ApiError apiError) {
throw convertApiError(apiError);
}
}
private RemoteFolder getRemoteFolderByPath(String path) throws Exception {
RemoteEntry remoteEntry = this.getRemoteEntryByPath(path);
Call<RemoteFolder> call;
if ("".equals(path))
call = this.apiClient.listFolder(RemoteFolder.ROOT_FOLDER_ID, false);
else
call = this.apiClient.listFolder(path, false);
try {
return remoteEntry.asFolder();
} catch (IllegalStateException e) {
throw new FileNotFoundException(e.toString());
return call.execute();
} catch (ApiError apiError) {
throw convertApiError(apiError);
}
}
private RemoteEntry getRemoteEntryByPath(String path) throws Exception {
Call<RemoteFolder> call = this.apiClient.listFolder(RemoteFolder.ROOT_FOLDER_ID, true);
if ("/".equals(path)) {
try {
return this.apiClient.listFolder(RemoteFolder.ROOT_FOLDER_ID, false).execute();
} catch (ApiError apiError) {
throw convertApiError(apiError);
}
}
String filename = path.substring(path.lastIndexOf("/") + 1);
String parentPath = path.substring(0, path.lastIndexOf("/"));
Call<RemoteFolder> call;
if ("".equals(parentPath))
call = this.apiClient.listFolder(RemoteFolder.ROOT_FOLDER_ID, false);
else
call = this.apiClient.listFolder(parentPath, false);
RemoteFolder folder;
try {
@@ -348,40 +395,12 @@ public class PCloudFileStorage extends JavaFileStorageBase
throw convertApiError(apiError);
}
if ("/".equals(path)) {
return folder;
for (RemoteEntry remoteEntry : folder.children()) {
if (remoteEntry.name() != null && remoteEntry.name().equals(filename))
return remoteEntry;
}
throw new FileNotFoundException("did not find " + path);
String[] fileNames = path.substring(1).split("/");
RemoteFolder currentFolder = folder;
Iterator<String> fileNamesIterator = Arrays.asList(fileNames).iterator();
while (true) {
String fileName = fileNamesIterator.next();
Iterator<RemoteEntry> entryIterator = currentFolder.children().iterator();
while (true) {
RemoteEntry remoteEntry;
try {
remoteEntry = entryIterator.next();
} catch (NoSuchElementException e) {
throw new FileNotFoundException(e.toString());
}
if (currentFolder.folderId() == remoteEntry.parentFolderId() && fileName.equals(remoteEntry.name())) {
if (!fileNamesIterator.hasNext()) {
return remoteEntry;
}
try {
currentFolder = remoteEntry.asFolder();
} catch (IllegalStateException e) {
throw new FileNotFoundException(e.toString());
}
break;
}
}
}
}
private Exception convertApiError(ApiError e) {

View File

@@ -0,0 +1,216 @@
package keepass2android.javafilestorage;
import android.util.Pair;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import java.util.regex.Pattern;
import androidx.annotation.Nullable;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.KeyPair;
class SftpPublicPrivateKeyUtils {
private enum Validity {
NOT_ATTEMPTED, VALID, NOT_VALID;
}
private static final String SFTP_CUSTOM_KEY_DIRNAME = "user_keys";
private static final String KP2A_PRIVATE_KEY_FILENAME = "id_kp2a_rsa";
private final File appBaseDir;
/**
* Do NOT access this variable directly! Use {@link #baseDir()} instead.
*/
private final File customKeyBaseDir;
private volatile Validity validDir = Validity.NOT_ATTEMPTED;
SftpPublicPrivateKeyUtils(String appBaseDir) {
// Assume app base directory exists already
this.appBaseDir = new File(appBaseDir);
// Intentionally skipping existence/creation checking in constructor
// See baseDir()
this.customKeyBaseDir = new File(appBaseDir, SFTP_CUSTOM_KEY_DIRNAME);
}
private Pair<File, Boolean> baseDir() {
if (validDir == Validity.NOT_ATTEMPTED) {
synchronized (this) {
if (!customKeyBaseDir.exists()) {
customKeyBaseDir.mkdirs();
}
if (customKeyBaseDir.exists() && customKeyBaseDir.isDirectory()) {
validDir = Validity.VALID;
} else {
validDir = Validity.NOT_VALID;
}
}
}
return new Pair<>(customKeyBaseDir, validDir == Validity.VALID);
}
boolean deleteCustomKey(String keyName) throws FileNotFoundException {
File f = getCustomKeyFile(keyName);
return f.isFile() && f.delete();
}
String[] getCustomKeyNames() {
Pair<File, Boolean> base = baseDir();
if (!base.second) {
// Log it?
return new String[]{};
}
return base.first.list();
}
void savePrivateKeyContent(String keyName, String keyContent) throws IOException, Exception {
keyContent = PrivateKeyValidator.ensureValidContent(keyContent);
File f = getCustomKeyFile(keyName);
try (BufferedWriter w = new BufferedWriter(new FileWriter(f))) {
w.write(keyContent);
}
}
String getCustomKeyFilePath(String customKeyName) throws FileNotFoundException {
return getCustomKeyFile(customKeyName).getAbsolutePath();
}
String resolveKeyFilePath(JSch jschInst, @Nullable String customKeyName) {
// Custom private key configured
if (customKeyName != null) {
try {
return getCustomKeyFilePath(customKeyName);
} catch (FileNotFoundException e) {
System.out.println(e);
}
}
// Use KP2A's public/private key
String keyFilePath = getAppKeyFileName();
try{
createKeyPair(jschInst, keyFilePath);
} catch (Exception ex) {
System.out.println(ex);
}
return keyFilePath;
}
String createKeyPair(JSch jschInst) throws IOException, JSchException {
return createKeyPair(jschInst, getAppKeyFileName());
}
/**
* Exposed for testing purposes only
* @param keyName
* @return
*/
String getSanitizedCustomKeyName(String keyName) {
return PrivateKeyValidator.sanitizeKeyAsFilename(keyName);
}
/**
* Exposed for testing purposes only.
* @param keyContent
* @return
* @throws Exception
*/
String getValidatedCustomKeyContent(String keyContent) throws Exception {
return PrivateKeyValidator.ensureValidContent(keyContent);
}
private String createKeyPair(JSch jschInst, String key_filename) throws JSchException, IOException {
String public_key_filename = key_filename + ".pub";
File file = new File(key_filename);
if (file.exists())
return public_key_filename;
int type = KeyPair.RSA;
KeyPair kpair = KeyPair.genKeyPair(jschInst, type, 4096);
kpair.writePrivateKey(key_filename);
kpair.writePublicKey(public_key_filename, "generated by Keepass2Android");
//ret = "Fingerprint: " + kpair.getFingerPrint();
kpair.dispose();
return public_key_filename;
}
private String getAppKeyFileName() {
return new File(appBaseDir, KP2A_PRIVATE_KEY_FILENAME).getAbsolutePath();
}
private File getCustomKeyFile(String customKeyName) throws FileNotFoundException {
Pair<File, Boolean> base = baseDir();
if (!base.second) {
throw new FileNotFoundException("Custom key directory");
}
String keyFileName = PrivateKeyValidator.sanitizeKeyAsFilename(customKeyName);
if (!keyFileName.isEmpty()) {
File keyFile = new File(base.first, keyFileName);
// Protect against bad actors trying to navigate away from the base directory.
// This is probably overkill, given sanitizeKeyAsFilename(...) but better safe than sorry.
if (base.first.equals(keyFile.getParentFile())) {
return keyFile;
}
}
// The key was sanitized to nothing, or the parent check above failed.
throw new FileNotFoundException("Malformed key name");
}
private static class PrivateKeyValidator {
private static final Pattern CONTENT_FIRST_LINE = Pattern.compile("^-+BEGIN\\s[^\\s]+\\sPRIVATE\\sKEY-+$");
private static final Pattern CONTENT_LAST_LINE = Pattern.compile("^-+END\\s[^\\s]+\\sPRIVATE\\sKEY-+$");
/**
* Key-to-filename sanitizer solution sourced from:
* <a href="https://www.b4x.com/android/forum/threads/sanitize-filename.82558/" />
*/
private static final Pattern KEY_SANITIZER = Pattern.compile("([^\\p{L}\\s\\d\\-_~,;:\\[\\]\\(\\).'])",
Pattern.CASE_INSENSITIVE);
static String sanitizeKeyAsFilename(String key) {
return KEY_SANITIZER.matcher(key.trim()).replaceAll("");
}
static String ensureValidContent(String content) throws Exception {
content = content.trim();
boolean isValid = true;
try (BufferedReader r = new BufferedReader(new StringReader(content))) {
boolean validFirst = false;
String line;
String last = null;
while ((line = r.readLine()) != null) {
if (!validFirst) {
if (CONTENT_FIRST_LINE.matcher(line).matches()) {
validFirst = true;
} else {
isValid = false;
break;
}
}
last = line;
}
if (!isValid || last == null || !CONTENT_LAST_LINE.matcher(last).matches()) {
throw new RuntimeException("Malformed private key content");
}
} catch (Exception e) {
android.util.Log.d(SftpStorage.class.getName(), "Invalid key content", e);
throw e;
}
return content;
}
}
}

View File

@@ -2,20 +2,24 @@ package keepass2android.javafilestorage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.ChannelSftp.LsEntry;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.KeyPair;
import com.jcraft.jsch.Logger;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;
@@ -26,10 +30,38 @@ import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
@SuppressWarnings("unused") // Exposed by JavaFileStorageBindings
public class SftpStorage extends JavaFileStorageBase {
@FunctionalInterface
interface ValueResolver<T> {
/**
* Takes a raw value and resolves it to either a String containing the String representation
* of that value, or null. The latter signifying that the raw value could not be "resolved".
*
* @param value
* @return String, or null if not resolvable
*/
String resolve(T value);
}
public static final int DEFAULT_SFTP_PORT = 22;
JSch jsch;
public static final int UNSET_SFTP_CONNECT_TIMEOUT = -1;
private static final String SFTP_CONNECT_TIMEOUT_OPTION_NAME = "connectTimeout";
private static final String SFTP_KEYNAME_OPTION_NAME = "key";
private static final String SFTP_KEYPASSPHRASE_OPTION_NAME = "phrase";
public static final String SSH_CFG_KEX = "kex";
public static final String SSH_CFG_SERVER_HOST_KEY = "server_host_key";
private static final Set<String> SSH_CFG_CSV_EXPANDABLE = Set.of(SSH_CFG_KEX, SSH_CFG_SERVER_HOST_KEY);
private static final ValueResolver<Integer> cTimeoutResolver = c ->
c == null || c == UNSET_SFTP_CONNECT_TIMEOUT ? null : String.valueOf(c);
private static final ValueResolver<String> nonBlankStringResolver = s ->
s == null || s.isBlank() ? null : s;
private static final String TAG = "KP2AJFS";
private static final String THREAD_TAG = TAG + "[thread]";
private JSch jsch;
public class ConnectionInfo
{
@@ -37,13 +69,42 @@ public class SftpStorage extends JavaFileStorageBase {
public String username;
public String password;
public String localPath;
public String keyName;
public String keyPassphrase;
public int port;
public int connectTimeoutSec = UNSET_SFTP_CONNECT_TIMEOUT;
public final Map<String, String> configOpts = new HashMap<>();
public String toString() {
return "ConnectionInfo{host=" + host + ",port=" + port + ",user=" + username +
",pwd=<hidden>,localPath=" + localPath + ",key=" + keyName +
",phrase=<hidden>,connectTimeout=" + connectTimeoutSec +
",cfgOpts=" + configOpts +
"}";
}
}
private static Map<String, String> buildOptionMap(ConnectionInfo ci, boolean includeSensitive) {
OptionMapBuilder b = new OptionMapBuilder()
.addOption(SFTP_CONNECT_TIMEOUT_OPTION_NAME, ci.connectTimeoutSec, cTimeoutResolver)
.addOption(SFTP_KEYNAME_OPTION_NAME, ci.keyName, nonBlankStringResolver);
// Assume all config options are not sensitive and use the same resolver...
for (Map.Entry<String, String> entry : ci.configOpts.entrySet()) {
b.addOption(entry.getKey(), entry.getValue(), nonBlankStringResolver);
}
if (includeSensitive) {
b.addOption(SFTP_KEYPASSPHRASE_OPTION_NAME, ci.keyPassphrase, nonBlankStringResolver);
}
return b.build();
}
Context _appContext;
private final SftpPublicPrivateKeyUtils _keyUtils;
public SftpStorage(Context appContext) {
_appContext = appContext;
_keyUtils = new SftpPublicPrivateKeyUtils(getBaseDir());
}
private static final String SFTP_PROTOCOL_ID = "sftp";
@@ -65,15 +126,15 @@ public class SftpStorage extends JavaFileStorageBase {
@Override
public InputStream openFileForRead(String path) throws Exception {
ChannelSftp c = init(path);
ConnectionInfo cInfo = splitStringToConnectionInfo(path);
ChannelSftp c = init(cInfo);
try {
byte[] buff = new byte[8000];
int bytesRead = 0;
InputStream in = c.get(extractSessionPath(path));
InputStream in = c.get(cInfo.localPath);
ByteArrayOutputStream bao = new ByteArrayOutputStream();
while ((bytesRead = in.read(buff)) != -1) {
@@ -105,14 +166,15 @@ public class SftpStorage extends JavaFileStorageBase {
public void uploadFile(String path, byte[] data, boolean writeTransactional)
throws Exception {
ChannelSftp c = init(path);
ConnectionInfo cInfo = splitStringToConnectionInfo(path);
ChannelSftp c = init(cInfo);
try {
InputStream in = new ByteArrayInputStream(data);
String targetPath = extractSessionPath(path);
String targetPath = cInfo.localPath;
if (writeTransactional)
{
//upload to temporary location:
String tmpPath = targetPath+".tmp";
String tmpPath = targetPath + ".tmp";
c.put(in, tmpPath);
//remove previous file:
try
@@ -128,9 +190,9 @@ public class SftpStorage extends JavaFileStorageBase {
}
else
{
c.put(in, targetPath);
c.put(in, targetPath);
}
tryDisconnect(c);
} catch (Exception e) {
tryDisconnect(c);
@@ -142,53 +204,98 @@ public class SftpStorage extends JavaFileStorageBase {
@Override
public String createFolder(String parentPath, String newDirName)
throws Exception {
ConnectionInfo cInfo = splitStringToConnectionInfo(parentPath);
try {
ChannelSftp c = init(parentPath);
String newPath = concatPaths(parentPath, newDirName);
c.mkdir(extractSessionPath(newPath));
ChannelSftp c = init(cInfo);
String newPath = concatPaths(cInfo.localPath, newDirName);
c.mkdir(newPath);
tryDisconnect(c);
return newPath;
return buildFullPath(cInfo.host, cInfo.port, newPath,
cInfo.username, cInfo.password, cInfo.connectTimeoutSec,
cInfo.keyName, cInfo.keyPassphrase,
cInfo.configOpts.get(SSH_CFG_KEX),
cInfo.configOpts.get(SSH_CFG_SERVER_HOST_KEY));
} catch (Exception e) {
throw convertException(e);
}
}
private String extractUserPwdHostPort(String path) {
String withoutProtocol = path
.substring(getProtocolPrefix().length());
return withoutProtocol.substring(0, withoutProtocol.indexOf("/"));
}
private String extractSessionPath(String newPath) {
String withoutProtocol = newPath
.substring(getProtocolPrefix().length());
return withoutProtocol.substring(withoutProtocol.indexOf("/"));
int pathStartIdx = withoutProtocol.indexOf("/");
int pathEndIdx = withoutProtocol.indexOf("?");
if (pathEndIdx < 0) {
pathEndIdx = withoutProtocol.length();
}
return withoutProtocol.substring(pathStartIdx, pathEndIdx);
}
private String extractUserPwdHost(String path) {
private Map<String, String> extractOptionsMap(String path) throws UnsupportedEncodingException {
String withoutProtocol = path
.substring(getProtocolPrefix().length());
return withoutProtocol.substring(0,withoutProtocol.indexOf("/"));
Map<String, String> options = new HashMap<>();
int extraOptsIdx = withoutProtocol.indexOf("?");
if (extraOptsIdx > 0 && extraOptsIdx + 1 < withoutProtocol.length()) {
String optsString = withoutProtocol.substring(extraOptsIdx + 1);
String[] parts = optsString.split("&");
for (String p : parts) {
int sepIdx = p.indexOf('=');
if (sepIdx > 0) {
String key = decode(p.substring(0, sepIdx));
String value = decode(p.substring(sepIdx + 1));
options.put(key, value);
} else {
options.put(decode(p), "true");
}
}
}
return options;
}
private String concatPaths(String parentPath, String newDirName) {
String res = parentPath;
if (!res.endsWith("/"))
res += "/";
res += newDirName;
return res;
StringBuilder fp = new StringBuilder(parentPath);
if (!parentPath.endsWith("/"))
fp.append("/");
return fp.append(newDirName).toString();
}
@Override
public String createFilePath(String parentPath, String newFileName)
public String createFilePath(final String parentUri, String newFileName)
throws Exception {
if (parentPath.endsWith("/") == false)
parentPath += "/";
return parentPath + newFileName;
String parentPath = parentUri;
String params = null;
int paramsIdx = parentUri.lastIndexOf("?");
if (paramsIdx > 0) {
params = parentUri.substring(paramsIdx);
parentPath = parentPath.substring(0, paramsIdx);
}
String newPath = concatPaths(parentPath, newFileName);
if (params != null) {
newPath += params;
}
return newPath;
}
@Override
public List<FileEntry> listFiles(String parentPath) throws Exception {
ConnectionInfo cInfo = splitStringToConnectionInfo(parentPath);
ChannelSftp c = init(cInfo);
ChannelSftp c = init(parentPath);
return listFiles(parentPath, c);
}
private void setFromAttrs(FileEntry fileEntry, SftpATTRS attrs) {
@@ -212,23 +319,27 @@ public class SftpStorage extends JavaFileStorageBase {
if (sftpEx.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
return new FileNotFoundException(sftpEx.getMessage());
}
return e;
}
@Override
public FileEntry getFileEntry(String filename) throws Exception {
ChannelSftp c = init(filename);
ConnectionInfo cInfo = splitStringToConnectionInfo(filename);
ChannelSftp c = init(cInfo);
try {
FileEntry fileEntry = new FileEntry();
String sessionPath = extractSessionPath(filename);
SftpATTRS attr = c.stat(sessionPath);
SftpATTRS attr = c.stat(cInfo.localPath);
setFromAttrs(fileEntry, attr);
// Full URI
fileEntry.path = filename;
fileEntry.displayName = getFilename(sessionPath);
fileEntry.displayName = getFilename(cInfo.localPath);
tryDisconnect(c);
return fileEntry;
} catch (Exception e) {
logDebug("Exception in getFileEntry! " + e);
@@ -239,8 +350,9 @@ public class SftpStorage extends JavaFileStorageBase {
@Override
public void delete(String path) throws Exception {
ConnectionInfo cInfo = splitStringToConnectionInfo(path);
ChannelSftp c = init(cInfo);
ChannelSftp c = init(path);
delete(path, c);
}
@@ -264,10 +376,11 @@ public class SftpStorage extends JavaFileStorageBase {
tryDisconnect(c);
throw convertException(e);
}
}
private List<FileEntry> listFiles(String path, ChannelSftp c) throws Exception {
try {
List<FileEntry> res = new ArrayList<FileEntry>();
@SuppressWarnings("rawtypes")
@@ -283,7 +396,7 @@ public class SftpStorage extends JavaFileStorageBase {
||(lsEntry.getFilename().equals(".."))
)
continue;
FileEntry fileEntry = new FileEntry();
fileEntry.displayName = lsEntry.getFilename();
fileEntry.path = createFilePath(path, fileEntry.displayName);
@@ -313,100 +426,192 @@ public class SftpStorage extends JavaFileStorageBase {
throws UnsupportedEncodingException {
return java.net.URLDecoder.decode(encodedString, UTF_8);
}
@Override
protected String encode(final String unencoded)
throws UnsupportedEncodingException {
return java.net.URLEncoder.encode(unencoded, UTF_8);
}
ChannelSftp init(String filename) throws JSchException, UnsupportedEncodingException {
jsch = new JSch();
ConnectionInfo ci = splitStringToConnectionInfo(filename);
Log.d("KP2AJFS", "init SFTP");
ChannelSftp init(ConnectionInfo cInfo) throws JSchException, UnsupportedEncodingException {
jsch = new JSch();
Log.d(TAG, "init SFTP");
String base_dir = getBaseDir();
jsch.setKnownHosts(base_dir + "/known_hosts");
String key_filename = getKeyFileName();
try{
createKeyPair(key_filename);
} catch (Exception ex) {
System.out.println(ex);
}
String key_filepath = _keyUtils.resolveKeyFilePath(jsch, cInfo.keyName);
try {
jsch.addIdentity(key_filename);
} catch (java.lang.Exception e)
{
jsch.addIdentity(key_filepath);
} catch (java.lang.Exception e) {
}
Log.e("KP2AJFS[thread]", "getting session...");
Session session = jsch.getSession(ci.username, ci.host, ci.port);
Log.e("KP2AJFS", "creating SftpUserInfo");
UserInfo ui = new SftpUserInfo(ci.password,_appContext);
session.setUserInfo(ui);
Log.e(THREAD_TAG, "getting session...");
Session session = jsch.getSession(cInfo.username, cInfo.host, cInfo.port);
session.setConfig("PreferredAuthentications", "publickey,password");
session.connect();
sessionConfigure(session, cInfo);
sessionConnect(session, cInfo);
Channel channel = session.openChannel("sftp");
channel.connect();
ChannelSftp c = (ChannelSftp) channel;
logDebug("success: init Sftp");
return c;
}
private void sessionConnect(Session session, ConnectionInfo cInfo) throws JSchException {
if (cInfo.connectTimeoutSec != UNSET_SFTP_CONNECT_TIMEOUT) {
session.connect(cInfo.connectTimeoutSec * 1000);
} else {
session.connect();
}
}
private void sessionConfigure(Session session, ConnectionInfo cInfo) {
Log.e(TAG, "creating SftpUserInfo");
UserInfo ui = new SftpUserInfo(cInfo.password, cInfo.keyPassphrase, _appContext);
session.setUserInfo(ui);
session.setConfig("PreferredAuthentications", "publickey,password");
for (Map.Entry<String, String> e : cInfo.configOpts.entrySet()) {
String cfgKey = e.getKey();
String before = session.getConfig(cfgKey);
String after = e.getValue();
if (SSH_CFG_CSV_EXPANDABLE.contains(cfgKey)) {
SshConfigCsvValueResolver resolver = new SshConfigCsvValueResolver(cfgKey, after);
after = resolver.resolve(before);
}
session.setConfig(cfgKey, after);
}
}
private String getBaseDir() {
return _appContext.getFilesDir().getAbsolutePath();
}
private String getKeyFileName() {
return getBaseDir() + "/id_kp2a_rsa";
public boolean deleteCustomKey(String keyName) throws FileNotFoundException {
return _keyUtils.deleteCustomKey(keyName);
}
public String[] getCustomKeyNames() {
return _keyUtils.getCustomKeyNames();
}
@SuppressWarnings("unused") // Exposed by JavaFileStorageBindings
public String createKeyPair() throws IOException, JSchException {
return createKeyPair(getKeyFileName());
return _keyUtils.createKeyPair(jsch);
}
private String createKeyPair(String key_filename) throws JSchException, IOException {
String public_key_filename = key_filename + ".pub";
File file = new File(key_filename);
if (file.exists())
return public_key_filename;
int type = KeyPair.RSA;
KeyPair kpair = KeyPair.genKeyPair(jsch, type, 4096);
kpair.writePrivateKey(key_filename);
@SuppressWarnings("unused") // Exposed by JavaFileStorageBindings
public void savePrivateKeyContent(String keyName, String keyContent) throws IOException, Exception {
_keyUtils.savePrivateKeyContent(keyName, keyContent);
}
kpair.writePublicKey(public_key_filename, "generated by Keepass2Android");
//ret = "Fingerprint: " + kpair.getFingerPrint();
kpair.dispose();
return public_key_filename;
@SuppressWarnings("unused") // Exposed by JavaFileStorageBindings
public void setJschLogging(boolean enabled, String logFilename) {
Logger impl = null;
if (enabled) {
if (logFilename != null) {
impl = Kp2aJSchLogger.createFileLogger(logFilename);
} else {
impl = Kp2aJSchLogger.createAndroidLogger();
}
}
JSch.setLogger(impl);
}
/**
* Exposed for testing purposes only.
* @param keyName
* @return
*/
public String sanitizeCustomKeyName(String keyName) {
return _keyUtils.getSanitizedCustomKeyName(keyName);
}
/**
* Exposed for testing purposes only.
* @param keyContent
* @return
* @throws Exception
*/
public String getValidatedCustomKeyContent(String keyContent) throws Exception {
return _keyUtils.getValidatedCustomKeyContent(keyContent);
}
/**
* Exposed for testing purposes only.
* @param currentValues
* @param spec
* @return
* @throws Exception
*/
public String resolveCsvValues(String currentValues, String spec) {
return new SshConfigCsvValueResolver("test", spec)
.resolve(currentValues);
}
public ConnectionInfo splitStringToConnectionInfo(String filename)
throws UnsupportedEncodingException {
ConnectionInfo ci = new ConnectionInfo();
ci.host = extractUserPwdHost(filename);
ci.host = extractUserPwdHostPort(filename);
String userPwd = ci.host.substring(0, ci.host.indexOf('@'));
ci.username = decode(userPwd.substring(0, userPwd.indexOf(":")));
ci.password = decode(userPwd.substring(userPwd.indexOf(":")+1));
int sepIdx = userPwd.indexOf(":");
if (sepIdx > 0) {
ci.username = decode(userPwd.substring(0, sepIdx));
ci.password = decode(userPwd.substring(sepIdx + 1));
} else {
ci.username = userPwd;
ci.password = null;
}
ci.host = ci.host.substring(ci.host.indexOf('@') + 1);
ci.port = DEFAULT_SFTP_PORT;
int portSeparatorIndex = ci.host.indexOf(":");
int portSeparatorIndex = ci.host.lastIndexOf(':');
if (portSeparatorIndex >= 0)
{
ci.port = Integer.parseInt(ci.host.substring(portSeparatorIndex+1));
ci.port = Integer.parseInt(ci.host.substring(portSeparatorIndex + 1));
ci.host = ci.host.substring(0, portSeparatorIndex);
}
// Encode/decode required to support IPv6 (colons break host:port parse logic)
// See Bug #2350
ci.host = decode(ci.host);
ci.localPath = extractSessionPath(filename);
Map<String, String> options = extractOptionsMap(filename);
if (options.containsKey(SFTP_CONNECT_TIMEOUT_OPTION_NAME)) {
String optVal = options.get(SFTP_CONNECT_TIMEOUT_OPTION_NAME);
try {
ci.connectTimeoutSec = Integer.parseInt(optVal);
} catch (NumberFormatException nan) {
logDebug(SFTP_CONNECT_TIMEOUT_OPTION_NAME + " option not a number: " + optVal);
}
}
if (options.containsKey(SFTP_KEYNAME_OPTION_NAME)) {
ci.keyName = options.get(SFTP_KEYNAME_OPTION_NAME);
}
if (options.containsKey(SFTP_KEYPASSPHRASE_OPTION_NAME)) {
ci.keyPassphrase = options.get(SFTP_KEYPASSPHRASE_OPTION_NAME);
}
for (String cfgKey : SSH_CFG_CSV_EXPANDABLE) {
if (options.containsKey(cfgKey)) {
ci.configOpts.put(cfgKey, options.get(cfgKey));
}
}
return ci;
}
@@ -443,12 +648,18 @@ public class SftpStorage extends JavaFileStorageBase {
try
{
ConnectionInfo ci = splitStringToConnectionInfo(path);
return getProtocolPrefix()+ci.username+"@"+ci.host+ci.localPath;
StringBuilder dName = new StringBuilder(getProtocolPrefix())
.append(ci.username)
.append("@")
.append(ci.host)
.append(ci.localPath);
appendOptions(dName, buildOptionMap(ci, false));
return dName.toString();
}
catch (Exception e)
{
return extractSessionPath(path);
}
}
}
@Override
@@ -470,22 +681,105 @@ public class SftpStorage extends JavaFileStorageBase {
@Override
public void onActivityResult(FileStorageSetupActivity activity,
int requestCode, int resultCode, Intent data) {
}
public String buildFullPath( String host, int port, String localPath, String username, String password) throws UnsupportedEncodingException
{
if (port != DEFAULT_SFTP_PORT)
host += ":"+String.valueOf(port);
return getProtocolPrefix()+encode(username)+":"+encode(password)+"@"+host+localPath;
public String buildFullPath(String host, int port, String localPath,
String username, String password,
int connectTimeoutSec,
String keyName, String keyPassphrase,
String kexAlgorithms, String shkAlgorithms)
throws UnsupportedEncodingException {
StringBuilder uri = new StringBuilder(getProtocolPrefix()).append(encode(username));
if (password != null) {
uri.append(":").append(encode(password));
}
uri.append("@");
// Encode/decode required to support IPv6 (colons break host:port parse logic)
// See Bug #2350
uri.append(encode(host));
if (port != DEFAULT_SFTP_PORT) {
uri.append(":").append(port);
}
if (localPath != null && localPath.startsWith("/")) {
uri.append(localPath);
}
appendOptions(uri, new OptionMapBuilder()
.addOption(SFTP_CONNECT_TIMEOUT_OPTION_NAME, connectTimeoutSec, cTimeoutResolver)
.addOption(SFTP_KEYNAME_OPTION_NAME, keyName, nonBlankStringResolver)
.addOption(SFTP_KEYPASSPHRASE_OPTION_NAME, keyPassphrase, nonBlankStringResolver)
.addOption(SSH_CFG_KEX, kexAlgorithms, nonBlankStringResolver)
.addOption(SSH_CFG_SERVER_HOST_KEY, shkAlgorithms, nonBlankStringResolver)
.build());
return uri.toString();
}
private void appendOptions(StringBuilder uri, Map<String, String> opts)
throws UnsupportedEncodingException {
boolean first = true;
// Sort for stability/consistency
Set<Map.Entry<String, String>> sortedEntries = new TreeSet<>(new EntryComparator<>());
sortedEntries.addAll(opts.entrySet());
for (Map.Entry<String, String> me : sortedEntries) {
if (first) {
uri.append("?");
first = false;
} else {
uri.append("&");
}
uri.append(encode(me.getKey())).append("=").append(encode(me.getValue()));
}
}
@Override
public void prepareFileUsage(Context appContext, String path) {
//nothing to do
}
/**
* A comparator that compares Map.Entry objects by their keys, via natural ordering.
*
* @param <T> the Map.Entry key type, that must implement Comparable.
*/
private static class EntryComparator<T extends Comparable<T>> implements Comparator<Map.Entry<T, ?>> {
@Override
public int compare(Map.Entry<T, ?> o1, Map.Entry<T, ?> o2) {
return o1.getKey().compareTo(o2.getKey());
}
}
private static class OptionMapBuilder {
private final Map<String, String> options = new HashMap<>();
/**
* Attempts to add a raw value <code>oVal</code> to the underlying option map with key <code>oName</code>
* iff the <code>resolver</code> produces a non-null output when invoked using the raw value.
*
* @param oName the name/key associated with the value, if added
* @param oVal the raw value attempting to be added
* @param resolver the resolver that determines if the value will be added
*
* @return OptionMapBuilder (updated)
* @param <T> the raw value type
*/
<T> OptionMapBuilder addOption(final String oName, T oVal, ValueResolver<T> resolver) {
String resolved = resolver.resolve(oVal);
if (resolved != null) {
options.put(oName, resolved);
}
return this;
}
Map<String, String> build() {
return new HashMap<>(options);
}
}
}

View File

@@ -116,17 +116,19 @@ public class SftpUserInfo implements UserInfo {
Context _appContext;
String _password;
String _passphrase;
public SftpUserInfo(String password, Context appContext)
public SftpUserInfo(String password, String passphrase, Context appContext)
{
_password = password;
_passphrase = passphrase;
_appContext = appContext;
}
@Override
public String getPassphrase() {
return null;
return _passphrase;
}
@Override
@@ -137,12 +139,12 @@ public class SftpUserInfo implements UserInfo {
@Override
public boolean promptPassword(String message) {
return true;
return _password != null;
}
@Override
public boolean promptPassphrase(String message) {
return false; //passphrase not supported
return _passphrase != null;
}
@Override

View File

@@ -0,0 +1,178 @@
package keepass2android.javafilestorage;
import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A class that manipulates CSV String values based on a list of CSV "spec" definitions, where each definition
* can describe one of the following:
*
* - Prepend to existing list: +something
* - Append to end of existing list: something+
* - Remove a specific value: -something
* - Remove values matching prefix: -something*
* - Remove values matching suffix: -*something
* - Remove values matching substring: -*something*
* - Remove values matching prefix and suffix: -some*thing
*
* Otherwise CSV of values completely replace original config values
*
* Examples:
* <code>
* var r = new SshConfigCsvValueResolver("foo", "addToEnd+,-remove*,+addToBeginning,-*del*");
* r.resolve("one,removeTwo,three,removeThree,four") --> "addToBeginning,one,three,four,addToEnd"
* r.resolve("one,my-del,del-me,two,foodelbar,three") --> "addToBeginning,one,two,three,addToEnd"
*
* r = new SshConfigCsvValueResolver("foo", "replace,the,config");
* r.resolve("one,two,three,four") --> "replace,the,config"
* </code>
*
*/
class SshConfigCsvValueResolver {
interface Matcher {
boolean matches(String s);
}
private final String cfgKey;
private static final String TAG = "KP2AJFS[sshcfg]";
private static final String DELIM = ",";
private static final char ADD = '+';
private static final char REMOVE = '-';
private static final char WILD = '*';
private final List<String> prepends;
private final List<String> appends;
private final List<Matcher> removes;
private final List<String> replaces;
/**
* Creates a new resolver.
*
* @param cfgKey - configuration key name (used for logging)
* @param incomingSpec - A CSV String of "spec" definitions that will be used to
* (potentially) modify incoming CSV String values.
*/
SshConfigCsvValueResolver(String cfgKey, String incomingSpec) {
List<String> prepends = new ArrayList<>();
List<String> appends = new ArrayList<>();
List<Matcher> removes = new ArrayList<>();
List<String> replaces = new ArrayList<>();
for (String iVal : incomingSpec.split(DELIM)) {
if (iVal.isBlank()) {
continue;
}
int evLen = iVal.length();
if (iVal.charAt(0) == ADD && evLen > 1) {
prepends.add(iVal.substring(1));
} else if (iVal.charAt(iVal.length() - 1) == ADD && evLen > 1) {
appends.add(iVal.substring(0, evLen - 1));
} else if (iVal.charAt(iVal.length() - 1) == REMOVE && evLen > 1) {
removes.add(createMatcher(iVal.substring(1)));
} else {
// This looks like a straight replace
replaces.add(iVal);
}
}
this.cfgKey = cfgKey;
this.prepends = Collections.unmodifiableList(prepends);
this.appends = Collections.unmodifiableList(appends);
this.removes = Collections.unmodifiableList(removes);
this.replaces = Collections.unmodifiableList(replaces);
}
/**
* Takes a CSV String and (potentially) modifies it according to the "spec" entries of this resolver.
*
* @param existingValues - the original CSV String
* @return an updated representation of <code>existingValues</code>, based on the defined "spec"
* entries of this resolver.
*/
public String resolve(String existingValues) {
List<String> newValues;
// If there's even one replace, it wins over everything and the rest is thrown out
if (!replaces.isEmpty()) {
if (!(prepends.isEmpty() || appends.isEmpty() || removes.isEmpty())) {
Log.w(TAG, "Discarded SSH cfg parts: key=" + cfgKey +
", prepends=" + prepends + ", appends=" + appends +
", removes=" + removes);
}
newValues = replaces;
} else {
// Otherwise we rebuild from existing and incoming values
newValues = createResolvedValues(existingValues);
}
return String.join(DELIM, newValues);
}
private List<String> createResolvedValues(String existingValues) {
List<String> newValues = new ArrayList<>(prepends);
for (String a : existingValues.split(DELIM)) {
if (!shouldRemove(a)) {
newValues.add(a);
}
}
newValues.addAll(appends);
return newValues;
}
private boolean shouldRemove(String s) {
s = normalize(s);
for (Matcher m : removes) {
if (m.matches(s)) {
return true;
}
}
return false;
}
private Matcher createMatcher(String val) {
final String v = normalize(val);
Matcher impl = s -> v.equals(s);
int wildcardIdx = v.indexOf(WILD);
if (wildcardIdx < 0) {
return impl;
}
// *blah *blah* blah* some*thing
// endsWith substring startsWith startsWith && endsWith
String subStr = null;
String suffix = null;
String prefix = null;
int vLen = v.length();
if (v.charAt(0) == WILD && vLen > 1) {
if (vLen > 2 && v.charAt(vLen - 1) == WILD) {
//substring
subStr = v.substring(1, vLen - 1);
} else {
// endsWith
suffix = v.substring(1);
}
} else if (v.charAt(vLen - 1) == WILD && vLen > 1) {
// beginsWith
prefix = v.substring(0, v.length() - 1);
} else if (wildcardIdx > 0) {
// startsWith && endsWith
prefix = v.substring(0, wildcardIdx);
suffix = v.substring(wildcardIdx + 1);
}
if (subStr != null) {
final String sub = subStr;
impl = s -> s.contains(sub);
} else if (prefix != null || suffix != null) {
final String pre = prefix;
final String suf = suffix;
impl = s -> (pre == null || s.startsWith(pre)) && (suf == null || s.endsWith(suf));
}
return impl;
}
private static String normalize(String s) {
return s == null ? null : s.toLowerCase();
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -148,6 +148,7 @@ import java.util.List;
import keepass2android.javafilestorage.GoogleDriveAppDataFileStorage;
import keepass2android.javafilestorage.JavaFileStorage;
import keepass2android.javafilestorage.JavaFileStorage.FileEntry;
import keepass2android.javafilestorage.PCloudFileStorage;
import keepass2android.javafilestorage.SftpStorage;
import keepass2android.javafilestorage.UserInteractionRequiredException;
import keepass2android.javafilestorage.WebDavStorage;
@@ -539,11 +540,11 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
static JavaFileStorage createStorageToTest(Context ctx, Context appContext, boolean simulateRestart) {
//storageToTest = new SftpStorage(ctx.getApplicationContext());
//storageToTest = new PCloudFileStorage(ctx, "yCeH59Ffgtm");
storageToTest = new PCloudFileStorage(ctx, "FLm22de7bdS", "pcloud", "pcloudtest");
//storageToTest = new SkyDriveFileStorage("000000004010C234", appContext);
storageToTest = new GoogleDriveAppDataFileStorage();
//storageToTest = new GoogleDriveAppDataFileStorage();
/*storageToTest = new WebDavStorage(new ICertificateErrorHandler() {
@Override
public boolean onValidationError(String error) {
@@ -690,6 +691,13 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
return sb.toString();
}
private void populateCsvMockValues(View view) {
EditText etSpecs = view.findViewById(R.id.mock_csv_specs);
etSpecs.setText("-bar,+first,-*d*");
EditText etCfgs = view.findViewById(R.id.mock_csv_cfg);
etCfgs.setText("foo,del1,bar,del2");
}
@Override
public void performManualFileSelect(boolean isForSave, final int requestCode,
String protocolId)
@@ -697,12 +705,13 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
if (protocolId.equals("sftp"))
{
final View view = getLayoutInflater().inflate(R.layout.sftp_credentials, null);
final SftpStorage sftpStorage = (SftpStorage)storageToTest;
populateCsvMockValues(view);
view.findViewById(R.id.send_public_key).setOnClickListener(v -> {
Intent sendIntent = new Intent();
SftpStorage sftpStorage = (SftpStorage)storageToTest;
try {
String pub_filename = sftpStorage.createKeyPair();
@@ -715,39 +724,140 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
}
catch (Exception ex)
{
Toast.makeText(this,"Failed to create key pair: " + ex.getMessage(), Toast.LENGTH_LONG);
return;
Toast.makeText(this,"Failed to create key pair: " + ex.getMessage(), Toast.LENGTH_LONG).show();
}
});
view.findViewById(R.id.list_private_keys).setOnClickListener(v -> {
String[] keys = sftpStorage.getCustomKeyNames();
Toast.makeText(this, "keys: " + String.join(",", keys), Toast.LENGTH_LONG).show();
});
view.findViewById(R.id.add_private_key).setOnClickListener(v -> {
EditText etKeyName = view.findViewById(R.id.private_key_name);
String keyName = etKeyName.getText().toString();
EditText etKeyContent = view.findViewById(R.id.private_key_content);
String keyContent = etKeyContent.getText().toString();
try {
sftpStorage.savePrivateKeyContent(keyName, keyContent);
Toast.makeText(this, "Add successful", Toast.LENGTH_LONG).show();
}
catch (Exception e) {
Toast.makeText(this, "Add failed: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
});
view.findViewById(R.id.delete_private_key).setOnClickListener(v -> {
EditText etKeyName = view.findViewById(R.id.private_key_name);
String keyName = etKeyName.getText().toString();
String exMessage = null;
boolean success = false;
try {
success = sftpStorage.deleteCustomKey(keyName);
}
catch (Exception e) {
exMessage = e.getMessage();
}
StringBuilder msg = new StringBuilder("Delete ");
msg.append(success ? "succeeded" : "FAILED");
if (exMessage != null) {
msg.append(" (").append(exMessage).append(")");
}
Toast.makeText(this, msg.toString(), Toast.LENGTH_LONG).show();
});
view.findViewById(R.id.validate_private_key).setOnClickListener(v -> {
EditText etKeyName = view.findViewById(R.id.private_key_name);
String inKeyName = etKeyName.getText().toString();
if (!inKeyName.isEmpty()) {
String keyResponse;
try {
keyResponse = sftpStorage.sanitizeCustomKeyName(inKeyName);
} catch (Exception e) {
keyResponse = "EX:" + e.getMessage();
}
String msg = "key: [" + inKeyName + "] -> [" + keyResponse + "]";
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
}
EditText etKeyContent = view.findViewById(R.id.private_key_content);
String inKeyContent = etKeyContent.getText().toString();
String msg;
if (!inKeyContent.isEmpty()) {
try {
// We could print the key, but I don't it's that helpful
sftpStorage.getValidatedCustomKeyContent(inKeyContent);
msg = "Key content is valid";
} catch (Exception e) {
msg = "Invalid key content: " + e.getMessage();
}
Toast.makeText(this, msg, Toast.LENGTH_LONG).show();
}
});
view.findViewById(R.id.resolve_mock_csv).setOnClickListener(v -> {
EditText etSpecs = view.findViewById(R.id.mock_csv_specs);
String specs = etSpecs.getText().toString();
EditText etCfg = view.findViewById(R.id.mock_csv_cfg);
String cfg = etCfg.getText().toString();
if (!specs.isBlank() && !cfg.isBlank()) {
String result = sftpStorage.resolveCsvValues(cfg, specs);
Toast.makeText(this, result, Toast.LENGTH_LONG).show();
}
});
view.findViewById(R.id.reset_mock_csv).setOnClickListener(v -> {
populateCsvMockValues(view);
});
new AlertDialog.Builder(this)
.setView(view)
.setTitle("Enter SFTP credentials")
.setPositiveButton("OK",new DialogInterface.OnClickListener() {
.setPositiveButton("OK", (dialog, which) -> {
@Override
public void onClick(DialogInterface dialog, int which) {
Toast.makeText(MainActivity.this, "Hey", Toast.LENGTH_LONG).show();
Toast.makeText(MainActivity.this, "Hey", Toast.LENGTH_LONG).show();
SftpStorage sftpStorage = (SftpStorage)storageToTest;
try {
EditText etHost = ((EditText)view.findViewById(R.id.sftp_host));
String host = etHost.getText().toString();
EditText etUser = ((EditText)view.findViewById(R.id.sftp_user));
String user = etUser.getText().toString();
EditText etPwd = ((EditText)view.findViewById(R.id.sftp_password));
String pwd = etPwd.getText().toString();
EditText etPort = ((EditText)view.findViewById(R.id.sftp_port));
int port = Integer.parseInt(etPort.getText().toString());
EditText etInitDir = ((EditText)view.findViewById(R.id.sftp_initial_dir));
String initialDir = etInitDir.getText().toString();
onReceivePathForFileSelect(requestCode, sftpStorage.buildFullPath( host, port, initialDir, user, pwd));
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
SftpStorage sftpStorage1 = (SftpStorage)storageToTest;
try {
EditText etHost = view.findViewById(R.id.sftp_host);
String host = etHost.getText().toString();
EditText etUser = view.findViewById(R.id.sftp_user);
String user = etUser.getText().toString();
EditText etPwd = view.findViewById(R.id.sftp_password);
String pwd = etPwd.getText().toString();
EditText etPort = view.findViewById(R.id.sftp_port);
int port = Integer.parseInt(etPort.getText().toString());
EditText etInitDir = view.findViewById(R.id.sftp_initial_dir);
String initialDir = etInitDir.getText().toString();
EditText etConnectTimeout = view.findViewById(R.id.sftp_connect_timeout);
int connectTimeout = SftpStorage.UNSET_SFTP_CONNECT_TIMEOUT;
String ctStr = etConnectTimeout.getText().toString();
if (!ctStr.isEmpty()) {
try {
int ct = Integer.parseInt(ctStr);
if (connectTimeout != ct) {
connectTimeout = ct;
}
} catch (NumberFormatException parseEx) {
}
}
EditText etKeyName = view.findViewById(R.id.private_key_name);
String keyName = etKeyName.getText().toString();
EditText etKeyPassphrase = view.findViewById(R.id.private_key_passphrase);
String keyPassphrase = etKeyPassphrase.getText().toString();
EditText etKex = view.findViewById(R.id.kex);
String kex = etKex.getText().toString();
EditText etShk = view.findViewById(R.id.shk);
String shk = etShk.getText().toString();
onReceivePathForFileSelect(requestCode, sftpStorage1.buildFullPath(
host, port, initialDir, user, pwd, connectTimeout,
keyName, keyPassphrase, kex, shk));
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
})
.create()

View File

@@ -3,69 +3,217 @@
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_margin="12dip"
>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<EditText
android:layout_margin="12dip">
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/sftp_host"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="10"
android:singleLine="true"
android:inputType="textNoSuggestions"
android:text=""
android:hint="@string/hint_sftp_host" />
<TextView
android:hint="@string/hint_sftp_host" />
<TextView
android:id="@+id/portsep"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=":" />
<EditText
<EditText
android:id="@+id/sftp_port"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="15"
android:singleLine="true"
android:inputType="number"
android:text="22"
android:hint="@string/hint_sftp_port" />
</LinearLayout>
<EditText
android:inputType="number"
android:text="22"
android:hint="@string/hint_sftp_port" />
<EditText
android:id="@+id/sftp_connect_timeout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="14"
android:singleLine="true"
android:inputType="number"
android:text=""
android:hint="@string/hint_sftp_connect_timeout" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<EditText
android:id="@+id/sftp_user"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true"
android:text=""
android:hint="@string/hint_username" />
<EditText
android:hint="@string/hint_username" />
<EditText
android:id="@+id/sftp_password"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:inputType="textPassword"
android:singleLine="true"
android:text=""
android:hint="@string/hint_pass"
android:importantForAccessibility="no" />
<TextView android:id="@+id/initial_dir"
android:hint="@string/hint_pass"
android:importantForAccessibility="no" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView android:id="@+id/initial_dir"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dip"
android:layout_marginTop="4dip"
android:text="@string/initial_directory" />
<EditText
<EditText
android:id="@+id/sftp_initial_dir"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="8dip"
android:singleLine="true"
android:text="/home/philipp"
/>
<Button android:id="@+id/send_public_key"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="send public key" />
android:text="/home/philipp"
/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<EditText android:id="@+id/kex"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true"
android:inputType="textNoSuggestions"
android:text=""
android:hint="KEX Algs" />
<EditText android:id="@+id/shk"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true"
android:inputType="textNoSuggestions"
android:text=""
android:hint="Server Host Key Algs" />
</LinearLayout>
<Button android:id="@+id/send_public_key"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="send public key" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:layout_marginTop="15dp"
android:textStyle="bold"
android:text="Private Keys Functions" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Button android:id="@+id/list_private_keys"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="List" />
<Button android:id="@+id/add_private_key"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Add" />
<Button android:id="@+id/delete_private_key"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Delete" />
<Button android:id="@+id/validate_private_key"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="Validate" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<EditText android:id="@+id/private_key_name"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true"
android:inputType="textNoSuggestions"
android:text=""
android:hint="key name" />
<EditText android:id="@+id/private_key_passphrase"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true"
android:inputType="textNoSuggestions"
android:text=""
android:hint="passphrase (optional)" />
</LinearLayout>
<EditText android:id="@+id/private_key_content"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:lines="4"
android:text=""
android:hint="key content" />
<TextView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:layout_marginTop="15dp"
android:textStyle="bold"
android:text="CSV Resolver Functions" />
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<EditText android:id="@+id/mock_csv_specs"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true"
android:inputType="textNoSuggestions"
android:text=""
android:hint="Test specs" />
<EditText android:id="@+id/mock_csv_cfg"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:singleLine="true"
android:inputType="textNoSuggestions"
android:text=""
android:hint="Test config" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<Button android:id="@+id/reset_mock_csv"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginLeft="50dp"
android:layout_marginRight="5dp"
android:text="Reset" />
<Button android:id="@+id/resolve_mock_csv"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginRight="50dp"
android:layout_marginLeft="5dp"
android:text="Resolve" />
</LinearLayout>
</LinearLayout>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -333,6 +333,7 @@
<string name="hint_sftp_host">host</string>
<string name="hint_sftp_port">port</string>
<string name="hint_sftp_connect_timeout">timeout sec</string>
<string name="select_storage_type">Select the storage type:</string>
@@ -524,6 +525,6 @@ Initial public release
<item>Do not accept invalid certificates</item>
</string-array>
<string name="initial_directory">Initial directory (optional):</string>
<string name="initial_directory">Initial dir (optional):</string>
</resources>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 511 B

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 760 B

After

Width:  |  Height:  |  Size: 634 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 913 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 730 B

After

Width:  |  Height:  |  Size: 434 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 940 B

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 461 B

After

Width:  |  Height:  |  Size: 277 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 B

After

Width:  |  Height:  |  Size: 192 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 498 B

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 811 B

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 715 B

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1001 B

After

Width:  |  Height:  |  Size: 577 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 892 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 745 B

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 903 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 833 B

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 B

After

Width:  |  Height:  |  Size: 166 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 807 B

After

Width:  |  Height:  |  Size: 507 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 690 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 681 B

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 548 B

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 438 B

After

Width:  |  Height:  |  Size: 274 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 B

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 972 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 379 B

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 301 B

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 B

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 B

After

Width:  |  Height:  |  Size: 214 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 388 B

After

Width:  |  Height:  |  Size: 227 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 244 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 B

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 417 B

After

Width:  |  Height:  |  Size: 246 B

Some files were not shown because too many files have changed in this diff Show More