Backup: Use tasks for restore

This commit is contained in:
Jakob Nixdorf 2021-02-06 23:30:51 +01:00
parent e6e16999ae
commit dfc788cd29
No known key found for this signature in database
GPG key ID: BE99BF86574A7DBC
6 changed files with 335 additions and 74 deletions

View file

@ -58,29 +58,32 @@ import org.shadowice.flocke.andotp.Database.Entry;
import org.shadowice.flocke.andotp.Dialogs.PasswordEntryDialog; import org.shadowice.flocke.andotp.Dialogs.PasswordEntryDialog;
import org.shadowice.flocke.andotp.R; import org.shadowice.flocke.andotp.R;
import org.shadowice.flocke.andotp.Tasks.EncryptedBackupTask; import org.shadowice.flocke.andotp.Tasks.EncryptedBackupTask;
import org.shadowice.flocke.andotp.Tasks.EncryptedRestoreTask;
import org.shadowice.flocke.andotp.Tasks.GenericBackupTask; import org.shadowice.flocke.andotp.Tasks.GenericBackupTask;
import org.shadowice.flocke.andotp.Tasks.GenericRestoreTask;
import org.shadowice.flocke.andotp.Tasks.PGPBackupTask; import org.shadowice.flocke.andotp.Tasks.PGPBackupTask;
import org.shadowice.flocke.andotp.Tasks.PGPRestoreTask;
import org.shadowice.flocke.andotp.Tasks.PlainTextBackupTask; import org.shadowice.flocke.andotp.Tasks.PlainTextBackupTask;
import org.shadowice.flocke.andotp.Tasks.PlainTextRestoreTask;
import org.shadowice.flocke.andotp.Utilities.BackupHelper; import org.shadowice.flocke.andotp.Utilities.BackupHelper;
import org.shadowice.flocke.andotp.Utilities.Constants; import org.shadowice.flocke.andotp.Utilities.Constants;
import org.shadowice.flocke.andotp.Utilities.DatabaseHelper; import org.shadowice.flocke.andotp.Utilities.DatabaseHelper;
import org.shadowice.flocke.andotp.Utilities.EncryptionHelper; import org.shadowice.flocke.andotp.Utilities.EncryptionHelper;
import org.shadowice.flocke.andotp.Utilities.StorageAccessHelper;
import org.shadowice.flocke.andotp.Utilities.Tools; import org.shadowice.flocke.andotp.Utilities.Tools;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
import java.io.InputStream; import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import javax.crypto.SecretKey; import javax.crypto.SecretKey;
public class BackupActivity extends BaseActivity { public class BackupActivity extends BaseActivity {
private final static String TAG = BackupActivity.class.getSimpleName(); private final static String TAG = BackupActivity.class.getSimpleName();
private static final String TAG_TASK_FRAGMENT = "BackupActivity.TaskFragmentTag";
private static final String TAG_BACKUP_TASK_FRAGMENT = "BackupActivity.BackupTaskFragmentTag";
private static final String TAG_RESTORE_TASK_FRAGMENT = "BackupActivity.RestoreTaskFragmentTag";
private Constants.BackupType backupType = Constants.BackupType.ENCRYPTED; private Constants.BackupType backupType = Constants.BackupType.ENCRYPTED;
private SecretKey encryptionKey = null; private SecretKey encryptionKey = null;
@ -279,12 +282,15 @@ public class BackupActivity extends BaseActivity {
if (result.messageId != 0) if (result.messageId != 0)
notifyBackupState(result.messageId); notifyBackupState(result.messageId);
else
if (!result.success)
notifyBackupState(R.string.backup_toast_export_failed);
// Clean up the task fragment // Clean up the task fragment
TaskFragment taskFragment = findTaskFragment(); BackupTaskFragment backupTaskFragment = findBackupTaskFragment();
if (taskFragment != null) { if (backupTaskFragment != null) {
getFragmentManager().beginTransaction() getFragmentManager().beginTransaction()
.remove(taskFragment) .remove(backupTaskFragment)
.commit(); .commit();
} }
@ -292,17 +298,59 @@ public class BackupActivity extends BaseActivity {
finishWithResult(); finishWithResult();
} }
private void showBackupProgress(boolean running) { private void handleRestoreTaskResult(GenericRestoreTask.RestoreTaskResult result) {
if (result.success) {
if (result.isPGP) {
InputStream is = new ByteArrayInputStream(result.payload.getBytes(StandardCharsets.UTF_8));
ByteArrayOutputStream os = new ByteArrayOutputStream();
OpenPgpApi api = new OpenPgpApi(this, pgpServiceConnection.getService());
Intent resultIntent = api.executeApi(result.decryptIntent, is, os);
handleOpenPGPResult(resultIntent, os, result.uri, Constants.INTENT_BACKUP_DECRYPT_PGP);
} else {
restoreEntries(result.payload, false);
}
} else {
if (result.messageId != 0)
notifyBackupState(result.messageId);
else
notifyBackupState(R.string.backup_toast_import_failed);
}
showRestoreProgress(false);
// Clean up the task fragment
RestoreTaskFragment restoreTaskFragment = findRestoreTaskFragment();
if (restoreTaskFragment != null) {
getFragmentManager().beginTransaction()
.remove(restoreTaskFragment)
.commit();
}
if (result.success && !result.isPGP)
finishWithResult();
}
private void toggleInProgressMode(boolean running) {
allowExit = !running; allowExit = !running;
btnBackup.setEnabled(!running); btnBackup.setEnabled(!running);
btnRestore.setEnabled(!running); btnRestore.setEnabled(!running);
chkOldFormat.setEnabled(!running); chkOldFormat.setEnabled(!running);
swReplace.setEnabled(!running); swReplace.setEnabled(!running);
}
private void showBackupProgress(boolean running) {
toggleInProgressMode(running);
progressBackup.setVisibility(running ? View.VISIBLE : View.GONE); progressBackup.setVisibility(running ? View.VISIBLE : View.GONE);
} }
private void showRestoreProgress(boolean running) {
toggleInProgressMode(running);
progressRestore.setVisibility(running ? View.VISIBLE : View.GONE);
}
// Get the result from external activities // Get the result from external activities
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) { protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
@ -387,7 +435,7 @@ public class BackupActivity extends BaseActivity {
} }
} }
private void restoreEntries(String text) { private void restoreEntries(String text, boolean finish) {
ArrayList<Entry> entries = DatabaseHelper.stringToEntries(text); ArrayList<Entry> entries = DatabaseHelper.stringToEntries(text);
if (entries.size() > 0) { if (entries.size() > 0) {
@ -401,6 +449,8 @@ public class BackupActivity extends BaseActivity {
if (DatabaseHelper.saveDatabase(this, entries, encryptionKey)) { if (DatabaseHelper.saveDatabase(this, entries, encryptionKey)) {
reload = true; reload = true;
Toast.makeText(this, R.string.backup_toast_import_success, Toast.LENGTH_LONG).show(); Toast.makeText(this, R.string.backup_toast_import_success, Toast.LENGTH_LONG).show();
if (finish)
finishWithResult(); finishWithResult();
} else { } else {
Toast.makeText(this, R.string.backup_toast_import_save_failed, Toast.LENGTH_LONG).show(); Toast.makeText(this, R.string.backup_toast_import_save_failed, Toast.LENGTH_LONG).show();
@ -414,9 +464,10 @@ public class BackupActivity extends BaseActivity {
private void doRestorePlain(Uri uri) { private void doRestorePlain(Uri uri) {
if (Tools.isExternalStorageReadable()) { if (Tools.isExternalStorageReadable()) {
String content = StorageAccessHelper.loadFileString(this, uri); PlainTextRestoreTask task = new PlainTextRestoreTask(this, uri);
task.setCallback(this::handleRestoreTaskResult);
restoreEntries(content); startRestoreTask(task);
} else { } else {
Toast.makeText(this, R.string.backup_toast_storage_not_accessible, Toast.LENGTH_LONG).show(); Toast.makeText(this, R.string.backup_toast_storage_not_accessible, Toast.LENGTH_LONG).show();
} }
@ -475,39 +526,10 @@ public class BackupActivity extends BaseActivity {
private void doRestoreCryptWithPassword(Uri uri, String password, boolean old_format) { private void doRestoreCryptWithPassword(Uri uri, String password, boolean old_format) {
if (Tools.isExternalStorageReadable()) { if (Tools.isExternalStorageReadable()) {
boolean success = true; EncryptedRestoreTask task = new EncryptedRestoreTask(this, uri, password, old_format);
String decryptedString = ""; task.setCallback(this::handleRestoreTaskResult);
try { startRestoreTask(task);
byte[] data = StorageAccessHelper.loadFile(this, uri);
if (old_format) {
SecretKey key = EncryptionHelper.generateSymmetricKeyFromPassword(password);
byte[] decrypted = EncryptionHelper.decrypt(key, data);
decryptedString = new String(decrypted, StandardCharsets.UTF_8);
} else {
byte[] iterBytes = Arrays.copyOfRange(data, 0, Constants.INT_LENGTH);
byte[] salt = Arrays.copyOfRange(data, Constants.INT_LENGTH, Constants.INT_LENGTH + Constants.ENCRYPTION_IV_LENGTH);
byte[] encrypted = Arrays.copyOfRange(data, Constants.INT_LENGTH + Constants.ENCRYPTION_IV_LENGTH, data.length);
int iter = ByteBuffer.wrap(iterBytes).getInt();
SecretKey key = EncryptionHelper.generateSymmetricKeyPBKDF2(password, iter, salt);
byte[] decrypted = EncryptionHelper.decrypt(key, encrypted);
decryptedString = new String(decrypted, StandardCharsets.UTF_8);
}
} catch (Exception e) {
success = false;
e.printStackTrace();
}
if (success) {
restoreEntries(decryptedString);
} else {
Toast.makeText(this,R.string.backup_toast_import_decryption_failed, Toast.LENGTH_LONG).show();
}
} else { } else {
Toast.makeText(this, R.string.backup_toast_storage_not_accessible, Toast.LENGTH_LONG).show(); Toast.makeText(this, R.string.backup_toast_storage_not_accessible, Toast.LENGTH_LONG).show();
} }
@ -548,13 +570,10 @@ public class BackupActivity extends BaseActivity {
if (decryptIntent == null) if (decryptIntent == null)
decryptIntent = new Intent(OpenPgpApi.ACTION_DECRYPT_VERIFY); decryptIntent = new Intent(OpenPgpApi.ACTION_DECRYPT_VERIFY);
String input = StorageAccessHelper.loadFileString(this, uri); PGPRestoreTask task = new PGPRestoreTask(this, uri, decryptIntent);
task.setCallback(this::handleRestoreTaskResult);
InputStream is = new ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)); startRestoreTask(task);
ByteArrayOutputStream os = new ByteArrayOutputStream();
OpenPgpApi api = new OpenPgpApi(this, pgpServiceConnection.getService());
Intent result = api.executeApi(decryptIntent, is, os);
handleOpenPGPResult(result, os, uri, Constants.INTENT_BACKUP_DECRYPT_PGP);
} }
private void doBackupEncrypted(Uri uri, String data) { private void doBackupEncrypted(Uri uri, String data) {
@ -608,12 +627,12 @@ public class BackupActivity extends BaseActivity {
OpenPgpSignatureResult sigResult = result.getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE); OpenPgpSignatureResult sigResult = result.getParcelableExtra(OpenPgpApi.RESULT_SIGNATURE);
if (sigResult.getResult() == OpenPgpSignatureResult.RESULT_VALID_KEY_CONFIRMED) { if (sigResult.getResult() == OpenPgpSignatureResult.RESULT_VALID_KEY_CONFIRMED) {
restoreEntries(outputStreamToString(os)); restoreEntries(outputStreamToString(os), true);
} else { } else {
Toast.makeText(this, R.string.backup_toast_openpgp_not_verified, Toast.LENGTH_LONG).show(); Toast.makeText(this, R.string.backup_toast_openpgp_not_verified, Toast.LENGTH_LONG).show();
} }
} else { } else {
restoreEntries(outputStreamToString(os)); restoreEntries(outputStreamToString(os), true);
} }
} }
} }
@ -639,42 +658,86 @@ public class BackupActivity extends BaseActivity {
} }
@Nullable @Nullable
private TaskFragment findTaskFragment() { private BackupTaskFragment findBackupTaskFragment() {
return (TaskFragment) getFragmentManager().findFragmentByTag(TAG_TASK_FRAGMENT); return (BackupTaskFragment) getFragmentManager().findFragmentByTag(TAG_BACKUP_TASK_FRAGMENT);
}
@Nullable
private RestoreTaskFragment findRestoreTaskFragment() {
return (RestoreTaskFragment) getFragmentManager().findFragmentByTag(TAG_RESTORE_TASK_FRAGMENT);
} }
private void startBackupTask(GenericBackupTask task) { private void startBackupTask(GenericBackupTask task) {
TaskFragment taskFragment = findTaskFragment(); BackupTaskFragment backupTaskFragment = findBackupTaskFragment();
RestoreTaskFragment restoreTaskFragment = findRestoreTaskFragment();
// Don't start a task if we already have an active task running. // Don't start a task if we already have an active task running (backup or restore).
if (taskFragment == null || taskFragment.task.isCanceled()) { if ((backupTaskFragment == null || backupTaskFragment.task.isCanceled()) && (restoreTaskFragment == null || restoreTaskFragment.task.isCanceled())) {
if (taskFragment == null) { if (backupTaskFragment == null) {
taskFragment = new TaskFragment(); backupTaskFragment = new BackupTaskFragment();
getFragmentManager() getFragmentManager()
.beginTransaction() .beginTransaction()
.add(taskFragment, TAG_TASK_FRAGMENT) .add(backupTaskFragment, TAG_BACKUP_TASK_FRAGMENT)
.commit(); .commit();
} }
taskFragment.startTask(task); backupTaskFragment.startTask(task);
showBackupProgress(true); showBackupProgress(true);
} }
} }
private void checkBackgroundTask() { private void startRestoreTask(GenericRestoreTask task) {
TaskFragment taskFragment = findTaskFragment(); BackupTaskFragment backupTaskFragment = findBackupTaskFragment();
if (taskFragment != null) { RestoreTaskFragment restoreTaskFragment = findRestoreTaskFragment();
if (taskFragment.task.isCanceled()) {
// Don't start a task if we already have an active task running (backup or restore).
if ((backupTaskFragment == null || backupTaskFragment.task.isCanceled()) && (restoreTaskFragment == null || restoreTaskFragment.task.isCanceled())) {
if (restoreTaskFragment == null) {
restoreTaskFragment = new RestoreTaskFragment();
getFragmentManager()
.beginTransaction()
.add(restoreTaskFragment, TAG_RESTORE_TASK_FRAGMENT)
.commit();
}
restoreTaskFragment.startTask(task);
showRestoreProgress(true);
}
}
private void checkBackgroundBackupTask() {
BackupTaskFragment backupTaskFragment = findBackupTaskFragment();
if (backupTaskFragment != null) {
if (backupTaskFragment.task.isCanceled()) {
// The task was canceled or has finished, so remove the task fragment. // The task was canceled or has finished, so remove the task fragment.
getFragmentManager().beginTransaction() getFragmentManager().beginTransaction()
.remove(taskFragment) .remove(backupTaskFragment)
.commit(); .commit();
} else { } else {
taskFragment.task.setCallback(this::handleBackupTaskResult); backupTaskFragment.task.setCallback(this::handleBackupTaskResult);
showBackupProgress(true); showBackupProgress(true);
} }
} }
}
private void checkBackgroundRestoreTask() {
RestoreTaskFragment restoreTaskFragment = findRestoreTaskFragment();
if (restoreTaskFragment != null) {
if (restoreTaskFragment.task.isCanceled()) {
// The task was canceled or has finished, so remove the task fragment.
getFragmentManager().beginTransaction()
.remove(restoreTaskFragment)
.commit();
} else {
restoreTaskFragment.task.setCallback(this::handleRestoreTaskResult);
showRestoreProgress(true);
}
}
} }
@Override @Override
@ -682,16 +745,22 @@ public class BackupActivity extends BaseActivity {
super.onPause(); super.onPause();
// We don't want the task to callback to a dead activity and cause a memory leak, so null it here. // We don't want the task to callback to a dead activity and cause a memory leak, so null it here.
TaskFragment taskFragment = findTaskFragment(); BackupTaskFragment backupTaskFragment = findBackupTaskFragment();
if (taskFragment != null) { RestoreTaskFragment restoreTaskFragment = findRestoreTaskFragment();
taskFragment.task.setCallback(null);
} if (backupTaskFragment != null)
backupTaskFragment.task.setCallback(null);
if (restoreTaskFragment != null)
restoreTaskFragment.task.setCallback(null);
} }
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
checkBackgroundTask();
checkBackgroundBackupTask();
checkBackgroundRestoreTask();
} }
@Override @Override
@ -700,10 +769,10 @@ public class BackupActivity extends BaseActivity {
} }
/** Retained instance fragment to hold a running {@link GenericBackupTask} between configuration changes.*/ /** Retained instance fragment to hold a running {@link GenericBackupTask} between configuration changes.*/
public static class TaskFragment extends Fragment { public static class BackupTaskFragment extends Fragment {
GenericBackupTask task; GenericBackupTask task;
public TaskFragment() { public BackupTaskFragment() {
super(); super();
setRetainInstance(true); setRetainInstance(true);
} }
@ -713,4 +782,19 @@ public class BackupActivity extends BaseActivity {
this.task.execute(); this.task.execute();
} }
} }
/** Retained instance fragment to hold a running {@link GenericRestoreTask} between configuration changes.*/
public static class RestoreTaskFragment extends Fragment {
GenericRestoreTask task;
public RestoreTaskFragment() {
super();
setRetainInstance(true);
}
public void startTask(@NonNull GenericRestoreTask task) {
this.task = task;
this.task.execute();
}
}
} }

