Compare commits

..

3 Commits

Author SHA1 Message Date
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
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
40 changed files with 343 additions and 1985 deletions

View File

@@ -1,72 +0,0 @@
# 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.

View File

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

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";
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

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

@@ -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,28 +97,7 @@ 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

@@ -1,101 +0,0 @@
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
@@ -228,11 +238,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 +289,7 @@ public class PCloudFileStorage extends JavaFileStorageBase
}
private ApiClient createApiClientFromSharedPrefs() {
SharedPreferences prefs = this.ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
SharedPreferences prefs = this.ctx.getSharedPreferences(sharedPrefPrefix + SHARED_PREF_NAME, Context.MODE_PRIVATE);
String authToken = prefs.getString(SHARED_PREF_AUTH_TOKEN, null);
String apiHost = prefs.getString(SHARED_PREF_API_HOST, null);
return this.createApiClient(authToken, apiHost);

View File

@@ -1,216 +0,0 @@
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,24 +2,20 @@ 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.Logger;
import com.jcraft.jsch.KeyPair;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;
@@ -30,38 +26,10 @@ 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;
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;
JSch jsch;
public class ConnectionInfo
{
@@ -69,42 +37,13 @@ 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";
@@ -126,15 +65,15 @@ public class SftpStorage extends JavaFileStorageBase {
@Override
public InputStream openFileForRead(String path) throws Exception {
ConnectionInfo cInfo = splitStringToConnectionInfo(path);
ChannelSftp c = init(cInfo);
ChannelSftp c = init(path);
try {
byte[] buff = new byte[8000];
int bytesRead = 0;
InputStream in = c.get(cInfo.localPath);
InputStream in = c.get(extractSessionPath(path));
ByteArrayOutputStream bao = new ByteArrayOutputStream();
while ((bytesRead = in.read(buff)) != -1) {
@@ -166,15 +105,14 @@ public class SftpStorage extends JavaFileStorageBase {
public void uploadFile(String path, byte[] data, boolean writeTransactional)
throws Exception {
ConnectionInfo cInfo = splitStringToConnectionInfo(path);
ChannelSftp c = init(cInfo);
ChannelSftp c = init(path);
try {
InputStream in = new ByteArrayInputStream(data);
String targetPath = cInfo.localPath;
String targetPath = extractSessionPath(path);
if (writeTransactional)
{
//upload to temporary location:
String tmpPath = targetPath + ".tmp";
String tmpPath = targetPath+".tmp";
c.put(in, tmpPath);
//remove previous file:
try
@@ -190,9 +128,9 @@ public class SftpStorage extends JavaFileStorageBase {
}
else
{
c.put(in, targetPath);
c.put(in, targetPath);
}
tryDisconnect(c);
} catch (Exception e) {
tryDisconnect(c);
@@ -204,98 +142,53 @@ public class SftpStorage extends JavaFileStorageBase {
@Override
public String createFolder(String parentPath, String newDirName)
throws Exception {
ConnectionInfo cInfo = splitStringToConnectionInfo(parentPath);
try {
ChannelSftp c = init(cInfo);
String newPath = concatPaths(cInfo.localPath, newDirName);
c.mkdir(newPath);
tryDisconnect(c);
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));
try {
ChannelSftp c = init(parentPath);
String newPath = concatPaths(parentPath, newDirName);
c.mkdir(extractSessionPath(newPath));
tryDisconnect(c);
return newPath;
} 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());
int pathStartIdx = withoutProtocol.indexOf("/");
int pathEndIdx = withoutProtocol.indexOf("?");
if (pathEndIdx < 0) {
pathEndIdx = withoutProtocol.length();
}
return withoutProtocol.substring(pathStartIdx, pathEndIdx);
return withoutProtocol.substring(withoutProtocol.indexOf("/"));
}
private Map<String, String> extractOptionsMap(String path) throws UnsupportedEncodingException {
private String extractUserPwdHost(String path) {
String withoutProtocol = path
.substring(getProtocolPrefix().length());
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;
return withoutProtocol.substring(0,withoutProtocol.indexOf("/"));
}
private String concatPaths(String parentPath, String newDirName) {
StringBuilder fp = new StringBuilder(parentPath);
if (!parentPath.endsWith("/"))
fp.append("/");
return fp.append(newDirName).toString();
String res = parentPath;
if (!res.endsWith("/"))
res += "/";
res += newDirName;
return res;
}
@Override
public String createFilePath(final String parentUri, String newFileName)
public String createFilePath(String parentPath, String newFileName)
throws Exception {
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;
if (parentPath.endsWith("/") == false)
parentPath += "/";
return parentPath + newFileName;
}
@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) {
@@ -319,27 +212,23 @@ 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 {
ConnectionInfo cInfo = splitStringToConnectionInfo(filename);
ChannelSftp c = init(cInfo);
ChannelSftp c = init(filename);
try {
FileEntry fileEntry = new FileEntry();
SftpATTRS attr = c.stat(cInfo.localPath);
String sessionPath = extractSessionPath(filename);
SftpATTRS attr = c.stat(sessionPath);
setFromAttrs(fileEntry, attr);
// Full URI
fileEntry.path = filename;
fileEntry.displayName = getFilename(cInfo.localPath);
fileEntry.displayName = getFilename(sessionPath);
tryDisconnect(c);
return fileEntry;
} catch (Exception e) {
logDebug("Exception in getFileEntry! " + e);
@@ -350,9 +239,8 @@ 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);
}
@@ -376,11 +264,10 @@ 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")
@@ -396,7 +283,7 @@ public class SftpStorage extends JavaFileStorageBase {
||(lsEntry.getFilename().equals(".."))
)
continue;
FileEntry fileEntry = new FileEntry();
fileEntry.displayName = lsEntry.getFilename();
fileEntry.path = createFilePath(path, fileEntry.displayName);
@@ -426,161 +313,97 @@ 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(ConnectionInfo cInfo) throws JSchException, UnsupportedEncodingException {
ChannelSftp init(String filename) throws JSchException, UnsupportedEncodingException {
jsch = new JSch();
ConnectionInfo ci = splitStringToConnectionInfo(filename);
Log.d(TAG, "init SFTP");
Log.d("KP2AJFS", "init SFTP");
String base_dir = getBaseDir();
jsch.setKnownHosts(base_dir + "/known_hosts");
String key_filepath = _keyUtils.resolveKeyFilePath(jsch, cInfo.keyName);
String key_filename = getKeyFileName();
try{
createKeyPair(key_filename);
} catch (Exception ex) {
System.out.println(ex);
}
try {
jsch.addIdentity(key_filepath);
} catch (java.lang.Exception e) {
jsch.addIdentity(key_filename);
} catch (java.lang.Exception e)
{
}
Log.e(THREAD_TAG, "getting session...");
Session session = jsch.getSession(cInfo.username, cInfo.host, cInfo.port);
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);
sessionConfigure(session, cInfo);
sessionConnect(session, cInfo);
session.setConfig("PreferredAuthentications", "publickey,password");
session.connect();
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();
}
public boolean deleteCustomKey(String keyName) throws FileNotFoundException {
return _keyUtils.deleteCustomKey(keyName);
private String getKeyFileName() {
return getBaseDir() + "/id_kp2a_rsa";
}
public String[] getCustomKeyNames() {
return _keyUtils.getCustomKeyNames();
}
@SuppressWarnings("unused") // Exposed by JavaFileStorageBindings
public String createKeyPair() throws IOException, JSchException {
return _keyUtils.createKeyPair(jsch);
return createKeyPair(getKeyFileName());
}
@SuppressWarnings("unused") // Exposed by JavaFileStorageBindings
public void savePrivateKeyContent(String keyName, String keyContent) throws IOException, Exception {
_keyUtils.savePrivateKeyContent(keyName, keyContent);
}
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 setJschLogging(boolean enabled, String logFilename) {
Logger impl = null;
if (enabled) {
if (logFilename != null) {
impl = Kp2aJSchLogger.createFileLogger(logFilename);
} else {
impl = Kp2aJSchLogger.createAndroidLogger();
}
}
JSch.setLogger(impl);
}
kpair.writePublicKey(public_key_filename, "generated by Keepass2Android");
//ret = "Fingerprint: " + kpair.getFingerPrint();
kpair.dispose();
return public_key_filename;
/**
* 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 = extractUserPwdHostPort(filename);
ci.host = extractUserPwdHost(filename);
String userPwd = ci.host.substring(0, ci.host.indexOf('@'));
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.username = decode(userPwd.substring(0, userPwd.indexOf(":")));
ci.password = decode(userPwd.substring(userPwd.indexOf(":")+1));
ci.host = ci.host.substring(ci.host.indexOf('@') + 1);
ci.port = DEFAULT_SFTP_PORT;
int portSeparatorIndex = ci.host.lastIndexOf(':');
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)
@@ -588,30 +411,6 @@ public class SftpStorage extends JavaFileStorageBase {
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;
}
@@ -648,18 +447,12 @@ public class SftpStorage extends JavaFileStorageBase {
try
{
ConnectionInfo ci = splitStringToConnectionInfo(path);
StringBuilder dName = new StringBuilder(getProtocolPrefix())
.append(ci.username)
.append("@")
.append(ci.host)
.append(ci.localPath);
appendOptions(dName, buildOptionMap(ci, false));
return dName.toString();
return getProtocolPrefix()+ci.username+"@"+ci.host+ci.localPath;
}
catch (Exception e)
{
return extractSessionPath(path);
}
}
}
@Override
@@ -681,105 +474,26 @@ 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,
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("@");
public String buildFullPath( String host, int port, String localPath, String username, String password) throws UnsupportedEncodingException
{
// Encode/decode required to support IPv6 (colons break host:port parse logic)
// See Bug #2350
uri.append(encode(host));
host = 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()));
}
if (port != DEFAULT_SFTP_PORT)
host += ":"+String.valueOf(port);
return getProtocolPrefix()+encode(username)+":"+encode(password)+"@"+host+localPath;
}
@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,19 +116,17 @@ public class SftpUserInfo implements UserInfo {
Context _appContext;
String _password;
String _passphrase;
public SftpUserInfo(String password, String passphrase, Context appContext)
public SftpUserInfo(String password, Context appContext)
{
_password = password;
_passphrase = passphrase;
_appContext = appContext;
}
@Override
public String getPassphrase() {
return _passphrase;
return null;
}
@Override
@@ -139,12 +137,12 @@ public class SftpUserInfo implements UserInfo {
@Override
public boolean promptPassword(String message) {
return _password != null;
return true;
}
@Override
public boolean promptPassphrase(String message) {
return _passphrase != null;
return false; //passphrase not supported
}
@Override

View File

@@ -1,178 +0,0 @@
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();
}
}

View File

@@ -538,12 +538,12 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
}
static JavaFileStorage createStorageToTest(Context ctx, Context appContext, boolean simulateRestart) {
storageToTest = new SftpStorage(ctx.getApplicationContext());
//storageToTest = new SftpStorage(ctx.getApplicationContext());
//storageToTest = new PCloudFileStorage(ctx, "yCeH59Ffgtm");
//storageToTest = new SkyDriveFileStorage("000000004010C234", appContext);
//storageToTest = new GoogleDriveAppDataFileStorage();
storageToTest = new GoogleDriveAppDataFileStorage();
/*storageToTest = new WebDavStorage(new ICertificateErrorHandler() {
@Override
public boolean onValidationError(String error) {
@@ -690,13 +690,6 @@ 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)
@@ -704,13 +697,12 @@ 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();
@@ -723,140 +715,39 @@ public class MainActivity extends Activity implements JavaFileStorage.FileStorag
}
catch (Exception ex)
{
Toast.makeText(this,"Failed to create key pair: " + ex.getMessage(), Toast.LENGTH_LONG).show();
Toast.makeText(this,"Failed to create key pair: " + ex.getMessage(), Toast.LENGTH_LONG);
return;
}
});
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", (dialog, which) -> {
.setPositiveButton("OK",new DialogInterface.OnClickListener() {
Toast.makeText(MainActivity.this, "Hey", Toast.LENGTH_LONG).show();
@Override
public void onClick(DialogInterface dialog, int which) {
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) {
}
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();
}
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,217 +3,69 @@
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" />
<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:inputType="number"
android:text="22"
android:hint="@string/hint_sftp_port" />
</LinearLayout>
<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" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView android:id="@+id/initial_dir"
android:hint="@string/hint_pass"
android:importantForAccessibility="no" />
<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"
/>
</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>
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" />
</LinearLayout>

View File

@@ -333,7 +333,6 @@
<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>
@@ -525,6 +524,6 @@ Initial public release
<item>Do not accept invalid certificates</item>
</string-array>
<string name="initial_directory">Initial dir (optional):</string>
<string name="initial_directory">Initial directory (optional):</string>
</resources>

View File

@@ -221,6 +221,7 @@ public class FileChooserActivity extends FragmentActivity {
public static final String EXTRA_RESULT_FILE_EXISTS = CLASSNAME + ".result_file_exists";
/*
* CONTROLS

View File

@@ -269,6 +269,7 @@ public class FragmentFiles extends Fragment implements
FileChooserActivity.EXTRA_MAX_FILE_COUNT, 1000);
mFileAdapter = new BaseFileAdapter(getActivity(), mFilterMode,
mIsMultiSelection);
/*
* History.
@@ -2267,15 +2268,12 @@ public class FragmentFiles extends Fragment implements
}
if (mIsSaveDialog) {
String fileName = BaseFileProviderUtils.getFileName(cursor);
Uri uri = BaseFileProviderUtils.getUri(cursor);
mTextSaveas.setText(fileName);
mTextSaveas.setText(BaseFileProviderUtils.getFileName(cursor));
/*
* Always set tag after setting text, or tag will be reset to
* null.
*/
mTextSaveas.setTag(uri);
mTextSaveas.setTag(BaseFileProviderUtils.getUri(cursor));
}
if (mDoubleTapToChooseFiles) {
@@ -2288,12 +2286,10 @@ public class FragmentFiles extends Fragment implements
if (mIsMultiSelection)
return;
if (mIsSaveDialog) {
if (mIsSaveDialog)
checkSaveasFilenameAndFinish();
}
else {
else
finish(BaseFileProviderUtils.getUri(cursor));
}
}// single tap to choose files
}// onItemClick()
};// mViewFilesOnItemClickListener

View File

@@ -15,24 +15,4 @@ public class FileEntry {
isDirectory = false;
canRead = canWrite = true;
}
@Override
public String toString() {
StringBuilder s = new StringBuilder("kp2afilechooser.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);
}
return s.append("}").toString();
}
}

View File

@@ -20,7 +20,6 @@ public class Kp2aFileChooserBridge {
.buildUpon()
.appendPath(defaultPath)
.build());
return intent;
}
}

View File

@@ -306,9 +306,10 @@ public abstract class Kp2aFileProvider extends BaseFileProvider {
String parentPath = getParentPath(path);
if (parentPath == null) {
if (parentPath == null)
{
if (Utils.doLog())
Log.d(CLASSNAME, "parent file is null");
Log.d(CLASSNAME, "parent file is null");
return null;
}
FileEntry e;
@@ -500,10 +501,10 @@ public abstract class Kp2aFileProvider extends BaseFileProvider {
RowBuilder newRow = matrixCursor.newRow();
newRow.add(id);// _ID
newRow.add(BaseFile
.genContentIdUriBase(
getAuthority())
.buildUpon().appendPath(f.path)
.build().toString());
.genContentIdUriBase(
getAuthority())
.buildUpon().appendPath(f.path)
.build().toString());
newRow.add(f.path);
if (f.displayName == null)
Log.w("KP2AJ", "displayName is null for " + f.path);
@@ -548,7 +549,7 @@ public abstract class Kp2aFileProvider extends BaseFileProvider {
//puts the file entry in the cache for later reuse with retrieveFileInfo
private void updateFileEntryCache(FileEntry f) {
if (f != null)
fileEntryMap.put(f.path, f);
fileEntryMap.put(f.path, f);
}
//removes the file entry from the cache (if cached). Should be called whenever the file changes
private void removeFromCache(String filename, boolean recursive) {
@@ -583,7 +584,7 @@ public abstract class Kp2aFileProvider extends BaseFileProvider {
//returns the file entry from the cache if present or queries the concrete provider method to return the file info
private FileEntry getFileEntryCached(String filename) {
//check if entry is cached:
//check if enry is cached:
FileEntry cachedEntry = fileEntryMap.get(filename);
if (cachedEntry != null)
{
@@ -727,7 +728,7 @@ public abstract class Kp2aFileProvider extends BaseFileProvider {
if (targetParent != null && targetParent.startsWith(source))
{
if (Utils.doLog())
Log.d("KP2A_FC_P", source + " is parent of " + target);
Log.d("KP2A_FC_P", source+" is parent of "+target);
return BaseFileProviderUtils.newClosedCursor();
}
if (Utils.doLog())
@@ -767,37 +768,28 @@ public abstract class Kp2aFileProvider extends BaseFileProvider {
private String getParentPath(String path)
{
String params = null;
int paramsIdx = path.lastIndexOf("?");
if (paramsIdx > 0) {
params = path.substring(paramsIdx);
path = path.substring(0, paramsIdx);
}
path = removeTrailingSlash(path);
if (path.indexOf("://") == -1)
{
Log.d("KP2A_FC_P", "invalid path: " + path);
return null;
}
String pathWithoutProtocol = path.substring(path.indexOf("://") + 3);
int lastSlashPos = path.lastIndexOf("/");
if (pathWithoutProtocol.indexOf("/") == -1)
{
Log.d("KP2A_FC_P", "parent of " + path + " is null");
return null;
}
else
{
String parent = path.substring(0, lastSlashPos) + "/";
if (params != null) {
parent += params;
}
Log.d("KP2A_FC_P", "parent of " + path +" is " + parent);
return parent;
}
path = removeTrailingSlash(path);
if (path.indexOf("://") == -1)
{
Log.d("KP2A_FC_P", "invalid path: " + path);
return null;
}
String pathWithoutProtocol = path.substring(path.indexOf("://")+3);
int lastSlashPos = path.lastIndexOf("/");
if (pathWithoutProtocol.indexOf("/") == -1)
{
Log.d("KP2A_FC_P", "parent of " + path +" is null");
return null;
}
else
{
String parent = path.substring(0, lastSlashPos)+"/";
Log.d("KP2A_FC_P", "parent of " + path +" is "+parent);
return parent;
}
}
protected abstract FileEntry getFileEntry(String path, StringBuilder errorMessageBuilder) throws Exception;

View File

@@ -48,8 +48,6 @@ using KeePassLib.Serialization;
using PluginTOTP;
using File = Java.IO.File;
using Uri = Android.Net.Uri;
using keepass2android.fileselect;
using Boolean = Java.Lang.Boolean;
namespace keepass2android
{
@@ -556,90 +554,21 @@ namespace keepass2android
}
}
public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
{
if (permissions.Length == 1 && permissions.First() == Android.Manifest.Permission.PostNotifications &&
grantResults.First() == Permission.Granted)
{
StartNotificationsServiceAfterPermissionsCheck(requestCode == 1 /*requestCode is used to transfer this flag*/);
}
internal void StartNotificationsService(bool activateKeyboard)
{
Intent showNotIntent = new Intent(this, typeof (CopyToClipboardService));
showNotIntent.SetAction(Intents.ShowNotification);
showNotIntent.PutExtra(KeyEntry, new ElementAndDatabaseId(App.Kp2a.CurrentDb, Entry).FullId);
AppTask.PopulatePasswordAccessServiceIntent(showNotIntent);
showNotIntent.PutExtra(KeyActivateKeyboard, activateKeyboard);
base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
}
internal void StartNotificationsService(bool activateKeyboard)
{
if (PreferenceManager.GetDefaultSharedPreferences(this).GetBoolean(
GetString(Resource.String.CopyToClipboardNotification_key),
Resources.GetBoolean(Resource.Boolean.CopyToClipboardNotification_default)) == false
&& PreferenceManager.GetDefaultSharedPreferences(this).GetBoolean(
GetString(Resource.String.UseKp2aKeyboard_key),
Resources.GetBoolean(Resource.Boolean.UseKp2aKeyboard_default)) == false)
{
//notifications are disabled
return;
}
if ((int)Build.VERSION.SdkInt < 33 || CheckSelfPermission(Android.Manifest.Permission.PostNotifications) ==
Permission.Granted)
{
StartNotificationsServiceAfterPermissionsCheck(activateKeyboard);
return;
}
//user has not yet granted Android 13's POST_NOTIFICATONS permission for the app.
//check if we should ask them to grant:
if (!ShouldShowRequestPermissionRationale(Android.Manifest.Permission.PostNotifications) //this menthod returns false if we haven't asked yet or if the user has denied permission too often
&& PreferenceManager.GetDefaultSharedPreferences(this).GetBoolean("RequestedPostNotificationsPermission", false))//use a preference to tell the difference between "haven't asked yet" and "have asked too often"
{
//user has denied permission before. Do not show the dialog. User must give permission in the Android App settings.
return;
}
new AlertDialog.Builder(this)
.SetTitle(Resource.String.post_notifications_dialog_title)
.SetMessage(Resource.String.post_notifications_dialog_message)
.SetNegativeButton(Resource.String.post_notifications_dialog_disable, (sender, args) =>
{
//disable this dialog for the future by disabling the notification preferences
var edit= PreferenceManager.GetDefaultSharedPreferences(this).Edit();
edit.PutBoolean(GetString(Resource.String.CopyToClipboardNotification_key), false);
edit.PutBoolean(GetString(Resource.String.UseKp2aKeyboard_key), false);
edit.Commit();
})
.SetPositiveButton(Resource.String.post_notifications_dialog_allow, (sender, args) =>
{
//remember that we did ask for permission at least once:
var edit = PreferenceManager.GetDefaultSharedPreferences(this).Edit();
edit.PutBoolean("RequestedPostNotificationsPermission", true);
edit.Commit();
//request permission. user must grant, we'll show notifications in the OnRequestPermissionResults() callback
Android.Support.V4.App.ActivityCompat.RequestPermissions(this, new[] { Android.Manifest.Permission.PostNotifications }, activateKeyboard ? 1 : 0 /*use requestCode to transfer the flag*/);
StartService(showNotIntent);
}
})
.SetNeutralButton(Resource.String.post_notifications_dialog_notnow, (sender, args) => { })
.Show();
}
private void StartNotificationsServiceAfterPermissionsCheck(bool activateKeyboard)
{
Intent showNotIntent = new Intent(this, typeof(CopyToClipboardService));
showNotIntent.SetAction(Intents.ShowNotification);
showNotIntent.PutExtra(KeyEntry, new ElementAndDatabaseId(App.Kp2a.CurrentDb, Entry).FullId);
AppTask.PopulatePasswordAccessServiceIntent(showNotIntent);
showNotIntent.PutExtra(KeyActivateKeyboard, activateKeyboard);
StartService(showNotIntent);
}
private String getDateTime(DateTime dt)
private String getDateTime(DateTime dt)
{
return dt.ToLocalTime().ToString("g", CultureInfo.CurrentUICulture);
}

View File

@@ -42,12 +42,6 @@ namespace keepass2android
private readonly string _schemeSeparator = "://";
private bool _tryGetPermanentAccess;
private const int SftpModeSpinnerPasswd = 0;
private const int SftpModeSpinnerPubKey = 1;
private const int SftpModeSpinnerCustomKey = 2;
private const int SftpKeySpinnerCreateNewIdx = 0;
public string DefaultExtension { get; set; }
public FileSelectHelper(Activity activity, bool isForSave, bool tryGetPermanentAccess, int requestCode)
@@ -63,210 +57,71 @@ namespace keepass2android
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
View dlgContents = activity.LayoutInflater.Inflate(Resource.Layout.sftpcredentials, null);
var ctx = activity.ApplicationContext;
var fileStorage = new Keepass2android.Javafilestorage.SftpStorage(ctx);
LinearLayout addNewBtn = dlgContents.FindViewById<LinearLayout>(Resource.Id.sftp_add_key_group);
Button deleteBtn = dlgContents.FindViewById<Button>(Resource.Id.sftp_delete_key_button);
EditText keyNameTxt = dlgContents.FindViewById<EditText>(Resource.Id.sftp_key_name);
EditText keyContentTxt = dlgContents.FindViewById<EditText>(Resource.Id.sftp_key_content);
var keySpinner = dlgContents.FindViewById<Spinner>(Resource.Id.sftp_key_names);
var keyNamesAdapter = new ArrayAdapter(ctx, Android.Resource.Layout.SimpleSpinnerDropDownItem, new List<string>());
UpdatePrivateKeyNames(keyNamesAdapter, fileStorage, ctx);
keyNamesAdapter.SetDropDownViewResource(Android.Resource.Layout.SimpleSpinnerDropDownItem);
keySpinner.Adapter = keyNamesAdapter;
keySpinner.SetSelection(SftpKeySpinnerCreateNewIdx);
keySpinner.ItemSelected += (sender, args) =>
{
if (keySpinner.SelectedItemPosition == SftpKeySpinnerCreateNewIdx)
{
keyNameTxt.Text = "";
keyContentTxt.Text = "";
addNewBtn.Visibility = ViewStates.Visible;
deleteBtn.Visibility = ViewStates.Gone;
}
else
{
addNewBtn.Visibility = ViewStates.Gone;
deleteBtn.Visibility = ViewStates.Visible;
}
};
var authModeSpinner = dlgContents.FindViewById<Spinner>(Resource.Id.sftp_auth_mode_spinner);
dlgContents.FindViewById<Button>(Resource.Id.send_public_key_button).Click += (sender, args) =>
{
string pub_filename = fileStorage.CreateKeyPair();
Intent sendIntent = new Intent();
sendIntent.SetAction(Intent.ActionSend);
sendIntent.PutExtra(Intent.ExtraText, System.IO.File.ReadAllText(pub_filename));
sendIntent.PutExtra(Intent.ExtraSubject, "Keepass2Android sftp public key");
sendIntent.SetType("text/plain");
activity.StartActivity(Intent.CreateChooser(sendIntent, "Send public key to..."));
};
dlgContents.FindViewById<Button>(Resource.Id.sftp_save_key_button).Click += (sender, args) =>
{
string keyName = keyNameTxt.Text;
string keyContent = keyContentTxt.Text;
string toastMsg = null;
if (!string.IsNullOrEmpty(keyName) && !string.IsNullOrEmpty(keyContent))
{
try
{
fileStorage.SavePrivateKeyContent(keyName, keyContent);
keyNameTxt.Text = "";
keyContentTxt.Text = "";
toastMsg = ctx.GetString(Resource.String.private_key_saved);
}
catch (Exception e)
{
toastMsg = ctx.GetString(Resource.String.private_key_save_failed,
new Java.Lang.Object[] { e.Message });
}
}
else
{
toastMsg = ctx.GetString(Resource.String.private_key_info);
}
if (toastMsg!= null) {
Toast.MakeText(_activity, toastMsg, ToastLength.Long).Show();
}
UpdatePrivateKeyNames(keyNamesAdapter, fileStorage, ctx);
keySpinner.SetSelection(ResolveKeySpinnerSelection(keyNamesAdapter, keyName));
};
dlgContents.FindViewById<Button>(Resource.Id.sftp_delete_key_button).Click += (sender, args) =>
{
int selectedKey = dlgContents.FindViewById<Spinner>(Resource.Id.sftp_key_names).SelectedItemPosition;
string keyName = ResolveSelectedKeyName(keyNamesAdapter, selectedKey);
if (!string.IsNullOrEmpty(keyName))
{
bool deleted = fileStorage.DeleteCustomKey(keyName);
int msgId = deleted ? Resource.String.private_key_delete : Resource.String.private_key_delete_failed;
string msg = ctx.GetString(msgId, new Java.Lang.Object[] { keyName });
Toast.MakeText(_activity, msg, ToastLength.Long).Show();
UpdatePrivateKeyNames(keyNamesAdapter, fileStorage, ctx);
keySpinner.SetSelection(SftpKeySpinnerCreateNewIdx);
}
};
authModeSpinner.ItemSelected += (sender, args) =>
var spinner = dlgContents.FindViewById<Spinner>(Resource.Id.sftp_auth_mode_spinner);
dlgContents.FindViewById<Button>(Resource.Id.send_public_key_button).Click += (sender, args) =>
{
var passwordBox = dlgContents.FindViewById<EditText>(Resource.Id.sftp_password);
var publicKeyButton = dlgContents.FindViewById<Button>(Resource.Id.send_public_key_button);
var keyfileGroup = dlgContents.FindViewById<LinearLayout>(Resource.Id.sftp_keyfile_group);
var fileStorage = new Keepass2android.Javafilestorage.SftpStorage(activity.ApplicationContext);
string pub_filename = fileStorage.CreateKeyPair();
switch (authModeSpinner.SelectedItemPosition)
{
case SftpModeSpinnerPasswd:
passwordBox.Visibility = ViewStates.Visible;
publicKeyButton.Visibility = ViewStates.Gone;
keyfileGroup.Visibility = ViewStates.Gone;
break;
case SftpModeSpinnerPubKey:
passwordBox.Visibility = ViewStates.Gone;
publicKeyButton.Visibility = ViewStates.Visible;
keyfileGroup.Visibility = ViewStates.Gone;
break;
case SftpModeSpinnerCustomKey:
passwordBox.Visibility = ViewStates.Gone;
publicKeyButton.Visibility = ViewStates.Gone;
keyfileGroup.Visibility = ViewStates.Visible;
break;
}
Intent sendIntent = new Intent();
sendIntent.SetAction(Intent.ActionSend);
sendIntent.PutExtra(Intent.ExtraText, System.IO.File.ReadAllText(pub_filename));
sendIntent.PutExtra(Intent.ExtraSubject, "Keepass2Android sftp public key");
sendIntent.SetType("text/plain");
activity.StartActivity(Intent.CreateChooser(sendIntent, "Send public key to..."));
};
spinner.ItemSelected += (sender, args) =>
{
if (spinner.SelectedItemPosition == 0)
{
dlgContents.FindViewById<EditText>(Resource.Id.sftp_password).Visibility = ViewStates.Visible;
dlgContents.FindViewById<Button>(Resource.Id.send_public_key_button).Visibility = ViewStates.Gone;
}
else
{
dlgContents.FindViewById<EditText>(Resource.Id.sftp_password).Visibility = ViewStates.Gone;
dlgContents.FindViewById<Button>(Resource.Id.send_public_key_button).Visibility = ViewStates.Visible;
}
};
if (!defaultPath.EndsWith(_schemeSeparator))
{
var fileStorage = new Keepass2android.Javafilestorage.SftpStorage(activity.ApplicationContext);
SftpStorage.ConnectionInfo ci = fileStorage.SplitStringToConnectionInfo(defaultPath);
dlgContents.FindViewById<EditText>(Resource.Id.sftp_host).Text = ci.Host;
dlgContents.FindViewById<EditText>(Resource.Id.sftp_port).Text = ci.Port.ToString();
dlgContents.FindViewById<EditText>(Resource.Id.sftp_user).Text = ci.Username;
dlgContents.FindViewById<EditText>(Resource.Id.sftp_password).Text = ci.Password;
dlgContents.FindViewById<EditText>(Resource.Id.sftp_key_name).Text = ci.KeyName;
dlgContents.FindViewById<EditText>(Resource.Id.sftp_key_passphrase).Text = ci.KeyPassphrase;
dlgContents.FindViewById<EditText>(Resource.Id.sftp_initial_dir).Text = ci.LocalPath;
if (ci.ConnectTimeoutSec != SftpStorage.UnsetSftpConnectTimeout)
if (string.IsNullOrEmpty(ci.Password))
{
dlgContents.FindViewById<EditText>(Resource.Id.sftp_connect_timeout).Text = ci.ConnectTimeoutSec.ToString();
spinner.SetSelection(1);
}
if (ci.ConfigOpts.Contains(SftpStorage.SshCfgKex))
{
dlgContents.FindViewById<EditText>(Resource.Id.sftp_kex).Text = ci.ConfigOpts[SftpStorage.SshCfgKex].ToString();
}
if (ci.ConfigOpts.Contains(SftpStorage.SshCfgServerHostKey))
{
dlgContents.FindViewById<EditText>(Resource.Id.sftp_shk).Text = ci.ConfigOpts[SftpStorage.SshCfgServerHostKey].ToString();
}
if (!string.IsNullOrEmpty(ci.Password))
{
authModeSpinner.SetSelection(SftpModeSpinnerPasswd);
} else if (!string.IsNullOrEmpty(ci.KeyName))
{
authModeSpinner.SetSelection(SftpModeSpinnerCustomKey);
keySpinner.SetSelection(ResolveKeySpinnerSelection(keyNamesAdapter, ci.KeyName));
} else
{
authModeSpinner.SetSelection(SftpModeSpinnerPubKey);
}
}
builder.SetView(dlgContents);
builder.SetPositiveButton(Android.Resource.String.Ok, (sender, args) => {
int idx = 0;
string host = dlgContents.FindViewById<EditText>(Resource.Id.sftp_host).Text;
string portText = dlgContents.FindViewById<EditText>(Resource.Id.sftp_port).Text;
int port = SftpStorage.DefaultSftpPort;
if (!string.IsNullOrEmpty(portText))
int.TryParse(portText, out port);
string user = dlgContents.FindViewById<EditText>(Resource.Id.sftp_user).Text;
string password = null;
string keyName = null, keyPassphrase = null;
switch (dlgContents.FindViewById<Spinner>(Resource.Id.sftp_auth_mode_spinner).SelectedItemPosition)
{
case SftpModeSpinnerPasswd:
password = dlgContents.FindViewById<EditText>(Resource.Id.sftp_password).Text;
break;
case SftpModeSpinnerPubKey:
break;
case SftpModeSpinnerCustomKey:
keyName = ResolveSelectedKeyName(keyNamesAdapter, keySpinner.SelectedItemPosition);
keyPassphrase = dlgContents.FindViewById<EditText>(Resource.Id.sftp_key_passphrase).Text;
break;
}
string initialPath = dlgContents.FindViewById<EditText>(Resource.Id.sftp_initial_dir).Text;
if (string.IsNullOrEmpty(initialPath))
initialPath = "/";
string connectTimeoutText = dlgContents.FindViewById<EditText>(Resource.Id.sftp_connect_timeout).Text;
int connectTimeout = SftpStorage.UnsetSftpConnectTimeout;
if (!string.IsNullOrEmpty(connectTimeoutText))
{
int.TryParse(connectTimeoutText, out connectTimeout);
}
string kexAlgorithms = dlgContents.FindViewById<EditText>(Resource.Id.sftp_kex).Text;
string shkAlgorithms = dlgContents.FindViewById<EditText>(Resource.Id.sftp_shk).Text;
string sftpPath = fileStorage.BuildFullPath(
host, port, initialPath, user, password, connectTimeout, keyName, keyPassphrase,
kexAlgorithms, shkAlgorithms);
onStartBrowse(sftpPath);
});
EventHandler<DialogClickEventArgs> evtH = new EventHandler<DialogClickEventArgs>((sender, e) => onCancel());
builder.SetPositiveButton(Android.Resource.String.Ok,
(sender, args) =>
{
string host = dlgContents.FindViewById<EditText>(Resource.Id.sftp_host).Text;
string portText = dlgContents.FindViewById<EditText>(Resource.Id.sftp_port).Text;
int port = Keepass2android.Javafilestorage.SftpStorage.DefaultSftpPort;
if (!string.IsNullOrEmpty(portText))
int.TryParse(portText, out port);
string user = dlgContents.FindViewById<EditText>(Resource.Id.sftp_user).Text;
string password = dlgContents.FindViewById<EditText>(Resource.Id.sftp_password).Text;
string initialPath = dlgContents.FindViewById<EditText>(Resource.Id.sftp_initial_dir).Text;
if (string.IsNullOrEmpty(initialPath))
initialPath = "/";
string sftpPath = new Keepass2android.Javafilestorage.SftpStorage(activity.ApplicationContext).BuildFullPath(host, port, initialPath, user,
password);
onStartBrowse(sftpPath);
});
EventHandler<DialogClickEventArgs> evtH = new EventHandler<DialogClickEventArgs>((sender, e) => onCancel());
builder.SetNegativeButton(Android.Resource.String.Cancel, evtH);
builder.SetTitle(activity.GetString(Resource.String.enter_sftp_login_title));
@@ -274,41 +129,9 @@ namespace keepass2android
dialog.Show();
#endif
}
}
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
private void UpdatePrivateKeyNames(ArrayAdapter dataView, SftpStorage storage, Context ctx)
{
dataView.Clear();
dataView.Add(ctx.GetString(Resource.String.private_key_create_new));
foreach (string keyName in storage.GetCustomKeyNames())
dataView.Add(keyName);
}
private int ResolveKeySpinnerSelection(ArrayAdapter dataView, string keyName)
{
int idx = -1;
for (int i = 0; i < dataView.Count; i++)
{
string itemName = dataView.GetItem(i).ToString();
if (string.Equals(keyName, itemName)) {
idx = i;
break;
}
}
return idx < 0 ? SftpKeySpinnerCreateNewIdx : idx;
}
private string ResolveSelectedKeyName(ArrayAdapter dataView, int selectedItem)
{
if (selectedItem != SftpKeySpinnerCreateNewIdx && selectedItem > 0 && selectedItem < dataView.Count)
return dataView.GetItem(selectedItem).ToString();
else
return null;
}
#endif
private void ShowHttpDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath)
private void ShowHttpDialog(Activity activity, Util.FileSelectedHandler onStartBrowse, Action onCancel, string defaultPath)
{
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
AlertDialog.Builder builder = new AlertDialog.Builder(activity);

View File

@@ -21,7 +21,6 @@ using System.Collections.Generic;
using System.Linq;
using Android.App;
using Android.Content;
using Android.Content.PM;
using Android.OS;
using Android.Runtime;
using Android.Views;
@@ -58,9 +57,8 @@ namespace keepass2android
{ Resource.Id.child_db_infotext, 13 },
{ Resource.Id.fingerprint_infotext, 12 },
{ Resource.Id.autofill_infotext, 11 },
{ Resource.Id.notification_permission_infotext, 10 },
{ Resource.Id.notification_info_android8_infotext, 9 },
{ Resource.Id.infotext, 8 },
{ Resource.Id.notification_info_android8_infotext, 10 },
{ Resource.Id.infotext, 9 },
{ Resource.Id.select_other_entry, 20},
{ Resource.Id.add_url_entry, 20},
};
@@ -275,7 +273,6 @@ namespace keepass2android
UpdateFingerprintInfo();
UpdateAutofillInfo();
UpdateAndroid8NotificationInfo();
UpdatePostNotificationsPermissionInfo();
UpdateInfotexts();
RefreshIfDirty();
@@ -283,7 +280,6 @@ namespace keepass2android
SetSearchItemVisibility();
}
private void UpdateInfotexts()
{
@@ -389,31 +385,6 @@ namespace keepass2android
hasCalledOtherActivity = false;
}
private void UpdatePostNotificationsPermissionInfo(bool hideForever=false)
{
const string prefsKey = "DidShowNotificationPermissionInfo";
bool canShowNotificationInfo = ((int)Build.VERSION.SdkInt >= 33)
&& (!_prefs.GetBoolean(prefsKey, false)
&& (CheckSelfPermission(Android.Manifest.Permission.PostNotifications) !=
Permission.Granted)
&& (ShouldShowRequestPermissionRationale(Android.Manifest.Permission.PostNotifications) //this menthod returns false if we haven't asked yet or if the user has denied permission too often
|| !PreferenceManager.GetDefaultSharedPreferences(this).GetBoolean("RequestedPostNotificationsPermission", false))//use a preference to tell the difference between "haven't asked yet" and "have asked too often"
);
if ((canShowNotificationInfo) && hideForever)
{
_prefs.Edit().PutBoolean(prefsKey, true).Commit();
canShowNotificationInfo = false;
}
if (canShowNotificationInfo)
{
RegisterInfoTextDisplay("NotificationPermissionInfo"); //this ensures that we don't show the general info texts too soon
}
UpdateBottomBarElementVisibility(Resource.Id.notification_permission_infotext, canShowNotificationInfo);
}
private void UpdateAndroid8NotificationInfo(bool hideForever = false)
{
const string prefsKey = "DidShowAndroid8NotificationInfo";
@@ -635,25 +606,6 @@ namespace keepass2android
}
if (FindViewById(Resource.Id.post_notification_button_allow) != null)
{
FindViewById(Resource.Id.post_notification_button_allow).Click += (sender, args) =>
{
//remember that we did ask for permission at least once:
var edit = PreferenceManager.GetDefaultSharedPreferences(this).Edit();
edit.PutBoolean("RequestedPostNotificationsPermission", true);
edit.Commit();
Android.Support.V4.App.ActivityCompat.RequestPermissions(this, new[] { Android.Manifest.Permission.PostNotifications }, 0);
UpdatePostNotificationsPermissionInfo(true);
};
FindViewById(Resource.Id.post_notification_button_dont_show_again).Click += (sender, args) =>
{
UpdatePostNotificationsPermissionInfo(true);
};
}

View File

@@ -43,7 +43,7 @@
</queries>
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
<permission android:description="@string/permission_desc2" android:icon="@drawable/ic_notify_locked" android:label="KP2A entry search" android:name="keepass2android.keepass2android_debug.permission.KP2aInternalSearch" android:protectionLevel="signature" />
<permission android:description="@string/permission_desc3" android:icon="@drawable/ic_launcher" android:label="KP2A choose autofill dataset" android:name="keepass2android.keepass2android_debug.permission.Kp2aChooseAutofill" android:protectionLevel="signature" />
<application
@@ -258,7 +258,6 @@ The scheme=file is still there for old OS devices. It's also queried by apps lik
<uses-permission android:name="keepass2android.keepass2android_debug.permission.KP2aInternalFileBrowsing" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- Samsung Pass permission -->
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
<!-- READ_PHONE_STATE seems to come from some library or so, not clear where. We don't want to have it, remove it: -->

View File

@@ -42,8 +42,8 @@
<action android:name="android.intent.action.VIEW" />
</intent>
</queries>
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="33" />
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="31" />
<permission android:description="@string/permission_desc2" android:icon="@drawable/ic_launcher" android:label="KP2A entry search" android:name="keepass2android.keepass2android.permission.KP2aInternalSearch" android:protectionLevel="signature" />
<permission android:description="@string/permission_desc3" android:icon="@drawable/ic_launcher" android:label="KP2A choose autofill dataset" android:name="keepass2android.keepass2android.permission.Kp2aChooseAutofill" android:protectionLevel="signature" />
@@ -270,10 +270,6 @@ The scheme=file is still there for old OS devices. It's also queried by apps lik
<uses-permission android:name="keepass2android.keepass2android.permission.KP2aInternalSearch" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<!-- Samsung Pass permission -->
<uses-permission android:name="com.samsung.android.providers.context.permission.WRITE_USE_APP_FEATURE_SURVEY" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" />

View File

@@ -40,7 +40,7 @@
</intent>
</queries>
<uses-sdk android:minSdkVersion="18" android:targetSdkVersion="33" />
<uses-sdk android:minSdkVersion="18" android:targetSdkVersion="31" />
<permission android:description="@string/permission_desc2" android:icon="@drawable/ic_launcher_offline" android:label="KP2A entry search" android:name="keepass2android.keepass2android_nonet.permission.KP2aInternalSearch" android:protectionLevel="signature" />
<permission android:description="@string/permission_desc3" android:icon="@drawable/ic_launcher_offline" android:label="KP2A choose autofill dataset" android:name="keepass2android.keepass2android_nonet.permission.Kp2aChooseAutofill" android:protectionLevel="signature" />
<application
@@ -243,9 +243,6 @@ The scheme=file is still there for old OS devices. It's also queried by apps lik
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-permission android:name="keepass2android.keepass2android_nonet.permission.KP2aInternalFileBrowsing" />
<uses-permission android:name="keepass2android.keepass2android_nonet.permission.KP2aInternalSearch" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@@ -245,50 +245,6 @@
style="@style/BottomBarButton" />
</RelativeLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/notification_permission_infotext"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:orientation="vertical">
<TextView android:id="@+id/infotext" android:text="@string/PostNotificationsPermissionInfo_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:layout_margin="6dp"
android:layout_marginBottom="2dp"
/>
<RelativeLayout
android:id="@+id/notification_permissions_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:baselineAligned="false">
<Button
android:id="@+id/post_notification_button_allow"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:paddingTop="4dp"
android:text="@string/post_notifications_dialog_allow"
style="@style/BottomBarButton" />
<Button
android:id="@+id/post_notification_button_dont_show_again"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:paddingTop="4dp"
android:text="@string/dont_show_again"
style="@style/BottomBarButton" />
</RelativeLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/infotext"
android:layout_width="match_parent"

View File

@@ -61,80 +61,17 @@
</LinearLayout>
<EditText
android:id="@+id/sftp_password"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:singleLine="true"
android:hint="@string/hint_pass"
android:importantForAccessibility="no"/>
<Button
android:id="@+id/send_public_key_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/send_public_key" />
<LinearLayout
android:id="@+id/sftp_keyfile_group"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<LinearLayout
android:orientation="horizontal"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/private_key_select" />
<Spinner
android:id="@+id/sftp_key_names"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="3dp" />
</LinearLayout>
<LinearLayout
android:id="@+id/sftp_add_key_group"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="wrap_content">
<EditText android:id="@+id/sftp_key_name"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:hint="@string/hint_sftp_key_name" />
<EditText
android:id="@+id/sftp_key_content"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:minLines="1"
android:maxLines="6"
android:hint="@string/hint_sftp_key_content" />
<Button
android:id="@+id/sftp_save_key_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/save_key" />
</LinearLayout>
<Button
android:id="@+id/sftp_delete_key_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/delete_key" />
<EditText
android:id="@+id/sftp_key_passphrase"
android:id="@+id/sftp_password"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:singleLine="true"
android:inputType="textPassword"
android:hint="@string/hint_sftp_key_passphrase" />
</LinearLayout>
android:hint="@string/hint_pass"
android:importantForAccessibility="no"/>
<Button android:id="@+id/send_public_key_button"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="@string/send_public_key" />
<TextView android:id="@+id/initial_dir"
@@ -151,49 +88,6 @@
android:singleLine="true"
android:text="/"
/>
<TextView android:id="@+id/connect_timeout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dip"
android:layout_marginTop="4dip"
android:text="@string/connect_timeout" />
<EditText
android:id="@+id/sftp_connect_timeout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:inputType="number" />
<TextView android:id="@+id/sftp_kex_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dip"
android:layout_marginTop="4dip"
android:text="@string/sftp_kex_title" />
<EditText
android:id="@+id/sftp_kex"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textNoSuggestions"
android:hint="@string/hint_sftp_kex"
android:singleLine="true"
android:text=""
/>
<TextView android:id="@+id/sftp_shk_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="4dip"
android:layout_marginTop="4dip"
android:text="@string/sftp_shk_title" />
<EditText
android:id="@+id/sftp_shk"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:inputType="textNoSuggestions"
android:hint="@string/hint_sftp_shk"
android:singleLine="true"
android:text=""
/>
</LinearLayout>
</LinearLayout>

View File

@@ -98,7 +98,6 @@
<string name="TrayTotp_SeedField_key">TrayTotp_SeedField_key</string>
<string name="TrayTotp_prefs_key">TrayTotp_prefs_key</string>
<string name="DebugLog_key">DebugLog_key</string>
<string name="JSchDebug_key">JSchDebug_key</string>
<string name="DebugLog_prefs_key">DebugLog_prefs_key</string>
<string name="DebugLog_send_key">DebugLog_send</string>
<string name="AutofillDisabledQueriesPreference_key">AutofillDisabledQueriesPreference_key</string>

View File

@@ -167,7 +167,6 @@
<string name="hint_keyfile">key file</string>
<string name="hint_length">length</string>
<string name="hint_pass">password</string>
<string name="hint_keyfile_path">SSH private key path</string>
<string name="hint_login_pass">Password</string>
<string name="hint_title">name</string>
<string name="hint_url">URL</string>
@@ -484,10 +483,6 @@
<string name="IconVisibilityInfo_Android8_text">Android 8 has introduced new behavior for notifications. If you want to hide the icon for Keepass2Android\'s notifications, please configure this through the system settings. Set the importance of the notification category to Minimum.</string>
<string name="IconVisibilityInfo_Android8_btnSettings">Open settings</string>
<string name="PostNotificationsPermissionInfo_text">Keepass2Android can display a system notification while your database is not locked. For this to work, please grant permission.</string>
<string name="DontCare">I don\'t care</string>
<string name="DocumentAccessRevoked">The file is no longer accessible to Keepass2Android. Either it was removed or the access permissions have been revoked. Please use re-open the file, e.g. using Change database.</string>
@@ -594,27 +589,9 @@
<string name="hint_sftp_host">host (ex: 192.168.0.1)</string>
<string name="hint_sftp_port">port</string>
<string name="initial_directory">Initial directory (optional):</string>
<string name="connect_timeout">Connection timeout seconds (optional):"</string>
<string name="enter_sftp_login_title">Enter SFTP login data:</string>
<string name="sftp_auth_mode">Authentication mode</string>
<string name="send_public_key">Send public key...</string>
<string name="select_private_keyfile">Select private key...</string>
<string name="hint_sftp_key_name">New key name</string>
<string name="hint_sftp_key_content">New key content</string>
<string name="private_key_saved">Private key saved</string>
<string name="private_key_save_failed">FAILED to save private key: %1$s</string>
<string name="private_key_info">Enter key name and content to save</string>
<string name="private_key_delete">Deleted private key: %1$s</string>
<string name="private_key_delete_failed">FAILED to deleted private key: %1$s</string>
<string name="save_key">Save Private Key</string>
<string name="delete_key">Delete Private Key</string>
<string name="private_key_select">Selected private key</string>
<string name="private_key_create_new">[Add New...]</string>
<string name="hint_sftp_key_passphrase">Key passphrase (optional)</string>
<string name="sftp_kex_title">Key Exchange (KEX) Algorithm(s) (optional)</string>
<string name="hint_sftp_kex">"Comma-separated names/spec</string>
<string name="sftp_shk_title">Server Host Key Algorithm(s) (optional)</string>
<string name="hint_sftp_shk">"Comma-separated names/spec</string>
<string name="enter_ftp_login_title">Enter FTP login data:</string>
@@ -637,8 +614,9 @@
<string name="filestoragename_gdrive">Google Drive</string>
<string name="filestoragename_gdriveKP2A">Google Drive (KP2A files)</string>
<string name="filestoragehelp_gdriveKP2A">If you do not want to give KP2A access to your full Google Drive, you may select this option. Note that you need to create a database file first, existing files are not visible to the app. Either choose this option from the Create database screen or, if you already opened a database, by exporting the database choosing this option.</string>
<string name="filestoragename_pcloud">PCloud</string>
<string name="filestoragename_pcloud">PCloud (KP2A folder)</string>
<string name="filestoragehelp_pcloud">This storage type will only request access to the pCloud folder "Applications/Keepass2Android". If you want to use an existing database from your pCloud account, please make sure the file is placed in this pCloud folder.</string>
<string name="filestoragename_pcloudall">PCloud (Full access)</string>
<string name="filestoragename_onedrive">OneDrive</string>
<string name="filestoragename_onedrive2">OneDrive</string>
<string name="filestoragename_onedrive2_full">All files and shared files</string>
@@ -703,7 +681,6 @@
<string name="TrayTotp_prefs">TrayTotp</string>
<string name="DebugLog_prefs_prefs">Log-File for Debugging</string>
<string name="DebugLog_title">Use log file</string>
<string name="JSchDebug_title">SFTP debug logging</string>
<string name="DebugLog_summary">Write app output to a local log file</string>
<string name="DebugLog_send">Send debug log...</string>
@@ -1349,8 +1326,7 @@ Initial public release
</string-array>
<string-array name="sftp_auth_modes">
<item>Password</item>
<item>K2A Private/Public key</item>
<item>Custom Private key</item>
<item>Private/Public key</item>
</string-array>
<string-array name="AcceptAllServerCertificates_options">
<item>Ignore certificate validation failures</item>
@@ -1373,12 +1349,6 @@ Initial public release
<string name="enable_fingerprint_hint">Keepass2Android has detected biometric hardware. Do you want to enable Biometric Unlock for this database?</string>
<string name="post_notifications_dialog_title">Allow notifications</string>
<string name="post_notifications_dialog_message">Keepass2Android can show notifications with buttons to copy values like passwords and TOTPs to clipboard, or to bring up the built-in keyboard. This is useful to transfer values into other apps without switching to Keepass2Android repeatedly. Do you want to enable such notifications?</string>
<string name="post_notifications_dialog_allow">Allow notifications</string>
<string name="post_notifications_dialog_disable">Disable this feature</string>
<string name="post_notifications_dialog_notnow">Not now</string>
<string name="understand">I understand</string>
<string name="dont_show_again">Do not show again</string>

View File

@@ -670,17 +670,10 @@
android:defaultValue="false"
android:title="@string/DebugLog_title"
android:key="@string/DebugLog_key" />
<Preference
android:enabled="true"
android:title="@string/DebugLog_send"
android:key="@string/DebugLog_send_key" />
<CheckBoxPreference
android:enabled="true"
android:persistent="false"
android:defaultValue="false"
android:title="@string/JSchDebug_title"
android:key="@string/JSchDebug_key" />
</PreferenceScreen>
</PreferenceScreen>

View File

@@ -315,7 +315,7 @@ namespace keepass2android
}
else
{
//Android 8 requires that we call StartForeground() shortly after starting the service with StartForegroundService.
//Anrdoid 8 requires that we call StartForeground() shortly after starting the service with StartForegroundService.
//This is not possible when we're closing the service. In this case we don't use the StopSelf in the OngoingNotificationsService.OnStartCommand() anymore but directly stop the service.
OngoingNotificationsService.CancelNotifications(ctx); //The docs are not 100% clear if OnDestroy() will be called immediately. So make sure the notifications are up to date.
@@ -751,7 +751,8 @@ namespace keepass2android
new NetFtpFileStorage(LocaleManager.LocalizedAppContext, this),
new WebDavFileStorage(this),
new PCloudFileStorage(LocaleManager.LocalizedAppContext, this),
new MegaFileStorage(App.Context),
new PCloudFileStorageAll(LocaleManager.LocalizedAppContext, this),
new MegaFileStorage(App.Context),
//new LegacyWebDavStorage(this),
//new LegacyFtpStorage(this),
#endif

View File

@@ -1979,6 +1979,12 @@
<SubType>Designer</SubType>
</AndroidResource>
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-xhdpi\ic_storage_pcloudall.png" />
</ItemGroup>
<ItemGroup>
<AndroidResource Include="Resources\drawable-mdpi\ic_storage_pcloudall.png" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.

View File

@@ -178,12 +178,6 @@ namespace keepass2android
FindPreference(GetString(Resource.String.DebugLog_key)).PreferenceChange += OnDebugLogChanged;
FindPreference(GetString(Resource.String.DebugLog_send_key)).PreferenceClick += OnSendDebug;
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
FindPreference(GetString(Resource.String.JSchDebug_key)).PreferenceChange += OnJSchDebugChanged;
#else
FindPreference(GetString(Resource.String.JSchDebug_key)).Enabled = false;
#endif
HashSet<string> supportedLocales = new HashSet<string>() { "en", "af", "ar", "az", "be", "bg", "ca", "cs", "da", "de", "el", "es", "eu", "fa", "fi", "fr", "gl", "he", "hr", "hu", "id", "in", "it", "iw", "ja", "ko", "ml", "nb", "nl", "nn", "no", "pl", "pt", "ro", "ru", "si", "sk", "sl", "sr", "sv", "tr", "uk", "vi", "zh" };
var languagePref = (ListPreference)FindPreference(GetString(Resource.String.app_language_pref_key));
new AppLanguageManager(this, languagePref, supportedLocales);
@@ -387,37 +381,18 @@ namespace keepass2android
private void OnDebugLogChanged(object sender, Preference.PreferenceChangeEventArgs e)
{
if ((bool)e.NewValue)
Kp2aLog.CreateLogFile();
if ((bool)e.NewValue)
{
Kp2aLog.CreateLogFile();
}
else
Kp2aLog.FinishLogFile();
{
Kp2aLog.FinishLogFile();
}
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
bool jschLogEnable = PreferenceManager.GetDefaultSharedPreferences(Application.Context)
.GetBoolean(Application.Context.GetString(Resource.String.JSchDebug_key), false);
SetJSchLogging(jschLogEnable);
#endif
}
}
#if !EXCLUDE_JAVAFILESTORAGE && !NoNet
private void OnJSchDebugChanged(object sender, Preference.PreferenceChangeEventArgs e)
{
SetJSchLogging((bool)e.NewValue);
}
private void SetJSchLogging(bool enabled)
{
var sftpStorage = new Keepass2android.Javafilestorage.SftpStorage(Context);
string? logFilename = null;
if (Kp2aLog.LogToFile)
{
logFilename = Kp2aLog.LogFilename;
}
sftpStorage.SetJschLogging(enabled, logFilename);
}
#endif
private void AlgorithmPrefChange(object sender, Preference.PreferenceChangeEventArgs preferenceChangeEventArgs)
private void AlgorithmPrefChange(object sender, Preference.PreferenceChangeEventArgs preferenceChangeEventArgs)
{
var db = App.Kp2a.CurrentDb;
var previousCipher = db.KpDatabase.DataCipherUuid;
@@ -860,7 +835,7 @@ namespace keepass2android
#if DEBUG
preference.Enabled = (usageCount > 1);
#else
#else
preference.Enabled = (usageCount > 50);
#endif
preference.PreferenceChange += delegate(object sender, Preference.PreferenceChangeEventArgs args)