View file

@ -0,0 +1,66 @@
package org.shadowice.flocke.andotp.Tasks;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.shadowice.flocke.andotp.R;
import org.shadowice.flocke.andotp.Utilities.Constants;
import org.shadowice.flocke.andotp.Utilities.EncryptionHelper;
import org.shadowice.flocke.andotp.Utilities.StorageAccessHelper;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import javax.crypto.SecretKey;
public class EncryptedRestoreTask extends GenericRestoreTask {
private final String password;
private final boolean oldFormat;
public EncryptedRestoreTask(Context context, Uri uri, String password, boolean oldFormat) {
super(context, uri);
this.password = password;
this.oldFormat = oldFormat;
}
@Override
@NonNull
protected RestoreTaskResult doInBackground() {
boolean success = true;
String decryptedString = "";
try {
byte[] data = StorageAccessHelper.loadFile(applicationContext, uri);
if (oldFormat) {
SecretKey key = EncryptionHelper.generateSymmetricKeyFromPassword(password);
byte[] decrypted = EncryptionHelper.decrypt(key, data);
decryptedString = new String(decrypted, StandardCharsets.UTF_8);
} else {
byte[] iterBytes = Arrays.copyOfRange(data, 0, Constants.INT_LENGTH);
byte[] salt = Arrays.copyOfRange(data, Constants.INT_LENGTH, Constants.INT_LENGTH + Constants.ENCRYPTION_IV_LENGTH);
byte[] encrypted = Arrays.copyOfRange(data, Constants.INT_LENGTH + Constants.ENCRYPTION_IV_LENGTH, data.length);
int iter = ByteBuffer.wrap(iterBytes).getInt();
SecretKey key = EncryptionHelper.generateSymmetricKeyPBKDF2(password, iter, salt);
byte[] decrypted = EncryptionHelper.decrypt(key, encrypted);
decryptedString = new String(decrypted, StandardCharsets.UTF_8);
}
} catch (Exception e) {
success = false;
e.printStackTrace();
}
if (success) {
return RestoreTaskResult.success(decryptedString);
} else {
return RestoreTaskResult.failure(R.string.backup_toast_import_decryption_failed);
}
}
}

View file

@ -0,0 +1,63 @@
package org.shadowice.flocke.andotp.Tasks;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.shadowice.flocke.andotp.R;
import org.shadowice.flocke.andotp.Utilities.Settings;
public abstract class GenericRestoreTask extends UiBasedBackgroundTask<GenericRestoreTask.RestoreTaskResult> {
protected final Context applicationContext;
protected final Settings settings;
protected Uri uri;
public GenericRestoreTask(Context context, @Nullable Uri uri) {
super(GenericRestoreTask.RestoreTaskResult.failure(R.string.backup_toast_import_failed));
this.applicationContext = context.getApplicationContext();
this.settings = new Settings(applicationContext);
this.uri = uri;
}
@Override
@NonNull
protected abstract RestoreTaskResult doInBackground();
public static class RestoreTaskResult {
public final boolean success;
public final String payload;
public final int messageId;
public boolean isPGP = false;
public Intent decryptIntent = null;
public Uri uri = null;
public RestoreTaskResult(boolean success, String payload, int messageId) {
this.success = success;
this.payload = payload;
this.messageId = messageId;
}
public RestoreTaskResult(boolean success, String payload, int messageId, boolean isPGP, Intent decryptIntent, Uri uri) {
this.success = success;
this.payload = payload;
this.messageId = messageId;
this.isPGP = isPGP;
this.decryptIntent = decryptIntent;
this.uri = uri;
}
public static RestoreTaskResult success(String payload) {
return new RestoreTaskResult(true, payload, 0);
}
public static RestoreTaskResult failure(int messageId) {
return new RestoreTaskResult(false, null, messageId);
}
}
}

View file

@ -0,0 +1,26 @@
package org.shadowice.flocke.andotp.Tasks;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.shadowice.flocke.andotp.Utilities.StorageAccessHelper;
public class PGPRestoreTask extends GenericRestoreTask {
private final Intent decryptIntent;
public PGPRestoreTask(Context context, Uri uri, Intent decryptIntent) {
super(context, uri);
this.decryptIntent = decryptIntent;
}
@Override
@NonNull
protected RestoreTaskResult doInBackground() {
String data = StorageAccessHelper.loadFileString(applicationContext, uri);
return new RestoreTaskResult(true, data, 0, true, decryptIntent, uri);
}
}

View file

@ -0,0 +1,21 @@
package org.shadowice.flocke.andotp.Tasks;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import org.shadowice.flocke.andotp.Utilities.StorageAccessHelper;
public class PlainTextRestoreTask extends GenericRestoreTask {
public PlainTextRestoreTask(Context context, Uri uri) {
super(context, uri);
}
@Override
@NonNull
protected RestoreTaskResult doInBackground() {
String data = StorageAccessHelper.loadFileString(applicationContext, uri);
return RestoreTaskResult.success(data);
}
}

View file

@ -55,6 +55,7 @@
<string name="backup_toast_import_save_failed">Failed to save restored entries</string> <string name="backup_toast_import_save_failed">Failed to save restored entries</string>
<string name="backup_toast_import_decryption_failed">Decryption of the backup failed</string> <string name="backup_toast_import_decryption_failed">Decryption of the backup failed</string>
<string name="backup_toast_import_no_entries">No entries found in imported data</string> <string name="backup_toast_import_no_entries">No entries found in imported data</string>
<string name="backup_toast_import_failed">Import from external storage failed</string>
<string name="backup_toast_storage_not_accessible">External storage currently not accessible</string> <string name="backup_toast_storage_not_accessible">External storage currently not accessible</string>
<string name="backup_toast_openpgp_error">OpenPGP Error: %s</string> <string name="backup_toast_openpgp_error">OpenPGP Error: %s</string>
<string name="backup_toast_openpgp_not_verified">No verified signature detected</string> <string name="backup_toast_openpgp_not_verified">No verified signature detected</string>