Repo created
Some checks failed
build / build (push) Has been cancelled
build / test (push) Has been cancelled

This commit is contained in:
Fr4nz D13trich 2025-12-18 08:31:42 +01:00
commit 3c8e58604e
646 changed files with 69135 additions and 0 deletions

View file

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2014 Amulya Khare
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,316 @@
package com.amulyakhare.textdrawable;
import android.graphics.*;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.OvalShape;
import android.graphics.drawable.shapes.RectShape;
import android.graphics.drawable.shapes.RoundRectShape;
/**
* @author amulya
* @datetime 14 Oct 2014, 3:53 PM
*/
public class TextDrawable extends ShapeDrawable {
private final Paint textPaint;
private final Paint borderPaint;
private static final float SHADE_FACTOR = 0.9f;
private final String text;
private final int color;
private final RectShape shape;
private final int height;
private final int width;
private final int fontSize;
private final float radius;
private final int borderThickness;
private TextDrawable(Builder builder) {
super(builder.shape);
// shape properties
shape = builder.shape;
height = builder.height;
width = builder.width;
radius = builder.radius;
// text and color
text = builder.toUpperCase ? builder.text.toUpperCase() : builder.text;
color = builder.color;
// text paint settings
fontSize = builder.fontSize;
textPaint = new Paint();
textPaint.setColor(builder.textColor);
textPaint.setAntiAlias(true);
textPaint.setFakeBoldText(builder.isBold);
textPaint.setStyle(Paint.Style.FILL);
textPaint.setTypeface(builder.font);
textPaint.setTextAlign(Paint.Align.CENTER);
textPaint.setStrokeWidth(builder.borderThickness);
// border paint settings
borderThickness = builder.borderThickness;
borderPaint = new Paint();
borderPaint.setColor(getDarkerShade(color));
borderPaint.setStyle(Paint.Style.STROKE);
borderPaint.setStrokeWidth(borderThickness);
// drawable paint color
Paint paint = getPaint();
paint.setColor(color);
}
private int getDarkerShade(int color) {
return Color.rgb((int)(SHADE_FACTOR * Color.red(color)),
(int)(SHADE_FACTOR * Color.green(color)),
(int)(SHADE_FACTOR * Color.blue(color)));
}
@Override
public void draw(Canvas canvas) {
super.draw(canvas);
Rect r = getBounds();
// draw border
if (borderThickness > 0) {
drawBorder(canvas);
}
int count = canvas.save();
canvas.translate(r.left, r.top);
// draw text
int width = this.width < 0 ? r.width() : this.width;
int height = this.height < 0 ? r.height() : this.height;
int fontSize = this.fontSize < 0 ? (Math.min(width, height) / 2) : this.fontSize;
textPaint.setTextSize(fontSize);
canvas.drawText(text, width / 2, height / 2 - ((textPaint.descent() + textPaint.ascent()) / 2), textPaint);
canvas.restoreToCount(count);
}
private void drawBorder(Canvas canvas) {
RectF rect = new RectF(getBounds());
rect.inset(borderThickness/2, borderThickness/2);
if (shape instanceof OvalShape) {
canvas.drawOval(rect, borderPaint);
}
else if (shape instanceof RoundRectShape) {
canvas.drawRoundRect(rect, radius, radius, borderPaint);
}
else {
canvas.drawRect(rect, borderPaint);
}
}
@Override
public void setAlpha(int alpha) {
textPaint.setAlpha(alpha);
}
@Override
public void setColorFilter(ColorFilter cf) {
textPaint.setColorFilter(cf);
}
@Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
@Override
public int getIntrinsicWidth() {
return width;
}
@Override
public int getIntrinsicHeight() {
return height;
}
public static IShapeBuilder builder() {
return new Builder();
}
public static class Builder implements IConfigBuilder, IShapeBuilder, IBuilder {
private String text;
private int color;
private int borderThickness;
private int width;
private int height;
private Typeface font;
private RectShape shape;
public int textColor;
private int fontSize;
private boolean isBold;
private boolean toUpperCase;
public float radius;
private Builder() {
text = "";
color = Color.GRAY;
textColor = Color.WHITE;
borderThickness = 0;
width = -1;
height = -1;
shape = new RectShape();
font = Typeface.create("sans-serif-light", Typeface.NORMAL);
fontSize = -1;
isBold = false;
toUpperCase = false;
}
public IConfigBuilder width(int width) {
this.width = width;
return this;
}
public IConfigBuilder height(int height) {
this.height = height;
return this;
}
public IConfigBuilder textColor(int color) {
this.textColor = color;
return this;
}
public IConfigBuilder withBorder(int thickness) {
this.borderThickness = thickness;
return this;
}
public IConfigBuilder useFont(Typeface font) {
this.font = font;
return this;
}
public IConfigBuilder fontSize(int size) {
this.fontSize = size;
return this;
}
public IConfigBuilder bold() {
this.isBold = true;
return this;
}
public IConfigBuilder toUpperCase() {
this.toUpperCase = true;
return this;
}
@Override
public IConfigBuilder beginConfig() {
return this;
}
@Override
public IShapeBuilder endConfig() {
return this;
}
@Override
public IBuilder rect() {
this.shape = new RectShape();
return this;
}
@Override
public IBuilder round() {
this.shape = new OvalShape();
return this;
}
@Override
public IBuilder roundRect(int radius) {
this.radius = radius;
float[] radii = {radius, radius, radius, radius, radius, radius, radius, radius};
this.shape = new RoundRectShape(radii, null, null);
return this;
}
@Override
public TextDrawable buildRect(String text, int color) {
rect();
return build(text, color);
}
@Override
public TextDrawable buildRoundRect(String text, int color, int radius) {
roundRect(radius);
return build(text, color);
}
@Override
public TextDrawable buildRound(String text, int color) {
round();
return build(text, color);
}
@Override
public TextDrawable build(String text, int color) {
this.color = color;
this.text = text;
return new TextDrawable(this);
}
}
public interface IConfigBuilder {
public IConfigBuilder width(int width);
public IConfigBuilder height(int height);
public IConfigBuilder textColor(int color);
public IConfigBuilder withBorder(int thickness);
public IConfigBuilder useFont(Typeface font);
public IConfigBuilder fontSize(int size);
public IConfigBuilder bold();
public IConfigBuilder toUpperCase();
public IShapeBuilder endConfig();
}
public static interface IBuilder {
public TextDrawable build(String text, int color);
}
public static interface IShapeBuilder {
public IConfigBuilder beginConfig();
public IBuilder rect();
public IBuilder round();
public IBuilder roundRect(int radius);
public TextDrawable buildRect(String text, int color);
public TextDrawable buildRoundRect(String text, int color, int radius);
public TextDrawable buildRound(String text, int color);
}
}

View file

@ -0,0 +1,69 @@
package com.amulyakhare.textdrawable.util;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
/**
* @author amulya
* @datetime 14 Oct 2014, 5:20 PM
*/
public class ColorGenerator {
public static ColorGenerator DEFAULT;
public static ColorGenerator MATERIAL;
static {
DEFAULT = create(Arrays.asList(
0xfff16364,
0xfff58559,
0xfff9a43e,
0xffe4c62e,
0xff67bf74,
0xff59a2be,
0xff2093cd,
0xffad62a7,
0xff805781
));
MATERIAL = create(Arrays.asList(
0xffe57373,
0xfff06292,
0xffba68c8,
0xff9575cd,
0xff7986cb,
0xff64b5f6,
0xff4fc3f7,
0xff4dd0e1,
0xff4db6ac,
0xff81c784,
0xffaed581,
0xffff8a65,
0xffd4e157,
0xffffd54f,
0xffffb74d,
0xffa1887f,
0xff90a4ae
));
}
private final List<Integer> mColors;
private final Random mRandom;
public static ColorGenerator create(List<Integer> colorList) {
return new ColorGenerator(colorList);
}
private ColorGenerator(List<Integer> colorList) {
mColors = colorList;
mRandom = new Random(System.currentTimeMillis());
}
public int getRandomColor() {
return mColors.get(mRandom.nextInt(mColors.size()));
}
public int getColor(Object key) {
return mColors.get(Math.abs(key.hashCode()) % mColors.size());
}
}

View file

@ -0,0 +1,17 @@
package com.beemdevelopment.aegis;
public enum AccountNamePosition {
HIDDEN,
END,
BELOW;
private static AccountNamePosition[] _values;
static {
_values = values();
}
public static AccountNamePosition fromInteger(int x) {
return _values[x];
}
}

View file

@ -0,0 +1,8 @@
package com.beemdevelopment.aegis;
import dagger.hilt.android.HiltAndroidApp;
@HiltAndroidApp
public class AegisApplication extends AegisApplicationBase {
}

View file

@ -0,0 +1,121 @@
package com.beemdevelopment.aegis;
import android.app.Application;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleEventObserver;
import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.ProcessLifecycleOwner;
import com.beemdevelopment.aegis.receivers.VaultLockReceiver;
import com.beemdevelopment.aegis.ui.MainActivity;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultManager;
import com.topjohnwu.superuser.Shell;
import java.util.Collections;
import dagger.hilt.InstallIn;
import dagger.hilt.android.EarlyEntryPoint;
import dagger.hilt.android.EarlyEntryPoints;
import dagger.hilt.components.SingletonComponent;
public abstract class AegisApplicationBase extends Application {
private static final String CODE_LOCK_STATUS_ID = "lock_status_channel";
private VaultManager _vaultManager;
static {
// Enable verbose libsu logging in debug builds
Shell.enableVerboseLogging = BuildConfig.DEBUG;
}
@Override
public void onCreate() {
super.onCreate();
_vaultManager = EarlyEntryPoints.get(this, EntryPoint.class).getVaultManager();
VaultLockReceiver lockReceiver = new VaultLockReceiver();
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_SCREEN_OFF);
ContextCompat.registerReceiver(this, lockReceiver, intentFilter, ContextCompat.RECEIVER_NOT_EXPORTED);
// lock the app if the user moves the application to the background
ProcessLifecycleOwner.get().getLifecycle().addObserver(new AppLifecycleObserver());
// clear the cache directory on startup, to make sure no temporary vault export files remain
IOUtils.clearDirectory(getCacheDir(), false);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
initAppShortcuts();
}
// NOTE: Disabled for now. See issue: #1047
/*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
initNotificationChannels();
}*/
}
@RequiresApi(api = Build.VERSION_CODES.N_MR1)
private void initAppShortcuts() {
ShortcutManager shortcutManager = getSystemService(ShortcutManager.class);
if (shortcutManager == null) {
return;
}
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra("action", "scan");
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.setAction(Intent.ACTION_MAIN);
ShortcutInfo shortcut = new ShortcutInfo.Builder(this, "shortcut_new")
.setShortLabel(getString(R.string.new_entry))
.setLongLabel(getString(R.string.add_new_entry))
.setIcon(Icon.createWithResource(this, R.drawable.ic_qr_code))
.setIntent(intent)
.build();
shortcutManager.setDynamicShortcuts(Collections.singletonList(shortcut));
}
private void initNotificationChannels() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = getString(R.string.channel_name_lock_status);
String description = getString(R.string.channel_description_lock_status);
int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel channel = new NotificationChannel(CODE_LOCK_STATUS_ID, name, importance);
channel.setDescription(description);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
private class AppLifecycleObserver implements LifecycleEventObserver {
@Override
public void onStateChanged(@NonNull LifecycleOwner source, @NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_STOP
&& _vaultManager.isAutoLockEnabled(Preferences.AUTO_LOCK_ON_MINIMIZE)
&& !_vaultManager.isAutoLockBlocked()) {
_vaultManager.lock(false);
}
}
}
@EarlyEntryPoint
@InstallIn(SingletonComponent.class)
interface EntryPoint {
VaultManager getVaultManager();
}
}

View file

@ -0,0 +1,147 @@
package com.beemdevelopment.aegis;
import android.app.backup.BackupAgent;
import android.app.backup.BackupDataInput;
import android.app.backup.BackupDataOutput;
import android.app.backup.FullBackupDataOutput;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import com.beemdevelopment.aegis.database.AppDatabase;
import com.beemdevelopment.aegis.database.AuditLogRepository;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultRepository;
import com.beemdevelopment.aegis.vault.VaultRepositoryException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class AegisBackupAgent extends BackupAgent {
private static final String TAG = AegisBackupAgent.class.getSimpleName();
private Preferences _prefs;
private AuditLogRepository _auditLogRepository;
@Override
public void onCreate() {
super.onCreate();
// Cannot use injection with Dagger Hilt here, because the app is launched in a restricted mode on restore
_prefs = new Preferences(this);
AppDatabase appDatabase = AegisModule.provideAppDatabase(this);
_auditLogRepository = AegisModule.provideAuditLogRepository(appDatabase);
}
@Override
public synchronized void onFullBackup(FullBackupDataOutput data) throws IOException {
Log.i(TAG, String.format("onFullBackup() called: flags=%d, quota=%d",
Build.VERSION.SDK_INT >= Build.VERSION_CODES.P ? data.getTransportFlags() : -1,
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? data.getQuota() : -1));
boolean isD2D = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
&& (data.getTransportFlags() & FLAG_DEVICE_TO_DEVICE_TRANSFER) == FLAG_DEVICE_TO_DEVICE_TRANSFER;
if (isD2D) {
Log.i(TAG, "onFullBackup(): allowing D2D transfer");
} else if (!_prefs.isAndroidBackupsEnabled()) {
Log.i(TAG, "onFullBackup() skipped: Android backups disabled in preferences");
return;
}
// We perform a catch of any Exception here to make sure we also
// report any runtime exceptions, in addition to the expected IOExceptions.
try {
fullBackup(data);
_auditLogRepository.addAndroidBackupCreatedEvent();
_prefs.setAndroidBackupResult(new Preferences.BackupResult(null));
} catch (Exception e) {
Log.e(TAG, String.format("onFullBackup() failed: %s", e));
_prefs.setAndroidBackupResult(new Preferences.BackupResult(e));
throw e;
}
Log.i(TAG, "onFullBackup() finished");
}
private void fullBackup(FullBackupDataOutput data) throws IOException {
// First copy the vault to the files/backup directory
createBackupDir();
File vaultBackupFile = getVaultBackupFile();
try (OutputStream outputStream = new FileOutputStream(vaultBackupFile)) {
VaultFile vaultFile = VaultRepository.readVaultFile(this);
byte[] bytes = vaultFile.exportable().toBytes();
outputStream.write(bytes);
} catch (VaultRepositoryException | IOException e) {
deleteBackupDir();
throw new IOException(e);
}
// Then call the original implementation so that fullBackupContent specified in AndroidManifest is read
try {
super.onFullBackup(data);
} finally {
deleteBackupDir();
}
}
@Override
public synchronized void onRestoreFile(ParcelFileDescriptor data, long size, File destination, int type, long mode, long mtime) throws IOException {
Log.i(TAG, String.format("onRestoreFile() called: dest=%s", destination));
super.onRestoreFile(data, size, destination, type, mode, mtime);
File vaultBackupFile = getVaultBackupFile();
if (destination.getCanonicalFile().equals(vaultBackupFile.getCanonicalFile())) {
try (InputStream inStream = new FileInputStream(vaultBackupFile)) {
VaultRepository.writeToFile(this, inStream);
} catch (IOException e) {
Log.e(TAG, String.format("onRestoreFile() failed: dest=%s, error=%s", destination, e));
throw e;
} finally {
deleteBackupDir();
}
}
Log.i(TAG, String.format("onRestoreFile() finished: dest=%s", destination));
}
@Override
public synchronized void onQuotaExceeded(long backupDataBytes, long quotaBytes) {
super.onQuotaExceeded(backupDataBytes, quotaBytes);
Log.e(TAG, String.format("onQuotaExceeded() called: backupDataBytes=%d, quotaBytes=%d", backupDataBytes, quotaBytes));
}
@Override
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) throws IOException {
}
@Override
public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) throws IOException {
}
private void createBackupDir() throws IOException {
File dir = getVaultBackupFile().getParentFile();
if (dir == null || (!dir.exists() && !dir.mkdir())) {
throw new IOException(String.format("Unable to create backup directory: %s", dir));
}
}
private void deleteBackupDir() {
File dir = getVaultBackupFile().getParentFile();
if (dir != null) {
IOUtils.clearDirectory(dir, true);
}
}
private File getVaultBackupFile() {
return new File(new File(getFilesDir(), "backup"), VaultRepository.FILENAME);
}
}

View file

@ -0,0 +1,55 @@
package com.beemdevelopment.aegis;
import android.content.Context;
import androidx.room.Room;
import com.beemdevelopment.aegis.database.AppDatabase;
import com.beemdevelopment.aegis.database.AuditLogDao;
import com.beemdevelopment.aegis.database.AuditLogRepository;
import com.beemdevelopment.aegis.icons.IconPackManager;
import com.beemdevelopment.aegis.vault.VaultManager;
import javax.inject.Singleton;
import dagger.Module;
import dagger.Provides;
import dagger.hilt.InstallIn;
import dagger.hilt.android.qualifiers.ApplicationContext;
import dagger.hilt.components.SingletonComponent;
@Module
@InstallIn(SingletonComponent.class)
public class AegisModule {
@Provides
@Singleton
public static IconPackManager provideIconPackManager(@ApplicationContext Context context) {
return new IconPackManager(context);
}
@Provides
@Singleton
public static AuditLogRepository provideAuditLogRepository(AppDatabase appDatabase) {
AuditLogDao auditLogDao = appDatabase.auditLogDao();
return new AuditLogRepository(auditLogDao);
}
@Provides
@Singleton
public static VaultManager provideVaultManager(@ApplicationContext Context context, AuditLogRepository auditLogRepository) {
return new VaultManager(context, auditLogRepository);
}
@Provides
public static Preferences providePreferences(@ApplicationContext Context context) {
return new Preferences(context);
}
@Provides
@Singleton
public static AppDatabase provideAppDatabase(@ApplicationContext Context context) {
return Room.databaseBuilder(context.getApplicationContext(),
AppDatabase.class, "aegis-db")
.build();
}
}

View file

@ -0,0 +1,7 @@
package com.beemdevelopment.aegis;
public enum BackupsVersioningStrategy {
UNDEFINED,
MULTIPLE_BACKUPS,
SINGLE_BACKUP
}

View file

@ -0,0 +1,17 @@
package com.beemdevelopment.aegis;
public enum CopyBehavior {
NEVER,
SINGLETAP,
DOUBLETAP;
private static CopyBehavior[] _values;
static {
_values = values();
}
public static CopyBehavior fromInteger(int x) {
return _values[x];
}
}

View file

@ -0,0 +1,42 @@
package com.beemdevelopment.aegis;
public enum EventType {
VAULT_UNLOCKED,
VAULT_BACKUP_CREATED,
VAULT_ANDROID_BACKUP_CREATED,
VAULT_EXPORTED,
ENTRY_SHARED,
VAULT_UNLOCK_FAILED_PASSWORD,
VAULT_UNLOCK_FAILED_BIOMETRICS;
private static EventType[] _values;
static {
_values = values();
}
public static EventType fromInteger(int x) {
return _values[x];
}
public static int getEventTitleRes(EventType eventType) {
switch (eventType) {
case VAULT_UNLOCKED:
return R.string.event_title_vault_unlocked;
case VAULT_BACKUP_CREATED:
return R.string.event_title_backup_created;
case VAULT_ANDROID_BACKUP_CREATED:
return R.string.event_title_android_backup_created;
case VAULT_EXPORTED:
return R.string.event_title_vault_exported;
case ENTRY_SHARED:
return R.string.event_title_entry_shared;
case VAULT_UNLOCK_FAILED_PASSWORD:
return R.string.event_title_vault_unlock_failed_password;
case VAULT_UNLOCK_FAILED_BIOMETRICS:
return R.string.event_title_vault_unlock_failed_biometrics;
default:
return R.string.event_unknown;
}
}
}

View file

@ -0,0 +1,20 @@
package com.beemdevelopment.aegis;
public enum GroupPlaceholderType {
ALL,
NEW_GROUP,
NO_GROUP;
public int getStringRes() {
switch (this) {
case ALL:
return R.string.all;
case NEW_GROUP:
return R.string.new_group;
case NO_GROUP:
return R.string.no_group;
default:
throw new IllegalArgumentException("Unexpected placeholder type: " + this);
}
}
}

View file

@ -0,0 +1,56 @@
package com.beemdevelopment.aegis;
import androidx.annotation.StringRes;
import java.util.concurrent.TimeUnit;
public enum PassReminderFreq {
NEVER,
WEEKLY,
BIWEEKLY,
MONTHLY,
QUARTERLY;
public long getDurationMillis() {
long weeks;
switch (this) {
case WEEKLY:
weeks = 1;
break;
case BIWEEKLY:
weeks = 2;
break;
case MONTHLY:
weeks = 4;
break;
case QUARTERLY:
weeks = 13;
break;
default:
weeks = 0;
break;
}
return TimeUnit.MILLISECONDS.convert(weeks * 7L, TimeUnit.DAYS);
}
@StringRes
public int getStringRes() {
switch (this) {
case WEEKLY:
return R.string.password_reminder_freq_weekly;
case BIWEEKLY:
return R.string.password_reminder_freq_biweekly;
case MONTHLY:
return R.string.password_reminder_freq_monthly;
case QUARTERLY:
return R.string.password_reminder_freq_quarterly;
default:
return R.string.password_reminder_freq_never;
}
}
public static PassReminderFreq fromInteger(int i) {
return PassReminderFreq.values()[i];
}
}

View file

@ -0,0 +1,707 @@
package com.beemdevelopment.aegis;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.provider.DocumentsContractCompat;
import androidx.preference.PreferenceManager;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.util.TimeUtils;
import com.beemdevelopment.aegis.vault.VaultBackupPermissionException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
public class Preferences {
public static final int AUTO_LOCK_OFF = 1 << 0;
public static final int AUTO_LOCK_ON_BACK_BUTTON = 1 << 1;
public static final int AUTO_LOCK_ON_MINIMIZE = 1 << 2;
public static final int AUTO_LOCK_ON_DEVICE_LOCK = 1 << 3;
public static final int SEARCH_IN_ISSUER = 1 << 0;
public static final int SEARCH_IN_NAME = 1 << 1;
public static final int SEARCH_IN_NOTE = 1 << 2;
public static final int SEARCH_IN_GROUPS = 1 << 3;
public static final int BACKUPS_VERSIONS_INFINITE = -1;
public static final int[] AUTO_LOCK_SETTINGS = {
AUTO_LOCK_ON_BACK_BUTTON,
AUTO_LOCK_ON_MINIMIZE,
AUTO_LOCK_ON_DEVICE_LOCK
};
public static final int[] SEARCH_BEHAVIOR_SETTINGS = {
SEARCH_IN_ISSUER,
SEARCH_IN_NAME,
SEARCH_IN_NOTE,
SEARCH_IN_GROUPS
};
private SharedPreferences _prefs;
public Preferences(Context context) {
_prefs = PreferenceManager.getDefaultSharedPreferences(context);
if (getPasswordReminderTimestamp().getTime() == 0) {
resetPasswordReminderTimestamp();
}
migratePreferences();
}
public void migratePreferences() {
// Change copy on tap to copy behavior to new preference and delete the old key
String prefCopyOnTapKey = "pref_copy_on_tap";
if (_prefs.contains(prefCopyOnTapKey)) {
boolean isCopyOnTapEnabled = _prefs.getBoolean(prefCopyOnTapKey, false);
if (isCopyOnTapEnabled) {
setCopyBehavior(CopyBehavior.SINGLETAP);
}
_prefs.edit().remove(prefCopyOnTapKey).apply();
}
}
public boolean isTapToRevealEnabled() {
return _prefs.getBoolean("pref_tap_to_reveal", false);
}
public boolean isGroupMultiselectEnabled() {
return _prefs.getBoolean("pref_groups_multiselect", false);
}
public boolean isEntryHighlightEnabled() {
return _prefs.getBoolean("pref_highlight_entry", false);
}
public boolean isHapticFeedbackEnabled() {
return _prefs.getBoolean("pref_haptic_feedback", true);
}
public boolean isPauseFocusedEnabled() {
boolean dependenciesEnabled = isTapToRevealEnabled() || isEntryHighlightEnabled();
if (!dependenciesEnabled) return false;
return _prefs.getBoolean("pref_pause_entry", false);
}
public boolean isPanicTriggerEnabled() {
return _prefs.getBoolean("pref_panic_trigger", false);
}
public void setIsPanicTriggerEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_panic_trigger", enabled).apply();
}
public boolean isSecureScreenEnabled() {
// screen security should be enabled by default, but not for debug builds
return _prefs.getBoolean("pref_secure_screen", !BuildConfig.DEBUG);
}
public PassReminderFreq getPasswordReminderFrequency() {
final String key = "pref_password_reminder_freq";
if (_prefs.contains(key) || _prefs.getBoolean("pref_password_reminder", true)) {
int i = _prefs.getInt(key, PassReminderFreq.BIWEEKLY.ordinal());
return PassReminderFreq.fromInteger(i);
}
return PassReminderFreq.NEVER;
}
public void setPasswordReminderFrequency(PassReminderFreq freq) {
_prefs.edit().putInt("pref_password_reminder_freq", freq.ordinal()).apply();
}
public boolean isPasswordReminderNeeded() {
return isPasswordReminderNeeded(new Date().getTime());
}
boolean isPasswordReminderNeeded(long currTime) {
PassReminderFreq freq = getPasswordReminderFrequency();
if (freq == PassReminderFreq.NEVER) {
return false;
}
long duration = currTime - getPasswordReminderTimestamp().getTime();
return duration >= freq.getDurationMillis();
}
public Date getPasswordReminderTimestamp() {
return new Date(_prefs.getLong("pref_password_reminder_counter", 0));
}
void setPasswordReminderTimestamp(long timestamp) {
_prefs.edit().putLong("pref_password_reminder_counter", timestamp).apply();
}
public void resetPasswordReminderTimestamp() {
setPasswordReminderTimestamp(new Date().getTime());
}
public boolean onlyShowNecessaryAccountNames() { return _prefs.getBoolean("pref_shared_issuer_account_name", false); }
public boolean isIconVisible() {
return _prefs.getBoolean("pref_show_icons", true);
}
public boolean getShowNextCode() {
return _prefs.getBoolean("pref_show_next_code", false);
}
public boolean getShowExpirationState() {
return _prefs.getBoolean("pref_expiration_state", true);
}
public CodeGrouping getCodeGroupSize() {
String value = _prefs.getString("pref_code_group_size_string", "GROUPING_THREES");
return CodeGrouping.valueOf(value);
}
public void setCodeGroupSize(CodeGrouping codeGroupSize) {
_prefs.edit().putString("pref_code_group_size_string", codeGroupSize.name()).apply();
}
public boolean isIntroDone() {
return _prefs.getBoolean("pref_intro", false);
}
private int getAutoLockMask() {
final int def = AUTO_LOCK_ON_BACK_BUTTON | AUTO_LOCK_ON_DEVICE_LOCK;
if (!_prefs.contains("pref_auto_lock_mask")) {
return _prefs.getBoolean("pref_auto_lock", true) ? def : AUTO_LOCK_OFF;
}
return _prefs.getInt("pref_auto_lock_mask", def);
}
public int getSearchBehaviorMask() {
final int def = SEARCH_IN_ISSUER | SEARCH_IN_NAME;
return _prefs.getInt("pref_search_behavior_mask", def);
}
public boolean isSearchBehaviorTypeEnabled(int searchBehaviorType) {
return (getSearchBehaviorMask() & searchBehaviorType) == searchBehaviorType;
}
public void setSearchBehaviorMask(int searchBehavior) {
_prefs.edit().putInt("pref_search_behavior_mask", searchBehavior).apply();
}
public boolean isAutoLockEnabled() {
return getAutoLockMask() != AUTO_LOCK_OFF;
}
public boolean isAutoLockTypeEnabled(int autoLockType) {
return (getAutoLockMask() & autoLockType) == autoLockType;
}
public void setAutoLockMask(int autoLock) {
_prefs.edit().putInt("pref_auto_lock_mask", autoLock).apply();
}
public void setIntroDone(boolean done) {
_prefs.edit().putBoolean("pref_intro", done).apply();
}
public void setTapToRevealTime(int number) {
_prefs.edit().putInt("pref_tap_to_reveal_time", number).apply();
}
public void setCurrentSortCategory(SortCategory category) {
_prefs.edit().putInt("pref_current_sort_category", category.ordinal()).apply();
}
public SortCategory getCurrentSortCategory() {
return SortCategory.fromInteger(_prefs.getInt("pref_current_sort_category", 0));
}
public int getTapToRevealTime() {
return _prefs.getInt("pref_tap_to_reveal_time", 30);
}
public Theme getCurrentTheme() {
return Theme.fromInteger(_prefs.getInt("pref_current_theme", Theme.SYSTEM.ordinal()));
}
public void setCurrentTheme(Theme theme) {
_prefs.edit().putInt("pref_current_theme", theme.ordinal()).apply();
}
public boolean isDynamicColorsEnabled() {
return _prefs.getBoolean("pref_dynamic_colors", false);
}
public ViewMode getCurrentViewMode() {
return ViewMode.fromInteger(_prefs.getInt("pref_current_view_mode", 0));
}
public void setCurrentViewMode(ViewMode viewMode) {
_prefs.edit().putInt("pref_current_view_mode", viewMode.ordinal()).apply();
}
public AccountNamePosition getAccountNamePosition() {
return AccountNamePosition.fromInteger(_prefs.getInt("pref_account_name_position", AccountNamePosition.END.ordinal()));
}
public void setAccountNamePosition(AccountNamePosition accountNamePosition) {
_prefs.edit().putInt("pref_account_name_position", accountNamePosition.ordinal()).apply();
}
public Integer getUsageCount(UUID uuid) {
Integer usageCount = getUsageCounts().get(uuid);
return usageCount != null ? usageCount : 0;
}
public void resetUsageCount(UUID uuid) {
Map<UUID, Integer> usageCounts = getUsageCounts();
usageCounts.put(uuid, 0);
setUsageCount(usageCounts);
}
public long getLastUsedTimestamp(UUID uuid) {
Map<UUID, Long> timestamps = getLastUsedTimestamps();
if (timestamps != null && timestamps.size() > 0){
Long timestamp = timestamps.get(uuid);
return timestamp != null ? timestamp : 0;
}
return 0;
}
public void clearUsageCount() {
_prefs.edit().remove("pref_usage_count").apply();
}
public Map<UUID, Long> getLastUsedTimestamps() {
Map<UUID, Long> lastUsedTimestamps = new HashMap<>();
String lastUsedTimestamp = _prefs.getString("pref_last_used_timestamps", "");
try {
JSONArray arr = new JSONArray(lastUsedTimestamp);
for (int i = 0; i < arr.length(); i++) {
JSONObject json = arr.getJSONObject(i);
lastUsedTimestamps.put(UUID.fromString(json.getString("uuid")), json.getLong("timestamp"));
}
} catch (JSONException ignored) {
}
return lastUsedTimestamps;
}
public void setLastUsedTimestamps(Map<UUID, Long> lastUsedTimestamps) {
JSONArray lastUsedTimestampJson = new JSONArray();
for (Map.Entry<UUID, Long> entry : lastUsedTimestamps.entrySet()) {
JSONObject entryJson = new JSONObject();
try {
entryJson.put("uuid", entry.getKey());
entryJson.put("timestamp", entry.getValue());
lastUsedTimestampJson.put(entryJson);
} catch (JSONException e) {
e.printStackTrace();
}
}
_prefs.edit().putString("pref_last_used_timestamps", lastUsedTimestampJson.toString()).apply();
}
public Map<UUID, Integer> getUsageCounts() {
Map<UUID, Integer> usageCounts = new HashMap<>();
String usageCount = _prefs.getString("pref_usage_count", "");
try {
JSONArray arr = new JSONArray(usageCount);
for (int i = 0; i < arr.length(); i++) {
JSONObject json = arr.getJSONObject(i);
usageCounts.put(UUID.fromString(json.getString("uuid")), json.getInt("count"));
}
} catch (JSONException ignored) {
}
return usageCounts;
}
public void setUsageCount(Map<UUID, Integer> usageCounts) {
JSONArray usageCountJson = new JSONArray();
for (Map.Entry<UUID, Integer> entry : usageCounts.entrySet()) {
JSONObject entryJson = new JSONObject();
try {
entryJson.put("uuid", entry.getKey());
entryJson.put("count", entry.getValue());
usageCountJson.put(entryJson);
} catch (JSONException e) {
e.printStackTrace();
}
}
_prefs.edit().putString("pref_usage_count", usageCountJson.toString()).apply();
}
public int getTimeout() {
return _prefs.getInt("pref_timeout", -1);
}
public String getLanguage() {
return _prefs.getString("pref_lang", "system");
}
public void setLanguage(String lang) {
_prefs.edit().putString("pref_lang", lang).apply();
}
public Locale getLocale() {
String lang = getLanguage();
if (lang.equals("system")) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return Resources.getSystem().getConfiguration().getLocales().get(0);
} else {
return Resources.getSystem().getConfiguration().locale;
}
}
String[] parts = lang.split("_");
if (parts.length == 1) {
return new Locale(parts[0]);
}
return new Locale(parts[0], parts[1]);
}
public boolean isAndroidBackupsEnabled() {
return _prefs.getBoolean("pref_android_backups", false);
}
public void setIsAndroidBackupsEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_android_backups", enabled).apply();
setAndroidBackupResult(null);
}
public boolean isBackupsEnabled() {
return _prefs.getBoolean("pref_backups", false);
}
public void setIsBackupsEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_backups", enabled).apply();
setBuiltInBackupResult(null);
}
public boolean isBackupReminderEnabled() {
return _prefs.getBoolean("pref_backup_reminder", true);
}
public void setIsBackupReminderEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_backup_reminder", enabled).apply();
}
public Uri getBackupsLocation() {
String str = _prefs.getString("pref_backups_location", null);
if (str != null) {
return Uri.parse(str);
}
return null;
}
public boolean getFocusSearchEnabled() {
return _prefs.getBoolean("pref_focus_search", false);
}
public void setFocusSearch(boolean enabled) {
_prefs.edit().putBoolean("pref_focus_search", enabled).apply();
}
public void setLatestExportTimeNow() {
_prefs.edit().putLong("pref_export_latest", new Date().getTime()).apply();
setIsBackupReminderNeeded(false);
}
public Date getLatestBackupOrExportTime() {
List<Date> dates = new ArrayList<>();
long l = _prefs.getLong("pref_export_latest", 0);
if (l > 0) {
dates.add(new Date(l));
}
BackupResult builtinRes = getBuiltInBackupResult();
if (builtinRes != null) {
dates.add(builtinRes.getTime());
}
BackupResult androidRes = getAndroidBackupResult();
if (androidRes != null) {
dates.add(androidRes.getTime());
}
if (dates.size() == 0) {
return null;
}
return Collections.max(dates, Date::compareTo);
}
public void setBackupsLocation(Uri location) {
_prefs.edit().putString("pref_backups_location", location == null ? null : location.toString()).apply();
}
public int getBackupsVersionCount() {
return _prefs.getInt("pref_backups_versions", 5);
}
public void setBackupsVersionCount(int versions) {
_prefs.edit().putInt("pref_backups_versions", versions).apply();
}
public void setAndroidBackupResult(@Nullable BackupResult res) {
setBackupResult(false, res);
}
public void setBuiltInBackupResult(@Nullable BackupResult res) {
setBackupResult(true, res);
}
@Nullable
public BackupResult getAndroidBackupResult() {
return getBackupResult(false);
}
@Nullable
public BackupResult getBuiltInBackupResult() {
return getBackupResult(true);
}
@Nullable
public Preferences.BackupResult getErroredBackupResult() {
Preferences.BackupResult res = getBuiltInBackupResult();
if (res != null && !res.isSuccessful()) {
return res;
}
res = getAndroidBackupResult();
if (res != null && !res.isSuccessful()) {
return res;
}
return null;
}
private void setBackupResult(boolean isBuiltInBackup, @Nullable BackupResult res) {
String json = null;
if (res != null) {
res.setIsBuiltIn(isBuiltInBackup);
json = res.toJson();
}
_prefs.edit().putString(getBackupResultKey(isBuiltInBackup), json).apply();
}
@Nullable
private BackupResult getBackupResult(boolean isBuiltInBackup) {
String json = _prefs.getString(getBackupResultKey(isBuiltInBackup), null);
if (json == null) {
return null;
}
try {
BackupResult res = BackupResult.fromJson(json);
res.setIsBuiltIn(isBuiltInBackup);
return res;
} catch (JSONException e) {
return null;
}
}
private static String getBackupResultKey(boolean isBuiltInBackup) {
return isBuiltInBackup ? "pref_backups_result_builtin": "pref_backups_result_android";
}
public void setIsBackupReminderNeeded(boolean needed) {
if (isBackupsReminderNeeded() != needed) {
_prefs.edit().putBoolean("pref_backups_reminder_needed", needed).apply();
}
}
public boolean isBackupsReminderNeeded() {
return _prefs.getBoolean("pref_backups_reminder_needed", false);
}
public void setIsPlaintextBackupWarningNeeded(boolean needed) {
_prefs.edit().putBoolean("pref_plaintext_backup_warning_needed", needed).apply();
}
public boolean isPlaintextBackupWarningNeeded() {
return !isPlaintextBackupWarningDisabled()
&& _prefs.getBoolean("pref_plaintext_backup_warning_needed", false);
}
public void setIsPlaintextBackupWarningDisabled(boolean disabled) {
_prefs.edit().putBoolean("pref_plaintext_backup_warning_disabled", disabled).apply();
}
public boolean isPlaintextBackupWarningDisabled() {
return _prefs.getBoolean("pref_plaintext_backup_warning_disabled", false);
}
public boolean isPinKeyboardEnabled() {
return _prefs.getBoolean("pref_pin_keyboard", false);
}
public boolean isTimeSyncWarningEnabled() {
return _prefs.getBoolean("pref_warn_time_sync", true);
}
public void setIsTimeSyncWarningEnabled(boolean enabled) {
_prefs.edit().putBoolean("pref_warn_time_sync", enabled).apply();
}
public CopyBehavior getCopyBehavior() {
return CopyBehavior.fromInteger(_prefs.getInt("pref_current_copy_behavior", 0));
}
public void setCopyBehavior(CopyBehavior copyBehavior) {
_prefs.edit().putInt("pref_current_copy_behavior", copyBehavior.ordinal()).apply();
}
public boolean isMinimizeOnCopyEnabled() {
return _prefs.getBoolean("pref_minimize_on_copy", false);
}
public void setGroupFilter(Set<UUID> groupFilter) {
JSONArray json = new JSONArray(groupFilter);
_prefs.edit().putString("pref_group_filter_uuids", json.toString()).apply();
}
public Set<UUID> getGroupFilter() {
String raw = _prefs.getString("pref_group_filter_uuids", null);
if (raw == null || raw.isEmpty()) {
return Collections.emptySet();
}
try {
JSONArray json = new JSONArray(raw);
Set<UUID> filter = new HashSet<>();
for (int i = 0; i < json.length(); i++) {
filter.add(json.isNull(i) ? null : UUID.fromString(json.getString(i)));
}
return filter;
} catch (JSONException e) {
return Collections.emptySet();
}
}
@NonNull
public BackupsVersioningStrategy getBackupVersioningStrategy() {
Uri uri = getBackupsLocation();
if (uri == null) {
return BackupsVersioningStrategy.UNDEFINED;
}
if (DocumentsContractCompat.isTreeUri(uri)) {
return BackupsVersioningStrategy.MULTIPLE_BACKUPS;
} else {
return BackupsVersioningStrategy.SINGLE_BACKUP;
}
}
public static class BackupResult {
private final Date _time;
private boolean _isBuiltIn;
private final String _error;
private final boolean _isPermissionError;
public BackupResult(@Nullable Exception e) {
this(new Date(), e == null ? null : e.toString(), e instanceof VaultBackupPermissionException);
}
private BackupResult(Date time, @Nullable String error, boolean isPermissionError) {
_time = time;
_error = error;
_isPermissionError = isPermissionError;
}
@Nullable
public String getError() {
return _error;
}
public boolean isSuccessful() {
return _error == null;
}
public Date getTime() {
return _time;
}
public String getElapsedSince(Context context) {
return TimeUtils.getElapsedSince(context, _time);
}
public boolean isBuiltIn() {
return _isBuiltIn;
}
private void setIsBuiltIn(boolean isBuiltIn) {
_isBuiltIn = isBuiltIn;
}
public boolean isPermissionError() {
return _isPermissionError;
}
public String toJson() {
JSONObject obj = new JSONObject();
try {
obj.put("time", _time.getTime());
obj.put("error", _error == null ? JSONObject.NULL : _error);
obj.put("isPermissionError", _isPermissionError);
} catch (JSONException e) {
throw new RuntimeException(e);
}
return obj.toString();
}
public static BackupResult fromJson(String json) throws JSONException {
JSONObject obj = new JSONObject(json);
long time = obj.getLong("time");
String error = JsonUtils.optString(obj, "error");
boolean isPermissionError = obj.optBoolean("isPermissionError");
return new BackupResult(new Date(time), error, isPermissionError);
}
}
public enum CodeGrouping {
HALVES(-1),
NO_GROUPING(-2),
GROUPING_TWOS(2),
GROUPING_THREES(3),
GROUPING_FOURS(4);
private final int _value;
CodeGrouping(int value) {
_value = value;
}
public int getValue() {
return _value;
}
}
}

View file

@ -0,0 +1,77 @@
package com.beemdevelopment.aegis;
import com.beemdevelopment.aegis.helpers.comparators.LastUsedComparator;
import com.beemdevelopment.aegis.helpers.comparators.UsageCountComparator;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.helpers.comparators.AccountNameComparator;
import com.beemdevelopment.aegis.helpers.comparators.IssuerNameComparator;
import java.util.Collections;
import java.util.Comparator;
public enum SortCategory {
CUSTOM,
ACCOUNT,
ACCOUNT_REVERSED,
ISSUER,
ISSUER_REVERSED,
USAGE_COUNT,
LAST_USED;
private static SortCategory[] _values;
static {
_values = values();
}
public static SortCategory fromInteger(int x) {
return _values[x];
}
public Comparator<VaultEntry> getComparator() {
Comparator<VaultEntry> comparator = null;
switch (this) {
case ACCOUNT:
comparator = new AccountNameComparator().thenComparing(new IssuerNameComparator());
break;
case ACCOUNT_REVERSED:
comparator = Collections.reverseOrder(new AccountNameComparator().thenComparing(new IssuerNameComparator()));
break;
case ISSUER:
comparator = new IssuerNameComparator().thenComparing(new AccountNameComparator());
break;
case ISSUER_REVERSED:
comparator = Collections.reverseOrder(new IssuerNameComparator().thenComparing(new AccountNameComparator()));
break;
case USAGE_COUNT:
comparator = Collections.reverseOrder(new UsageCountComparator());
break;
case LAST_USED:
comparator = Collections.reverseOrder(new LastUsedComparator());
}
return comparator;
}
public int getMenuItem() {
switch (this) {
case CUSTOM:
return R.id.menu_sort_custom;
case ACCOUNT:
return R.id.menu_sort_alphabetically_name;
case ACCOUNT_REVERSED:
return R.id.menu_sort_alphabetically_name_reverse;
case ISSUER:
return R.id.menu_sort_alphabetically;
case ISSUER_REVERSED:
return R.id.menu_sort_alphabetically_reverse;
case USAGE_COUNT:
return R.id.menu_sort_usage_count;
case LAST_USED:
return R.id.menu_sort_last_used;
default:
return R.id.menu_sort_custom;
}
}
}

View file

@ -0,0 +1,19 @@
package com.beemdevelopment.aegis;
public enum Theme {
LIGHT,
DARK,
AMOLED,
SYSTEM,
SYSTEM_AMOLED;
private static Theme[] _values;
static {
_values = values();
}
public static Theme fromInteger(int x) {
return _values[x];
}
}

View file

@ -0,0 +1,17 @@
package com.beemdevelopment.aegis;
import com.google.common.collect.ImmutableMap;
import java.util.Map;
public class ThemeMap {
private ThemeMap() {
}
public static final Map<Theme, Integer> DEFAULT = ImmutableMap.of(
Theme.LIGHT, R.style.Theme_Aegis_Light,
Theme.DARK, R.style.Theme_Aegis_Dark,
Theme.AMOLED, R.style.Theme_Aegis_Amoled
);
}

View file

@ -0,0 +1,12 @@
package com.beemdevelopment.aegis;
import java.util.Arrays;
public class VibrationPatterns {
public static final long[] EXPIRING = {475, 20, 5, 20, 965, 20, 5, 20, 965, 20, 5, 20, 420};
public static final long[] REFRESH_CODE = {0, 100};
public static long getLengthInMillis(long[] pattern) {
return Arrays.stream(pattern).sum();
}
}

View file

@ -0,0 +1,65 @@
package com.beemdevelopment.aegis;
import androidx.annotation.LayoutRes;
public enum ViewMode {
NORMAL,
COMPACT,
SMALL,
TILES;
private static ViewMode[] _values;
static {
_values = values();
}
public static ViewMode fromInteger(int x) {
return _values[x];
}
@LayoutRes
public int getLayoutId() {
switch (this) {
case NORMAL:
return R.layout.card_entry;
case COMPACT:
return R.layout.card_entry_compact;
case SMALL:
return R.layout.card_entry_small;
case TILES:
return R.layout.card_entry_tile;
default:
return R.layout.card_entry;
}
}
/**
* Retrieves the offset (in dp) that should exist between entries in this view mode.
*/
public float getItemOffset() {
if (this == ViewMode.COMPACT) {
return 1;
} else if (this == ViewMode.TILES) {
return 4;
}
return 8;
}
public int getSpanCount() {
if (this == ViewMode.TILES) {
return 2;
}
return 1;
}
public String getFormattedAccountName(String accountName) {
if (this == ViewMode.TILES) {
return accountName;
}
return String.format("(%s)", accountName);
}
}

View file

@ -0,0 +1,46 @@
package com.beemdevelopment.aegis.crypto;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.Serializable;
public class CryptParameters implements Serializable {
private byte[] _nonce;
private byte[] _tag;
public CryptParameters(byte[] nonce, byte[] tag) {
_nonce = nonce;
_tag = tag;
}
public JSONObject toJson() {
JSONObject obj = new JSONObject();
try {
obj.put("nonce", Hex.encode(_nonce));
obj.put("tag", Hex.encode(_tag));
} catch (JSONException e) {
throw new RuntimeException(e);
}
return obj;
}
public static CryptParameters fromJson(JSONObject obj) throws JSONException, EncodingException {
byte[] nonce = Hex.decode(obj.getString("nonce"));
byte[] tag = Hex.decode(obj.getString("tag"));
return new CryptParameters(nonce, tag);
}
public byte[] getNonce() {
return _nonce;
}
public byte[] getTag() {
return _tag;
}
}

View file

@ -0,0 +1,19 @@
package com.beemdevelopment.aegis.crypto;
public class CryptResult {
private byte[] _data;
private CryptParameters _params;
public CryptResult(byte[] data, CryptParameters params) {
_data = data;
_params = params;
}
public byte[] getData() {
return _data;
}
public CryptParameters getParams() {
return _params;
}
}

View file

@ -0,0 +1,138 @@
package com.beemdevelopment.aegis.crypto;
import com.beemdevelopment.aegis.crypto.bc.SCrypt;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class CryptoUtils {
public static final String CRYPTO_AEAD = "AES/GCM/NoPadding";
public static final byte CRYPTO_AEAD_KEY_SIZE = 32;
public static final byte CRYPTO_AEAD_TAG_SIZE = 16;
public static final byte CRYPTO_AEAD_NONCE_SIZE = 12;
public static final int CRYPTO_SCRYPT_N = 1 << 15;
public static final int CRYPTO_SCRYPT_r = 8;
public static final int CRYPTO_SCRYPT_p = 1;
public static SecretKey deriveKey(byte[] input, SCryptParameters params) {
byte[] keyBytes = SCrypt.generate(input, params.getSalt(), params.getN(), params.getR(), params.getP(), CRYPTO_AEAD_KEY_SIZE);
return new SecretKeySpec(keyBytes, 0, keyBytes.length, "AES");
}
public static SecretKey deriveKey(char[] password, SCryptParameters params) {
byte[] bytes = toBytes(password);
return deriveKey(bytes, params);
}
public static Cipher createEncryptCipher(SecretKey key)
throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, InvalidKeyException {
return createCipher(key, Cipher.ENCRYPT_MODE, null);
}
public static Cipher createDecryptCipher(SecretKey key, byte[] nonce)
throws InvalidAlgorithmParameterException, NoSuchAlgorithmException,
InvalidKeyException, NoSuchPaddingException {
return createCipher(key, Cipher.DECRYPT_MODE, nonce);
}
private static Cipher createCipher(SecretKey key, int opmode, byte[] nonce)
throws NoSuchPaddingException, NoSuchAlgorithmException,
InvalidAlgorithmParameterException, InvalidKeyException {
Cipher cipher = Cipher.getInstance(CRYPTO_AEAD);
// generate the nonce if none is given
// we are not allowed to do this ourselves as "setRandomizedEncryptionRequired" is set to true
if (nonce != null) {
AlgorithmParameterSpec spec = new GCMParameterSpec(CRYPTO_AEAD_TAG_SIZE * 8, nonce);
cipher.init(opmode, key, spec);
} else {
cipher.init(opmode, key);
}
return cipher;
}
public static CryptResult encrypt(byte[] data, Cipher cipher)
throws BadPaddingException, IllegalBlockSizeException {
// split off the tag to store it separately
byte[] result = cipher.doFinal(data);
byte[] tag = Arrays.copyOfRange(result, result.length - CRYPTO_AEAD_TAG_SIZE, result.length);
byte[] encrypted = Arrays.copyOfRange(result, 0, result.length - CRYPTO_AEAD_TAG_SIZE);
return new CryptResult(encrypted, new CryptParameters(cipher.getIV(), tag));
}
public static CryptResult decrypt(byte[] encrypted, Cipher cipher, CryptParameters params)
throws IOException, BadPaddingException, IllegalBlockSizeException {
return decrypt(encrypted, 0, encrypted.length, cipher, params);
}
public static CryptResult decrypt(byte[] encrypted, int encryptedOffset, int encryptedLen, Cipher cipher, CryptParameters params)
throws IOException, BadPaddingException, IllegalBlockSizeException {
// append the tag to the ciphertext
ByteArrayOutputStream stream = new ByteArrayOutputStream();
stream.write(encrypted, encryptedOffset, encryptedLen);
stream.write(params.getTag());
encrypted = stream.toByteArray();
byte[] decrypted = cipher.doFinal(encrypted);
return new CryptResult(decrypted, params);
}
public static SecretKey generateKey() {
try {
KeyGenerator generator = KeyGenerator.getInstance("AES");
generator.init(CRYPTO_AEAD_KEY_SIZE * 8);
return generator.generateKey();
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
public static byte[] generateSalt() {
return generateRandomBytes(CRYPTO_AEAD_KEY_SIZE);
}
public static byte[] generateRandomBytes(int length) {
SecureRandom random = new SecureRandom();
byte[] data = new byte[length];
random.nextBytes(data);
return data;
}
public static byte[] toBytes(char[] chars) {
CharBuffer charBuf = CharBuffer.wrap(chars);
ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(charBuf);
byte[] bytes = new byte[byteBuf.limit()];
byteBuf.get(bytes);
return bytes;
}
@Deprecated
public static byte[] toBytesOld(char[] chars) {
CharBuffer charBuf = CharBuffer.wrap(chars);
ByteBuffer byteBuf = StandardCharsets.UTF_8.encode(charBuf);
return byteBuf.array();
}
}

View file

@ -0,0 +1,122 @@
package com.beemdevelopment.aegis.crypto;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.ProviderException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Collections;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
public class KeyStoreHandle {
private final KeyStore _keyStore;
private static final String STORE_NAME = "AndroidKeyStore";
public KeyStoreHandle() throws KeyStoreHandleException {
try {
_keyStore = KeyStore.getInstance(STORE_NAME);
_keyStore.load(null);
} catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e) {
throw new KeyStoreHandleException(e);
}
}
public boolean containsKey(String id) throws KeyStoreHandleException {
try {
return _keyStore.containsAlias(id);
} catch (KeyStoreException e) {
throw new KeyStoreHandleException(e);
}
}
public SecretKey generateKey(String id) throws KeyStoreHandleException {
try {
KeyGenerator generator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, STORE_NAME);
generator.init(new KeyGenParameterSpec.Builder(id,
KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true)
.setRandomizedEncryptionRequired(true)
.setKeySize(CryptoUtils.CRYPTO_AEAD_KEY_SIZE * 8)
.build());
return generator.generateKey();
} catch (ProviderException e) {
// a ProviderException can occur at runtime with buggy Keymaster HAL implementations
// so if this was caused by an android.security.KeyStoreException, throw a KeyStoreHandleException instead
Throwable cause = e.getCause();
if (cause != null && cause.getClass().getName().equals("android.security.KeyStoreException")) {
throw new KeyStoreHandleException(cause);
}
throw e;
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
throw new KeyStoreHandleException(e);
}
}
public SecretKey getKey(String id) throws KeyStoreHandleException {
SecretKey key;
try {
key = (SecretKey) _keyStore.getKey(id, null);
} catch (UnrecoverableKeyException e) {
return null;
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
} catch (KeyStoreException e) {
throw new KeyStoreHandleException(e);
}
if (isKeyPermanentlyInvalidated(key)) {
return null;
}
return key;
}
private static boolean isKeyPermanentlyInvalidated(SecretKey key) {
// try to initialize a dummy cipher and see if an InvalidKeyException is thrown
try {
Cipher cipher = Cipher.getInstance(CryptoUtils.CRYPTO_AEAD);
cipher.init(Cipher.ENCRYPT_MODE, key);
} catch (InvalidKeyException e) {
// some devices throw a plain InvalidKeyException, not KeyPermanentlyInvalidatedException
return true;
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
throw new RuntimeException(e);
}
return false;
}
public void deleteKey(String id) throws KeyStoreHandleException {
try {
_keyStore.deleteEntry(id);
} catch (KeyStoreException e) {
throw new KeyStoreHandleException(e);
}
}
public void clear() throws KeyStoreHandleException {
try {
for (String alias : Collections.list(_keyStore.aliases())) {
deleteKey(alias);
}
} catch (KeyStoreException e) {
throw new KeyStoreHandleException(e);
}
}
}

View file

@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.crypto;
public class KeyStoreHandleException extends Exception {
public KeyStoreHandleException(Throwable cause) {
super(cause);
}
public KeyStoreHandleException(String message) {
super(message);
}
}

View file

@ -0,0 +1,61 @@
package com.beemdevelopment.aegis.crypto;
import java.io.IOException;
import java.io.Serializable;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
public class MasterKey implements Serializable {
private SecretKey _key;
public MasterKey(SecretKey key) {
if (key == null) {
throw new IllegalArgumentException("Key cannot be null");
}
_key = key;
}
public static MasterKey generate() {
return new MasterKey(CryptoUtils.generateKey());
}
public CryptResult encrypt(byte[] bytes) throws MasterKeyException {
try {
Cipher cipher = CryptoUtils.createEncryptCipher(_key);
return CryptoUtils.encrypt(bytes, cipher);
} catch (NoSuchPaddingException
| NoSuchAlgorithmException
| InvalidAlgorithmParameterException
| InvalidKeyException
| BadPaddingException
| IllegalBlockSizeException e) {
throw new MasterKeyException(e);
}
}
public CryptResult decrypt(byte[] bytes, CryptParameters params) throws MasterKeyException {
try {
Cipher cipher = CryptoUtils.createDecryptCipher(_key, params.getNonce());
return CryptoUtils.decrypt(bytes, cipher, params);
} catch (NoSuchPaddingException
| NoSuchAlgorithmException
| InvalidAlgorithmParameterException
| InvalidKeyException
| BadPaddingException
| IOException
| IllegalBlockSizeException e) {
throw new MasterKeyException(e);
}
}
public byte[] getBytes() {
return _key.getEncoded();
}
}

View file

@ -0,0 +1,7 @@
package com.beemdevelopment.aegis.crypto;
public class MasterKeyException extends Exception {
public MasterKeyException(Throwable cause) {
super(cause);
}
}

View file

@ -0,0 +1,33 @@
package com.beemdevelopment.aegis.crypto;
import java.io.Serializable;
public class SCryptParameters implements Serializable {
private int _n;
private int _r;
private int _p;
private byte[] _salt;
public SCryptParameters(int n, int r, int p, byte[] salt) {
_n = n;
_r = r;
_p = p;
_salt = salt;
}
public byte[] getSalt() {
return _salt;
}
public int getN() {
return _n;
}
public int getR() {
return _r;
}
public int getP() {
return _p;
}
}

View file

@ -0,0 +1,255 @@
/*
Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
package com.beemdevelopment.aegis.crypto.bc;
import org.bouncycastle.crypto.PBEParametersGenerator;
import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.util.Arrays;
import org.bouncycastle.util.Integers;
import org.bouncycastle.util.Pack;
/**
* Implementation of the scrypt a password-based key derivation function.
* <p>
* Scrypt was created by Colin Percival and is specified in <a
* href="https://tools.ietf.org/html/rfc7914">RFC 7914 - The scrypt Password-Based Key Derivation Function</a>
*/
public class SCrypt
{
private SCrypt()
{
// not used.
}
/**
* Generate a key using the scrypt key derivation function.
*
* @param P the bytes of the pass phrase.
* @param S the salt to use for this invocation.
* @param N CPU/Memory cost parameter. Must be larger than 1, a power of 2 and less than
* <code>2^(128 * r / 8)</code>.
* @param r the block size, must be &gt;= 1.
* @param p Parallelization parameter. Must be a positive integer less than or equal to
* <code>Integer.MAX_VALUE / (128 * r * 8)</code>.
* @param dkLen the length of the key to generate.
* @return the generated key.
*/
public static byte[] generate(byte[] P, byte[] S, int N, int r, int p, int dkLen)
{
if (P == null)
{
throw new IllegalArgumentException("Passphrase P must be provided.");
}
if (S == null)
{
throw new IllegalArgumentException("Salt S must be provided.");
}
if (N <= 1 || !isPowerOf2(N))
{
throw new IllegalArgumentException("Cost parameter N must be > 1 and a power of 2");
}
// Only value of r that cost (as an int) could be exceeded for is 1
if (r == 1 && N >= 65536)
{
throw new IllegalArgumentException("Cost parameter N must be > 1 and < 65536.");
}
if (r < 1)
{
throw new IllegalArgumentException("Block size r must be >= 1.");
}
int maxParallel = Integer.MAX_VALUE / (128 * r * 8);
if (p < 1 || p > maxParallel)
{
throw new IllegalArgumentException("Parallelisation parameter p must be >= 1 and <= " + maxParallel
+ " (based on block size r of " + r + ")");
}
if (dkLen < 1)
{
throw new IllegalArgumentException("Generated key length dkLen must be >= 1.");
}
return MFcrypt(P, S, N, r, p, dkLen);
}
private static byte[] MFcrypt(byte[] P, byte[] S, int N, int r, int p, int dkLen)
{
int MFLenBytes = r * 128;
byte[] bytes = SingleIterationPBKDF2(P, S, p * MFLenBytes);
int[] B = null;
try
{
int BLen = bytes.length >>> 2;
B = new int[BLen];
Pack.littleEndianToInt(bytes, 0, B);
/*
* Chunk memory allocations; We choose 'd' so that there will be 2**d chunks, each not
* larger than 32KiB, except that the minimum chunk size is 2 * r * 32.
*/
int d = 0, total = N * r;
while ((N - d) > 2 && total > (1 << 10))
{
++d;
total >>>= 1;
}
int MFLenWords = MFLenBytes >>> 2;
for (int BOff = 0; BOff < BLen; BOff += MFLenWords)
{
// TODO These can be done in parallel threads
SMix(B, BOff, N, d, r);
}
Pack.intToLittleEndian(B, bytes, 0);
return SingleIterationPBKDF2(P, bytes, dkLen);
}
finally
{
Clear(bytes);
Clear(B);
}
}
private static byte[] SingleIterationPBKDF2(byte[] P, byte[] S, int dkLen)
{
PBEParametersGenerator pGen = new PKCS5S2ParametersGenerator(new SHA256Digest());
pGen.init(P, S, 1);
KeyParameter key = (KeyParameter)pGen.generateDerivedMacParameters(dkLen * 8);
return key.getKey();
}
private static void SMix(int[] B, int BOff, int N, int d, int r)
{
int powN = Integers.numberOfTrailingZeros(N);
int blocksPerChunk = N >>> d;
int chunkCount = 1 << d, chunkMask = blocksPerChunk - 1, chunkPow = powN - d;
int BCount = r * 32;
int[] blockX1 = new int[16];
int[] blockX2 = new int[16];
int[] blockY = new int[BCount];
int[] X = new int[BCount];
int[][] VV = new int[chunkCount][];
try
{
System.arraycopy(B, BOff, X, 0, BCount);
for (int c = 0; c < chunkCount; ++c)
{
int[] V = new int[blocksPerChunk * BCount];
VV[c] = V;
int off = 0;
for (int i = 0; i < blocksPerChunk; i += 2)
{
System.arraycopy(X, 0, V, off, BCount);
off += BCount;
BlockMix(X, blockX1, blockX2, blockY, r);
System.arraycopy(blockY, 0, V, off, BCount);
off += BCount;
BlockMix(blockY, blockX1, blockX2, X, r);
}
}
int mask = N - 1;
for (int i = 0; i < N; ++i)
{
int j = X[BCount - 16] & mask;
int[] V = VV[j >>> chunkPow];
int VOff = (j & chunkMask) * BCount;
System.arraycopy(V, VOff, blockY, 0, BCount);
Xor(blockY, X, 0, blockY);
BlockMix(blockY, blockX1, blockX2, X, r);
}
System.arraycopy(X, 0, B, BOff, BCount);
}
finally
{
ClearAll(VV);
ClearAll(new int[][]{X, blockX1, blockX2, blockY});
}
}
private static void BlockMix(int[] B, int[] X1, int[] X2, int[] Y, int r)
{
System.arraycopy(B, B.length - 16, X1, 0, 16);
int BOff = 0, YOff = 0, halfLen = B.length >>> 1;
for (int i = 2 * r; i > 0; --i)
{
Xor(X1, B, BOff, X2);
Salsa20Engine.salsaCore(8, X2, X1);
System.arraycopy(X1, 0, Y, YOff, 16);
YOff = halfLen + BOff - YOff;
BOff += 16;
}
}
private static void Xor(int[] a, int[] b, int bOff, int[] output)
{
for (int i = output.length - 1; i >= 0; --i)
{
output[i] = a[i] ^ b[bOff + i];
}
}
private static void Clear(byte[] array)
{
if (array != null)
{
Arrays.fill(array, (byte)0);
}
}
private static void Clear(int[] array)
{
if (array != null)
{
Arrays.fill(array, 0);
}
}
private static void ClearAll(int[][] arrays)
{
for (int i = 0; i < arrays.length; ++i)
{
Clear(arrays[i]);
}
}
// note: we know X is non-zero
private static boolean isPowerOf2(int x)
{
return ((x & (x - 1)) == 0);
}
}

View file

@ -0,0 +1,118 @@
/*
Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
package com.beemdevelopment.aegis.crypto.bc;
/**
* Implementation of Daniel J. Bernstein's Salsa20 stream cipher, Snuffle 2005
*/
public class Salsa20Engine {
private Salsa20Engine()
{
}
public static void salsaCore(int rounds, int[] input, int[] x)
{
if (input.length != 16)
{
throw new IllegalArgumentException();
}
if (x.length != 16)
{
throw new IllegalArgumentException();
}
if (rounds % 2 != 0)
{
throw new IllegalArgumentException("Number of rounds must be even");
}
int x00 = input[ 0];
int x01 = input[ 1];
int x02 = input[ 2];
int x03 = input[ 3];
int x04 = input[ 4];
int x05 = input[ 5];
int x06 = input[ 6];
int x07 = input[ 7];
int x08 = input[ 8];
int x09 = input[ 9];
int x10 = input[10];
int x11 = input[11];
int x12 = input[12];
int x13 = input[13];
int x14 = input[14];
int x15 = input[15];
for (int i = rounds; i > 0; i -= 2)
{
x04 ^= Integer.rotateLeft(x00 + x12, 7);
x08 ^= Integer.rotateLeft(x04 + x00, 9);
x12 ^= Integer.rotateLeft(x08 + x04, 13);
x00 ^= Integer.rotateLeft(x12 + x08, 18);
x09 ^= Integer.rotateLeft(x05 + x01, 7);
x13 ^= Integer.rotateLeft(x09 + x05, 9);
x01 ^= Integer.rotateLeft(x13 + x09, 13);
x05 ^= Integer.rotateLeft(x01 + x13, 18);
x14 ^= Integer.rotateLeft(x10 + x06, 7);
x02 ^= Integer.rotateLeft(x14 + x10, 9);
x06 ^= Integer.rotateLeft(x02 + x14, 13);
x10 ^= Integer.rotateLeft(x06 + x02, 18);
x03 ^= Integer.rotateLeft(x15 + x11, 7);
x07 ^= Integer.rotateLeft(x03 + x15, 9);
x11 ^= Integer.rotateLeft(x07 + x03, 13);
x15 ^= Integer.rotateLeft(x11 + x07, 18);
x01 ^= Integer.rotateLeft(x00 + x03, 7);
x02 ^= Integer.rotateLeft(x01 + x00, 9);
x03 ^= Integer.rotateLeft(x02 + x01, 13);
x00 ^= Integer.rotateLeft(x03 + x02, 18);
x06 ^= Integer.rotateLeft(x05 + x04, 7);
x07 ^= Integer.rotateLeft(x06 + x05, 9);
x04 ^= Integer.rotateLeft(x07 + x06, 13);
x05 ^= Integer.rotateLeft(x04 + x07, 18);
x11 ^= Integer.rotateLeft(x10 + x09, 7);
x08 ^= Integer.rotateLeft(x11 + x10, 9);
x09 ^= Integer.rotateLeft(x08 + x11, 13);
x10 ^= Integer.rotateLeft(x09 + x08, 18);
x12 ^= Integer.rotateLeft(x15 + x14, 7);
x13 ^= Integer.rotateLeft(x12 + x15, 9);
x14 ^= Integer.rotateLeft(x13 + x12, 13);
x15 ^= Integer.rotateLeft(x14 + x13, 18);
}
x[ 0] = x00 + input[ 0];
x[ 1] = x01 + input[ 1];
x[ 2] = x02 + input[ 2];
x[ 3] = x03 + input[ 3];
x[ 4] = x04 + input[ 4];
x[ 5] = x05 + input[ 5];
x[ 6] = x06 + input[ 6];
x[ 7] = x07 + input[ 7];
x[ 8] = x08 + input[ 8];
x[ 9] = x09 + input[ 9];
x[10] = x10 + input[10];
x[11] = x11 + input[11];
x[12] = x12 + input[12];
x[13] = x13 + input[13];
x[14] = x14 + input[14];
x[15] = x15 + input[15];
}
}

View file

@ -0,0 +1,45 @@
package com.beemdevelopment.aegis.crypto.otp;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class HOTP {
private HOTP() {
}
public static OTP generateOTP(byte[] secret, String algo, int digits, long counter)
throws NoSuchAlgorithmException, InvalidKeyException {
byte[] hash = getHash(secret, algo, counter);
// truncate hash to get the HTOP value
// http://tools.ietf.org/html/rfc4226#section-5.4
int offset = hash[hash.length - 1] & 0xf;
int otp = ((hash[offset] & 0x7f) << 24)
| ((hash[offset + 1] & 0xff) << 16)
| ((hash[offset + 2] & 0xff) << 8)
| (hash[offset + 3] & 0xff);
return new OTP(otp, digits);
}
public static byte[] getHash(byte[] secret, String algo, long counter)
throws NoSuchAlgorithmException, InvalidKeyException {
SecretKeySpec key = new SecretKeySpec(secret, "RAW");
// encode counter in big endian
byte[] counterBytes = ByteBuffer.allocate(8)
.order(ByteOrder.BIG_ENDIAN)
.putLong(counter)
.array();
// calculate the hash of the counter
Mac mac = Mac.getInstance(algo);
mac.init(key);
return mac.doFinal(counterBytes);
}
}

View file

@ -0,0 +1,54 @@
package com.beemdevelopment.aegis.crypto.otp;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.beemdevelopment.aegis.encoding.Hex;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MOTP {
private final String _code;
private final int _digits;
private MOTP(String code, int digits) {
_code = code;
_digits = digits;
}
@NonNull
public static MOTP generateOTP(byte[] secret, String algo, int digits, int period, String pin)
throws NoSuchAlgorithmException {
return generateOTP(secret, algo, digits, period, pin, System.currentTimeMillis() / 1000);
}
@NonNull
public static MOTP generateOTP(byte[] secret, String algo, int digits, int period, String pin, long time)
throws NoSuchAlgorithmException {
long timeBasedCounter = time / period;
String secretAsString = Hex.encode(secret);
String toDigest = timeBasedCounter + secretAsString + pin;
String code = getDigest(algo, toDigest.getBytes(StandardCharsets.UTF_8));
return new MOTP(code, digits);
}
@VisibleForTesting
@NonNull
protected static String getDigest(String algo, byte[] toDigest) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance(algo);
byte[] digest = md.digest(toDigest);
return Hex.encode(digest);
}
@NonNull
@Override
public String toString() {
return _code.substring(0, _digits);
}
}

View file

@ -0,0 +1,50 @@
package com.beemdevelopment.aegis.crypto.otp;
import androidx.annotation.NonNull;
public class OTP {
private static final String STEAM_ALPHABET = "23456789BCDFGHJKMNPQRTVWXY";
private final int _code;
private final int _digits;
public OTP(int code, int digits) {
_code = code;
_digits = digits;
}
public int getCode() {
return _code;
}
public int getDigits() {
return _digits;
}
@NonNull
@Override
public String toString() {
int code = _code % (int) Math.pow(10, _digits);
// prepend zeroes if needed
StringBuilder res = new StringBuilder(Long.toString(code));
while (res.length() < _digits) {
res.insert(0, "0");
}
return res.toString();
}
public String toSteamString() {
int code = _code;
StringBuilder res = new StringBuilder();
for (int i = 0; i < _digits; i++) {
char c = STEAM_ALPHABET.charAt(code % STEAM_ALPHABET.length());
res.append(c);
code /= STEAM_ALPHABET.length();
}
return res.toString();
}
}

View file

@ -0,0 +1,21 @@
package com.beemdevelopment.aegis.crypto.otp;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
public class TOTP {
private TOTP() {
}
public static OTP generateOTP(byte[] secret, String algo, int digits, long period, long seconds)
throws InvalidKeyException, NoSuchAlgorithmException {
long counter = (long) Math.floor((double) seconds / period);
return HOTP.generateOTP(secret, algo, digits, counter);
}
public static OTP generateOTP(byte[] secret, String algo, int digits, long period)
throws InvalidKeyException, NoSuchAlgorithmException {
return generateOTP(secret, algo, digits, period, System.currentTimeMillis() / 1000);
}
}

View file

@ -0,0 +1,71 @@
package com.beemdevelopment.aegis.crypto.otp;
import androidx.annotation.NonNull;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class YAOTP {
private static final int EN_ALPHABET_LENGTH = 26;
private final long _code;
private final int _digits;
private YAOTP(long code, int digits) {
_code = code;
_digits = digits;
}
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long period)
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
long seconds = System.currentTimeMillis() / 1000;
return generateOTP(secret, pin, digits, otpAlgo, period, seconds);
}
public static YAOTP generateOTP(byte[] secret, String pin, int digits, String otpAlgo, long period, long seconds)
throws NoSuchAlgorithmException, InvalidKeyException, IOException {
byte[] pinWithHash;
byte[] pinBytes = pin.getBytes(StandardCharsets.UTF_8);
try (ByteArrayOutputStream stream = new ByteArrayOutputStream(pinBytes.length + secret.length)) {
stream.write(pinBytes);
stream.write(secret);
pinWithHash = stream.toByteArray();
}
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] keyHash = md.digest(pinWithHash);
if (keyHash[0] == 0) {
keyHash = Arrays.copyOfRange(keyHash, 1, keyHash.length);
}
long counter = (long) Math.floor((double) seconds / period);
byte[] periodHash = HOTP.getHash(keyHash, otpAlgo, counter);
int offset = periodHash[periodHash.length - 1] & 0xf;
periodHash[offset] &= 0x7f;
long otp = ByteBuffer.wrap(periodHash)
.order(ByteOrder.BIG_ENDIAN)
.getLong(offset);
return new YAOTP(otp, digits);
}
@NonNull
@Override
public String toString() {
long code = _code % (long) Math.pow(EN_ALPHABET_LENGTH, _digits);
char[] chars = new char[_digits];
for (int i = _digits - 1; i >= 0; i--) {
chars[i] = (char) ('a' + (code % EN_ALPHABET_LENGTH));
code /= EN_ALPHABET_LENGTH;
}
return new String(chars);
}
}

View file

@ -0,0 +1,14 @@
package com.beemdevelopment.aegis.crypto.pins;
import info.guardianproject.trustedintents.ApkSignaturePin;
public final class GuardianProjectFDroidRSA2048 extends ApkSignaturePin {
public GuardianProjectFDroidRSA2048() {
fingerprints = new String[]{
"927f7e38b6acbecd84e02dace33efa9a7a2f0979750f28f585688ee38b3a4e28",
};
certificates = new byte[][]{
{48, -126, 3, 95, 48, -126, 2, 71, -96, 3, 2, 1, 2, 2, 4, 28, -30, 107, -102, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, 30, 23, 13, 49, 55, 49, 50, 48, 55, 49, 55, 51, 48, 52, 50, 90, 23, 13, 52, 53, 48, 52, 50, 52, 49, 55, 51, 48, 52, 50, 90, 48, 96, 49, 11, 48, 9, 6, 3, 85, 4, 6, 19, 2, 85, 75, 49, 12, 48, 10, 6, 3, 85, 4, 8, 19, 3, 79, 82, 71, 49, 12, 48, 10, 6, 3, 85, 4, 7, 19, 3, 79, 82, 71, 49, 19, 48, 17, 6, 3, 85, 4, 10, 19, 10, 102, 100, 114, 111, 105, 100, 46, 111, 114, 103, 49, 15, 48, 13, 6, 3, 85, 4, 11, 19, 6, 70, 68, 114, 111, 105, 100, 49, 15, 48, 13, 6, 3, 85, 4, 3, 19, 6, 70, 68, 114, 111, 105, 100, 48, -126, 1, 34, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 1, 5, 0, 3, -126, 1, 15, 0, 48, -126, 1, 10, 2, -126, 1, 1, 0, -107, -115, -106, 1, -26, 72, -105, -99, 62, 3, -55, 34, 99, -112, -68, -20, -115, 31, 34, 118, -50, 12, -32, -59, 74, -58, -37, -87, 21, 105, 36, -82, 13, -51, 66, 4, 55, -111, 13, -46, -7, -69, -15, 36, 118, -7, 101, -86, 123, -83, -103, 110, 116, -54, 112, 46, 12, 96, -76, -48, -70, -33, -81, 52, 59, 73, 107, -126, -72, -25, 32, 93, 29, -20, 5, -41, -27, 123, -9, 104, -31, -59, -1, -83, -93, 99, 85, -116, -62, -55, 18, -63, 6, -51, -110, 33, 9, 7, -49, 102, -20, -122, -124, -68, 93, -102, 31, 48, 86, 96, -99, 105, -52, 95, 12, 57, 99, 12, -24, 70, 40, -99, -20, -21, -85, -70, -105, 95, 117, -31, 126, -126, -39, 46, -62, 59, -23, -74, 108, -12, -56, -40, -96, 79, -37, -82, 1, 99, -104, 48, -60, 92, 14, 109, 127, -22, 31, 115, -27, 108, 9, 92, 118, -45, 103, 117, 57, -50, -82, 114, -113, 68, -82, 87, 96, 111, 72, 65, -63, 12, 31, -34, -31, -55, -101, 101, 101, 59, 73, -119, -122, 82, 28, 47, -108, -85, 59, 46, 89, -93, -1, 9, -11, -51, 63, -44, 109, -76, -103, -26, -49, -80, 6, 52, -27, 73, -104, 40, 2, -101, -124, 60, -52, -105, -70, -24, -62, 88, 38, 53, -99, -92, 31, 119, 26, 79, 60, -124, 25, -115, -89, -115, -109, 0, 6, 122, -78, 116, 82, 3, 39, -67, 45, -43, 17, -39, 2, 3, 1, 0, 1, -93, 33, 48, 31, 48, 29, 6, 3, 85, 29, 14, 4, 22, 4, 20, 63, 109, -42, -109, 25, 22, 7, -37, -22, -41, -38, 58, -56, 2, -68, -38, -22, 65, -28, -60, 48, 13, 6, 9, 42, -122, 72, -122, -9, 13, 1, 1, 11, 5, 0, 3, -126, 1, 1, 0, 94, 17, 31, 36, 85, -11, 85, 44, 19, -80, -20, -92, -118, 93, 40, 45, 96, 31, -3, -37, -110, -96, 102, 81, 61, -74, -125, -117, -112, 58, -47, 17, 78, -18, 111, -116, 26, -91, 73, 100, 84, -99, 21, 87, 73, -106, 108, -51, -125, -21, 119, -88, -78, 2, 82, -109, -64, -9, -86, -112, -115, 66, -86, 46, 71, 107, -65, 96, -102, 47, 35, -45, -126, 33, 34, 121, -25, -85, -121, -56, -42, 22, -1, -95, -86, 81, 100, -70, 113, 104, -73, 22, -19, 79, -19, 52, 62, 42, 76, -112, 94, -34, 42, -57, -75, -90, -58, 118, 127, -106, -39, 108, -56, -79, 103, -33, 22, 3, 47, 103, -76, -81, 53, -22, -44, -26, -102, 63, -99, 39, 38, -108, 75, 33, 10, 25, -110, -125, -115, 114, -69, 73, -112, 36, 74, 77, -82, -44, 29, -123, -8, -117, 71, -105, 15, -109, 51, 22, 4, 80, 1, 43, 118, 121, -113, -70, 83, -56, 82, -110, 4, -63, 16, -57, 126, -70, 81, 73, 61, 2, -61, 24, -14, -10, 4, -21, 90, 24, 66, 41, -57, -60, -113, -18, -54, -1, 103, -75, 32, -64, 67, 103, 109, -79, -12, -113, -27, 114, 89, 116, 115, -13, -123, -70, 61, -41, -46, -118, 29, -105, -97, -75, 39, -51, 60, 88, 125, 55, -46, -95, 52, 57, 52, -115, 80, 44, 109, 119, -116, -62, -77, -74, -88, 41, 57, -65, -71, -115, -67, 23, 66, -21, 56, 51, -91, 109},};
}
}

View file

@ -0,0 +1,15 @@
package com.beemdevelopment.aegis.database;
import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Database(entities = {AuditLogEntry.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract AuditLogDao auditLogDao();
}

View file

@ -0,0 +1,17 @@
package com.beemdevelopment.aegis.database;
import androidx.lifecycle.LiveData;
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.Query;
import java.util.List;
@Dao
public interface AuditLogDao {
@Insert
void insert(AuditLogEntry log);
@Query("SELECT * FROM audit_logs WHERE timestamp >= strftime('%s', 'now', '-30 days') ORDER BY timestamp DESC")
LiveData<List<AuditLogEntry>> getAll();
}

View file

@ -0,0 +1,61 @@
package com.beemdevelopment.aegis.database;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.Ignore;
import androidx.room.PrimaryKey;
import com.beemdevelopment.aegis.EventType;
@Entity(tableName = "audit_logs")
public class AuditLogEntry {
@PrimaryKey(autoGenerate = true)
protected long id;
@NonNull
@ColumnInfo(name = "event_type")
private final EventType _eventType;
@ColumnInfo(name = "reference")
private final String _reference;
@ColumnInfo(name = "timestamp")
private final long _timestamp;
@Ignore
public AuditLogEntry(@NonNull EventType eventType) {
this(eventType, null);
}
@Ignore
public AuditLogEntry(@NonNull EventType eventType, @Nullable String reference) {
_eventType = eventType;
_reference = reference;
_timestamp = System.currentTimeMillis();
}
AuditLogEntry(long id, @NonNull EventType eventType, @Nullable String reference, long timestamp) {
this.id = id;
_eventType = eventType;
_reference = reference;
_timestamp = timestamp;
}
public long getId() {
return id;
}
public EventType getEventType() {
return _eventType;
}
public String getReference() {
return _reference;
}
public long getTimestamp() {
return _timestamp;
}
}

View file

@ -0,0 +1,66 @@
package com.beemdevelopment.aegis.database;
import androidx.lifecycle.LiveData;
import com.beemdevelopment.aegis.EventType;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
public class AuditLogRepository {
private final AuditLogDao _auditLogDao;
private final Executor _executor;
public AuditLogRepository(AuditLogDao auditLogDao) {
_auditLogDao = auditLogDao;
_executor = Executors.newSingleThreadExecutor();
}
public LiveData<List<AuditLogEntry>> getAllAuditLogEntries() {
return _auditLogDao.getAll();
}
public void addVaultUnlockedEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_UNLOCKED);
insert(auditLogEntry);
}
public void addBackupCreatedEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_BACKUP_CREATED);
insert(auditLogEntry);
}
public void addAndroidBackupCreatedEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_ANDROID_BACKUP_CREATED);
insert(auditLogEntry);
}
public void addVaultExportedEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_EXPORTED);
insert(auditLogEntry);
}
public void addEntrySharedEvent(String reference) {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.ENTRY_SHARED, reference);
insert(auditLogEntry);
}
public void addVaultUnlockFailedPasswordEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_UNLOCK_FAILED_PASSWORD);
insert(auditLogEntry);
}
public void addVaultUnlockFailedBiometricsEvent() {
AuditLogEntry auditLogEntry = new AuditLogEntry(EventType.VAULT_UNLOCK_FAILED_BIOMETRICS);
insert(auditLogEntry);
}
public void insert(AuditLogEntry auditLogEntry) {
_executor.execute(() -> {
_auditLogDao.insert(auditLogEntry);
});
}
}

View file

@ -0,0 +1,29 @@
package com.beemdevelopment.aegis.encoding;
import com.google.common.io.BaseEncoding;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
public class Base32 {
private Base32() {
}
public static byte[] decode(String s) throws EncodingException {
try {
return BaseEncoding.base32().decode(s.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
}
}
public static String encode(byte[] data) {
return BaseEncoding.base32().omitPadding().encode(data);
}
public static String encode(String s) {
byte[] bytes = s.getBytes(StandardCharsets.UTF_8);
return encode(bytes);
}
}

View file

@ -0,0 +1,27 @@
package com.beemdevelopment.aegis.encoding;
import com.google.common.io.BaseEncoding;
import java.nio.charset.StandardCharsets;
public class Base64 {
private Base64() {
}
public static byte[] decode(String s) throws EncodingException {
try {
return BaseEncoding.base64().decode(s);
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
}
}
public static byte[] decode(byte[] s) throws EncodingException {
return decode(new String(s, StandardCharsets.UTF_8));
}
public static String encode(byte[] data) {
return BaseEncoding.base64().encode(data);
}
}

View file

@ -0,0 +1,13 @@
package com.beemdevelopment.aegis.encoding;
import java.io.IOException;
public class EncodingException extends IOException {
public EncodingException(Throwable cause) {
super(cause);
}
public EncodingException(String message) {
super(message);
}
}

View file

@ -0,0 +1,23 @@
package com.beemdevelopment.aegis.encoding;
import com.google.common.io.BaseEncoding;
import java.util.Locale;
public class Hex {
private Hex() {
}
public static byte[] decode(String s) throws EncodingException {
try {
return BaseEncoding.base16().decode(s.toUpperCase(Locale.ROOT));
} catch (IllegalArgumentException e) {
throw new EncodingException(e);
}
}
public static String encode(byte[] data) {
return BaseEncoding.base16().lowerCase().encode(data);
}
}

View file

@ -0,0 +1,54 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.provider.Settings;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;
public class AnimationsHelper {
private AnimationsHelper() {
}
public static Animation loadScaledAnimation(Context context, int animationResId) {
return loadScaledAnimation(context, animationResId, Scale.ANIMATOR);
}
public static Animation loadScaledAnimation(Context context, int animationResId, Scale scale) {
Animation animation = AnimationUtils.loadAnimation(context, animationResId);
long newDuration = (long) (animation.getDuration() * scale.getValue(context));
animation.setDuration(newDuration);
return animation;
}
public static LayoutAnimationController loadScaledLayoutAnimation(Context context, int animationResId) {
return loadScaledLayoutAnimation(context, animationResId, Scale.ANIMATOR);
}
public static LayoutAnimationController loadScaledLayoutAnimation(Context context, int animationResId, Scale scale) {
LayoutAnimationController controller = AnimationUtils.loadLayoutAnimation(context, animationResId);
Animation animation = controller.getAnimation();
animation.setDuration((long) (animation.getDuration() * scale.getValue(context)));
return controller;
}
public enum Scale {
ANIMATOR(Settings.Global.ANIMATOR_DURATION_SCALE),
TRANSITION(Settings.Global.TRANSITION_ANIMATION_SCALE);
private final String _setting;
Scale(String setting) {
_setting = setting;
}
public float getValue(Context context) {
return Settings.Global.getFloat(context.getContentResolver(), _setting, 1.0f);
}
public boolean isZero(Context context) {
return getValue(context) == 0;
}
}
}

View file

@ -0,0 +1,136 @@
package com.beemdevelopment.aegis.helpers;
import androidx.annotation.NonNull;
import androidx.biometric.BiometricPrompt;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import com.beemdevelopment.aegis.crypto.KeyStoreHandle;
import com.beemdevelopment.aegis.crypto.KeyStoreHandleException;
import com.beemdevelopment.aegis.vault.slots.BiometricSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
import com.beemdevelopment.aegis.vault.slots.SlotException;
import java.util.Objects;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
/**
* A class that can prepare initialization of a BiometricSlot by generating a new
* key in the Android KeyStore and authenticating a cipher for it through a
* BiometricPrompt.
*/
public class BiometricSlotInitializer extends BiometricPrompt.AuthenticationCallback {
private BiometricSlot _slot;
private Listener _listener;
private BiometricPrompt _prompt;
public BiometricSlotInitializer(Fragment fragment, Listener listener) {
_listener = listener;
_prompt = new BiometricPrompt(fragment, new UiThreadExecutor(), this);
}
public BiometricSlotInitializer(FragmentActivity activity, Listener listener) {
_listener = listener;
_prompt = new BiometricPrompt(activity, new UiThreadExecutor(), this);
}
/**
* Generates a new key in the Android KeyStore for the new BiometricSlot,
* initializes a cipher with it and shows a BiometricPrompt to the user for
* authentication. If authentication is successful, the new slot will be
* initialized and delivered back through the listener.
*/
public void authenticate(BiometricPrompt.PromptInfo info) {
if (_slot != null) {
throw new IllegalStateException("Biometric authentication already in progress");
}
KeyStoreHandle keyStore;
try {
keyStore = new KeyStoreHandle();
} catch (KeyStoreHandleException e) {
fail(e);
return;
}
// generate a new Android KeyStore key
// and assign it the UUID of the new slot as an alias
Cipher cipher;
BiometricSlot slot = new BiometricSlot();
try {
SecretKey key = keyStore.generateKey(slot.getUUID().toString());
cipher = Slot.createEncryptCipher(key);
} catch (KeyStoreHandleException | SlotException e) {
fail(e);
return;
}
_slot = slot;
_prompt.authenticate(info, new BiometricPrompt.CryptoObject(cipher));
}
/**
* Cancels the BiometricPrompt and resets the state of the initializer. It will
* also attempt to delete the previously generated Android KeyStore key.
*/
public void cancelAuthentication() {
if (_slot == null) {
throw new IllegalStateException("Biometric authentication not in progress");
}
reset();
_prompt.cancelAuthentication();
}
private void reset() {
if (_slot != null) {
try {
// clean up the unused KeyStore key
// this is non-critical, so just fail silently if an error occurs
String uuid = _slot.getUUID().toString();
KeyStoreHandle keyStore = new KeyStoreHandle();
if (keyStore.containsKey(uuid)) {
keyStore.deleteKey(uuid);
}
} catch (KeyStoreHandleException e) {
e.printStackTrace();
}
_slot = null;
}
}
private void fail(int errorCode, CharSequence errString) {
reset();
_listener.onSlotInitializationFailed(errorCode, errString);
}
private void fail(Exception e) {
e.printStackTrace();
fail(0, e.toString());
}
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
super.onAuthenticationError(errorCode, errString);
fail(errorCode, errString.toString());
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
super.onAuthenticationSucceeded(result);
_listener.onInitializeSlot(_slot, Objects.requireNonNull(result.getCryptoObject()).getCipher());
}
@Override
public void onAuthenticationFailed() {
super.onAuthenticationFailed();
}
public interface Listener {
void onInitializeSlot(BiometricSlot slot, Cipher cipher);
void onSlotInitializationFailed(int errorCode, @NonNull CharSequence errString);
}
}

View file

@ -0,0 +1,30 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
public class BiometricsHelper {
private BiometricsHelper() {
}
public static BiometricManager getManager(Context context) {
BiometricManager manager = BiometricManager.from(context);
if (manager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG) == BiometricManager.BIOMETRIC_SUCCESS) {
return manager;
}
return null;
}
public static boolean isCanceled(int errorCode) {
return errorCode == BiometricPrompt.ERROR_CANCELED
|| errorCode == BiometricPrompt.ERROR_USER_CANCELED
|| errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON;
}
public static boolean isAvailable(Context context) {
return getManager(context) != null;
}
}

View file

@ -0,0 +1,63 @@
package com.beemdevelopment.aegis.helpers;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import com.beemdevelopment.aegis.icons.IconType;
import com.beemdevelopment.aegis.vault.VaultEntryIcon;
import java.io.ByteArrayOutputStream;
import java.util.Objects;
public class BitmapHelper {
private BitmapHelper() {
}
/**
* Scales the given Bitmap to the given maximum width/height, while keeping the aspect ratio intact.
*/
public static Bitmap resize(Bitmap bitmap, int maxWidth, int maxHeight) {
if (maxHeight <= 0 || maxWidth <= 0) {
return bitmap;
}
float maxRatio = (float) maxWidth / maxHeight;
float ratio = (float) bitmap.getWidth() / bitmap.getHeight();
int width = maxWidth;
int height = maxHeight;
if (maxRatio > 1) {
width = (int) ((float) maxHeight * ratio);
} else {
height = (int) ((float) maxWidth / ratio);
}
return Bitmap.createScaledBitmap(bitmap, width, height, true);
}
public static boolean isVaultEntryIconOptimized(VaultEntryIcon icon) {
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeByteArray(icon.getBytes(), 0, icon.getBytes().length, opts);
return opts.outWidth <= VaultEntryIcon.MAX_DIMENS && opts.outHeight <= VaultEntryIcon.MAX_DIMENS;
}
public static VaultEntryIcon toVaultEntryIcon(Bitmap bitmap, IconType iconType) {
if (bitmap.getWidth() > VaultEntryIcon.MAX_DIMENS
|| bitmap.getHeight() > VaultEntryIcon.MAX_DIMENS) {
bitmap = resize(bitmap, VaultEntryIcon.MAX_DIMENS, VaultEntryIcon.MAX_DIMENS);
}
ByteArrayOutputStream stream = new ByteArrayOutputStream();
if (Objects.equals(iconType, IconType.PNG)) {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
} else {
iconType = IconType.JPEG;
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
}
byte[] data = stream.toByteArray();
return new VaultEntryIcon(data, iconType);
}
}

View file

@ -0,0 +1,30 @@
package com.beemdevelopment.aegis.helpers;
import android.graphics.Rect;
import android.text.TextPaint;
import android.text.style.MetricAffectingSpan;
import androidx.annotation.NonNull;
public class CenterVerticalSpan extends MetricAffectingSpan {
Rect _substringBounds;
public CenterVerticalSpan(Rect substringBounds) {
_substringBounds = substringBounds;
}
@Override
public void updateMeasureState(@NonNull TextPaint textPaint) {
applyBaselineShift(textPaint);
}
@Override
public void updateDrawState(@NonNull TextPaint textPaint) {
applyBaselineShift(textPaint);
}
private void applyBaselineShift(TextPaint textPaint) {
float topDifference = textPaint.getFontMetrics().top - _substringBounds.top;
textPaint.baselineShift -= (topDifference / 2f);
}
}

View file

@ -0,0 +1,39 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.content.ContextWrapper;
import androidx.activity.ComponentActivity;
import androidx.annotation.NonNull;
import androidx.lifecycle.Lifecycle;
import javax.annotation.Nullable;
/**
* ContextHelper contains some disgusting hacks to obtain the Activity/Lifecycle from a Context.
*/
public class ContextHelper {
private ContextHelper() {
}
// source: https://github.com/androidx/androidx/blob/e32e1da51a0c7448c74861c667fa76738a415a89/mediarouter/mediarouter/src/main/java/androidx/mediarouter/app/MediaRouteButton.java#L425-L435
@Nullable
public static ComponentActivity getActivity(@NonNull Context context) {
while (context instanceof ContextWrapper) {
if (context instanceof ComponentActivity) {
return (ComponentActivity) context;
}
context = ((ContextWrapper) context).getBaseContext();
}
return null;
}
@Nullable
public static Lifecycle getLifecycle(@NonNull Context context) {
ComponentActivity activity = getActivity(context);
return activity == null ? null : activity.getLifecycle();
}
}

View file

@ -0,0 +1,27 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.widget.ArrayAdapter;
import android.widget.AutoCompleteTextView;
import androidx.annotation.ArrayRes;
import com.beemdevelopment.aegis.R;
import java.util.List;
public class DropdownHelper {
private DropdownHelper() {
}
public static void fillDropdown(Context context, AutoCompleteTextView dropdown, @ArrayRes int textArrayResId) {
ArrayAdapter<CharSequence> adapter = ArrayAdapter.createFromResource(context, textArrayResId, R.layout.dropdown_list_item);
dropdown.setAdapter(adapter);
}
public static <T> void fillDropdown(Context context, AutoCompleteTextView dropdown, List<T> items) {
ArrayAdapter<T> adapter = new ArrayAdapter<>(context, R.layout.dropdown_list_item, items);
dropdown.setAdapter(adapter);
}
}

View file

@ -0,0 +1,24 @@
package com.beemdevelopment.aegis.helpers;
import android.text.Editable;
import android.widget.EditText;
import java.util.Arrays;
public class EditTextHelper {
private EditTextHelper() {
}
public static char[] getEditTextChars(EditText text) {
Editable editable = text.getText();
char[] chars = new char[editable.length()];
editable.getChars(0, editable.length(), chars, 0);
return chars;
}
public static boolean areEditTextsEqual(EditText text1, EditText text2) {
char[] password = getEditTextChars(text1);
char[] passwordConfirm = getEditTextChars(text2);
return password.length != 0 && Arrays.equals(password, passwordConfirm);
}
}

View file

@ -0,0 +1,57 @@
package com.beemdevelopment.aegis.helpers;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
public class FabScrollHelper {
private View _fabMenu;
private boolean _isAnimating;
public FabScrollHelper(View floatingActionsMenu) {
_fabMenu = floatingActionsMenu;
}
public void onScroll(int dx, int dy) {
if (dy > 2 && _fabMenu.getVisibility() == View.VISIBLE && !_isAnimating) {
setVisible(false);
} else if (dy < -2 && _fabMenu.getVisibility() != View.VISIBLE && !_isAnimating) {
setVisible(true);
}
}
public void setVisible(boolean visible) {
if (visible) {
_fabMenu.setVisibility(View.VISIBLE);
_fabMenu.animate()
.translationY(0)
.setInterpolator(new DecelerateInterpolator(2))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
_isAnimating = false;
super.onAnimationEnd(animation);
}
}).start();
} else {
_isAnimating = true;
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) _fabMenu.getLayoutParams();
int fabBottomMargin = lp.bottomMargin;
_fabMenu.animate()
.translationY(_fabMenu.getHeight() + fabBottomMargin)
.setInterpolator(new AccelerateInterpolator(2))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
_isAnimating = false;
_fabMenu.setVisibility(View.INVISIBLE);
super.onAnimationEnd(animation);
}
}).start();
}
}
}

View file

@ -0,0 +1,39 @@
package com.beemdevelopment.aegis.helpers;
import androidx.recyclerview.widget.RecyclerView;
public interface ItemTouchHelperAdapter {
/**
* Called when an item has been dragged far enough to trigger a move. This is called every time
* an item is shifted, and <strong>not</strong> at the end of a "drop" event.<br/>
* <br/>
* Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after
* adjusting the underlying data to reflect this move.
*
* @param fromPosition The start position of the moved item.
* @param toPosition Then resolved position of the moved item.
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
* @see RecyclerView.ViewHolder#getAdapterPosition()
*/
void onItemMove(int fromPosition, int toPosition);
/**
* Called when an item has been dismissed by a swipe.<br/>
* <br/>
* Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after
* adjusting the underlying data to reflect this removal.
*
* @param position The position of the item dismissed.
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
* @see RecyclerView.ViewHolder#getAdapterPosition()
*/
void onItemDismiss(int position);
/**
* Called when an item has been dropped after a drag.
*
* @param position The position of the moved item.
*/
void onItemDrop(int position);
}

View file

@ -0,0 +1,14 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.util.DisplayMetrics;
public class MetricsHelper {
private MetricsHelper() {
}
public static int convertDpToPixels(Context context, float dp) {
return (int) (dp * (context.getResources().getDisplayMetrics().densityDpi / DisplayMetrics.DENSITY_DEFAULT));
}
}

View file

@ -0,0 +1,73 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.beemdevelopment.aegis.R;
import com.google.android.material.textfield.TextInputLayout;
import com.google.common.base.Strings;
import com.nulabinc.zxcvbn.Strength;
import com.nulabinc.zxcvbn.Zxcvbn;
public class PasswordStrengthHelper {
// Limit the password length to prevent zxcvbn4j from exploding
private static final int MAX_PASSWORD_LENGTH = 64;
// Material design color palette
private final static String[] COLORS = {"#FF5252", "#FF5252", "#FFC107", "#8BC34A", "#4CAF50"};
private final Zxcvbn _zxcvbn = new Zxcvbn();
private final EditText _textPassword;
private final ProgressBar _barPasswordStrength;
private final TextView _textPasswordStrength;
private final TextInputLayout _textPasswordWrapper;
public PasswordStrengthHelper(
EditText textPassword,
ProgressBar barPasswordStrength,
TextView textPasswordStrength,
TextInputLayout textPasswordWrapper
) {
_textPassword = textPassword;
_barPasswordStrength = barPasswordStrength;
_textPasswordStrength = textPasswordStrength;
_textPasswordWrapper = textPasswordWrapper;
}
public void measure(Context context) {
if (_textPassword.getText().length() > MAX_PASSWORD_LENGTH) {
_barPasswordStrength.setProgress(0);
_textPasswordStrength.setText(R.string.password_strength_unknown);
} else {
Strength strength = _zxcvbn.measure(_textPassword.getText());
_barPasswordStrength.setProgress(strength.getScore());
_barPasswordStrength.setProgressTintList(ColorStateList.valueOf(Color.parseColor(getColor(strength.getScore()))));
_textPasswordStrength.setText((_textPassword.getText().length() != 0) ? getString(strength.getScore(), context) : "");
String warning = strength.getFeedback().getWarning();
_textPasswordWrapper.setError(warning);
_textPasswordWrapper.setErrorEnabled(!Strings.isNullOrEmpty(warning));
strength.wipe();
}
}
private static String getString(int score, Context context) {
if (score < 0 || score > 4) {
throw new IllegalArgumentException("Not a valid zxcvbn score");
}
String[] strings = context.getResources().getStringArray(R.array.password_strength);
return strings[score];
}
private static String getColor(int score) {
if (score < 0 || score > 4) {
throw new IllegalArgumentException("Not a valid zxcvbn score");
}
return COLORS[score];
}
}

View file

@ -0,0 +1,46 @@
package com.beemdevelopment.aegis.helpers;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import java.util.ArrayList;
import java.util.List;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
public class PermissionHelper {
private PermissionHelper() {
}
public static boolean granted(Context context, String permission) {
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED;
}
public static boolean request(Activity activity, int requestCode, String... perms) {
List<String> deniedPerms = new ArrayList<>();
for (String permission : perms) {
if (!granted(activity, permission)) {
deniedPerms.add(permission);
}
}
int size = deniedPerms.size();
if (size > 0) {
String[] array = new String[size];
ActivityCompat.requestPermissions(activity, deniedPerms.toArray(array), requestCode);
}
return size == 0;
}
public static boolean checkResults(int[] grantResults) {
for (int result : grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
return false;
}
}
return true;
}
}

View file

@ -0,0 +1,69 @@
package com.beemdevelopment.aegis.helpers;
import static android.graphics.ImageFormat.YUV_420_888;
import android.util.Log;
import android.util.Size;
import androidx.annotation.NonNull;
import androidx.camera.core.ImageAnalysis;
import androidx.camera.core.ImageProxy;
import com.google.zxing.NotFoundException;
import com.google.zxing.PlanarYUVLuminanceSource;
import com.google.zxing.Result;
import java.nio.ByteBuffer;
public class QrCodeAnalyzer implements ImageAnalysis.Analyzer {
private static final String TAG = QrCodeAnalyzer.class.getSimpleName();
public static final Size RESOLUTION = new Size(1200, 1600);
private final QrCodeAnalyzer.Listener _listener;
public QrCodeAnalyzer(QrCodeAnalyzer.Listener listener) {
_listener = listener;
}
@Override
public void analyze(@NonNull ImageProxy image) {
int format = image.getFormat();
if (format != YUV_420_888) {
Log.e(TAG, String.format("Unexpected YUV image format: %d", format));
image.close();
return;
}
ImageProxy.PlaneProxy plane = image.getPlanes()[0];
ByteBuffer buf = plane.getBuffer();
byte[] data = new byte[buf.remaining()];
buf.get(data);
buf.rewind();
PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(
data,
plane.getRowStride(),
image.getHeight(),
0,
0,
image.getWidth(),
image.getHeight(),
false
);
try {
Result result = QrCodeHelper.decodeFromSource(source);
if (_listener != null) {
_listener.onQrCodeDetected(result);
}
} catch (NotFoundException ignored) {
} finally {
image.close();
}
}
public interface Listener {
void onQrCodeDetected(Result result);
}
}

View file

@ -0,0 +1,96 @@
package com.beemdevelopment.aegis.helpers;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import androidx.annotation.ColorInt;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.NotFoundException;
import com.google.zxing.RGBLuminanceSource;
import com.google.zxing.Result;
import com.google.zxing.WriterException;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeWriter;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
public class QrCodeHelper {
private QrCodeHelper() {
}
public static Result decodeFromSource(LuminanceSource source) throws NotFoundException {
Map<DecodeHintType, Object> hints = new HashMap<>();
hints.put(DecodeHintType.POSSIBLE_FORMATS, Collections.singletonList(BarcodeFormat.QR_CODE));
hints.put(DecodeHintType.ALSO_INVERTED, true);
BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
MultiFormatReader reader = new MultiFormatReader();
return reader.decode(bitmap, hints);
}
public static Result decodeFromStream(InputStream inStream) throws DecodeError {
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
Bitmap bitmap = BitmapFactory.decodeStream(inStream, null, bmOptions);
if (bitmap == null) {
throw new DecodeError("Unable to decode stream to bitmap");
}
// If ZXing is not able to decode the image on the first try, we try a couple of
// more times with smaller versions of the same image.
for (int i = 0; i <= 2; i++) {
if (i != 0) {
bitmap = BitmapHelper.resize(bitmap, bitmap.getWidth() / (i * 2), bitmap.getHeight() / (i * 2));
}
try {
int[] pixels = new int[bitmap.getWidth() * bitmap.getHeight()];
bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());
LuminanceSource source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), pixels);
return decodeFromSource(source);
} catch (NotFoundException ignored) {
}
}
throw new DecodeError(NotFoundException.getNotFoundInstance());
}
public static Bitmap encodeToBitmap(String data, int width, int height, @ColorInt int backgroundColor) throws WriterException {
QRCodeWriter writer = new QRCodeWriter();
BitMatrix bitMatrix = writer.encode(data, BarcodeFormat.QR_CODE, width, height);
int[] pixels = new int[width * height];
for (int y = 0; y < height; y++) {
int offset = y * width;
for (int x = 0; x < width; x++) {
pixels[offset + x] = bitMatrix.get(x, y) ? Color.BLACK : backgroundColor;
}
}
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
return bitmap;
}
public static class DecodeError extends Exception {
public DecodeError(String message) {
super(message);
}
public DecodeError(Throwable cause) {
super(cause);
}
}
}

View file

@ -0,0 +1,47 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.provider.OpenableColumns;
import android.webkit.MimeTypeMap;
import androidx.documentfile.provider.DocumentFile;
public class SafHelper {
private SafHelper() {
}
public static String getFileName(Context context, Uri uri) {
if (uri.getScheme() != null && uri.getScheme().equals("content")) {
try (Cursor cursor = context.getContentResolver().query(uri, new String[]{OpenableColumns.DISPLAY_NAME}, null, null, null)) {
if (cursor != null && cursor.moveToFirst()) {
int i = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if (i != -1) {
return cursor.getString(i);
}
}
}
}
return uri.getLastPathSegment();
}
public static String getMimeType(Context context, Uri uri) {
DocumentFile file = DocumentFile.fromSingleUri(context, uri);
if (file != null) {
String fileType = file.getType();
if (fileType != null) {
return fileType;
}
String ext = MimeTypeMap.getFileExtensionFromUrl(uri.toString());
if (ext != null) {
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
}
}
return null;
}
}

View file

@ -0,0 +1,32 @@
package com.beemdevelopment.aegis.helpers;
import android.view.animation.Animation;
public class SimpleAnimationEndListener implements Animation.AnimationListener {
private final Listener _listener;
public SimpleAnimationEndListener(Listener listener) {
_listener = listener;
}
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
if (_listener != null) {
_listener.onAnimationEnd(animation);
}
}
@Override
public void onAnimationRepeat(Animation animation) {
}
public interface Listener {
void onAnimationEnd(Animation animation);
}
}

View file

@ -0,0 +1,111 @@
package com.beemdevelopment.aegis.helpers;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.ItemTouchHelper;
import androidx.recyclerview.widget.RecyclerView;
import com.beemdevelopment.aegis.ui.views.EntryAdapter;
import com.beemdevelopment.aegis.vault.VaultEntry;
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
private VaultEntry _selectedEntry;
private final EntryAdapter _adapter;
private boolean _positionChanged = false;
private boolean _isLongPressDragEnabled = true;
private int _dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
public SimpleItemTouchHelperCallback(EntryAdapter adapter) {
_adapter = adapter;
}
@Override
public boolean isLongPressDragEnabled() {
return _isLongPressDragEnabled;
}
public void setIsLongPressDragEnabled(boolean enabled) {
_isLongPressDragEnabled = enabled;
}
public void setSelectedEntry(VaultEntry entry) {
if (entry == null) {
_selectedEntry = null;
return;
}
if (!entry.isFavorite()) {
_selectedEntry = entry;
}
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
public void setDragFlags(int dragFlags) {
_dragFlags = dragFlags;
}
@Override
public int getMovementFlags(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
// It's not clear when this can happen, but sometimes the ViewHolder
// that's passed to this function has a position of -1, leading
// to a crash down the line.
int position = viewHolder.getBindingAdapterPosition();
if (position == NO_POSITION) {
return 0;
}
EntryAdapter adapter = (EntryAdapter) recyclerView.getAdapter();
if (adapter == null) {
return 0;
}
int swipeFlags = 0;
if (adapter.isPositionFooter(position)
|| adapter.isPositionErrorCard(position)
|| adapter.getEntryAtPosition(position) != _selectedEntry
|| !isLongPressDragEnabled()) {
return makeMovementFlags(0, swipeFlags);
}
return makeMovementFlags(_dragFlags, swipeFlags);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
int targetIndex = _adapter.translateEntryPosToIndex(target.getBindingAdapterPosition());
if (targetIndex < _adapter.getShownFavoritesCount()) {
return false;
}
int firstPosition = viewHolder.getLayoutPosition();
int secondPosition = target.getBindingAdapterPosition();
_adapter.onItemMove(firstPosition, secondPosition);
_positionChanged = true;
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
_adapter.onItemDismiss(viewHolder.getBindingAdapterPosition());
}
@Override
public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);
if (_positionChanged) {
_adapter.onItemDrop(viewHolder.getBindingAdapterPosition());
_positionChanged = false;
_adapter.refresh(false);
}
}
}

View file

@ -0,0 +1,33 @@
package com.beemdevelopment.aegis.helpers;
import android.text.Editable;
import android.text.TextWatcher;
public final class SimpleTextWatcher implements TextWatcher {
private final Listener _listener;
public SimpleTextWatcher(Listener listener) {
_listener = listener;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (_listener != null) {
_listener.afterTextChanged(s);
}
}
public interface Listener {
void afterTextChanged(Editable s);
}
}

View file

@ -0,0 +1,66 @@
package com.beemdevelopment.aegis.helpers;
import android.view.View;
import com.amulyakhare.textdrawable.TextDrawable;
import com.amulyakhare.textdrawable.util.ColorGenerator;
import java.text.BreakIterator;
import java.util.Arrays;
public class TextDrawableHelper {
// taken from: https://materialuicolors.co (level 700)
private static ColorGenerator _generator = ColorGenerator.create(Arrays.asList(
0xFFD32F2F,
0xFFC2185B,
0xFF7B1FA2,
0xFF512DA8,
0xFF303F9F,
0xFF1976D2,
0xFF0288D1,
0xFF0097A7,
0xFF00796B,
0xFF388E3C,
0xFF689F38,
0xFFAFB42B,
0xFFFBC02D,
0xFFFFA000,
0xFFF57C00,
0xFFE64A19,
0xFF5D4037,
0xFF616161,
0xFF455A64
));
private TextDrawableHelper() {
}
public static TextDrawable generate(String text, String fallback, View view) {
if (text == null || text.isEmpty()) {
if (fallback == null || fallback.isEmpty()) {
return null;
}
text = fallback;
}
int color = _generator.getColor(text);
return TextDrawable.builder().beginConfig()
.width(view.getLayoutParams().width)
.height(view.getLayoutParams().height)
.endConfig()
.buildRound(getFirstGrapheme(text).toUpperCase(), color);
}
private static String getFirstGrapheme(String text) {
BreakIterator iter = BreakIterator.getCharacterInstance();
iter.setText(text);
int start = iter.first(), end = iter.next();
if (end == BreakIterator.DONE) {
return "";
}
return text.substring(start, end);
}
}

View file

@ -0,0 +1,58 @@
package com.beemdevelopment.aegis.helpers;
import android.content.res.Configuration;
import androidx.appcompat.app.AppCompatActivity;
import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.Theme;
import com.google.android.material.color.DynamicColors;
import com.google.android.material.color.DynamicColorsOptions;
import java.util.Map;
public class ThemeHelper {
private final AppCompatActivity _activity;
private final Preferences _prefs;
public ThemeHelper(AppCompatActivity activity, Preferences prefs) {
_activity = activity;
_prefs = prefs;
}
/**
* Sets the theme of the activity. The actual style that is set is picked from the
* given map, based on the theme configured by the user.
*/
public void setTheme(Map<Theme, Integer> themeMap) {
int theme = themeMap.get(getConfiguredTheme());
_activity.setTheme(theme);
if (_prefs.isDynamicColorsEnabled()) {
DynamicColorsOptions.Builder optsBuilder = new DynamicColorsOptions.Builder();
if (getConfiguredTheme().equals(Theme.AMOLED)) {
optsBuilder.setThemeOverlay(R.style.ThemeOverlay_Aegis_Dynamic_Amoled);
} else if (getConfiguredTheme().equals(Theme.DARK)) {
optsBuilder.setThemeOverlay(R.style.ThemeOverlay_Aegis_Dynamic_Dark);
}
DynamicColors.applyToActivityIfAvailable(_activity, optsBuilder.build());
}
}
public Theme getConfiguredTheme() {
Theme theme = _prefs.getCurrentTheme();
if (theme == Theme.SYSTEM || theme == Theme.SYSTEM_AMOLED) {
int currentNightMode = _activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
if (currentNightMode == Configuration.UI_MODE_NIGHT_YES) {
theme = theme == Theme.SYSTEM_AMOLED ? Theme.AMOLED : Theme.DARK;
} else {
theme = Theme.LIGHT;
}
}
return theme;
}
}

View file

@ -0,0 +1,69 @@
package com.beemdevelopment.aegis.helpers;
import android.os.Handler;
import com.beemdevelopment.aegis.VibrationPatterns;
public class UiRefresher {
private boolean _running;
private Listener _listener;
private Handler _handler;
public UiRefresher(Listener listener) {
_listener = listener;
_handler = new Handler();
}
public void destroy() {
stop();
_listener = null;
}
public void start() {
if (_running) {
return;
}
_running = true;
_handler.postDelayed(new Runnable() {
@Override
public void run() {
_listener.onRefresh();
_handler.postDelayed(this, _listener.getMillisTillNextRefresh());
}
}, _listener.getMillisTillNextRefresh());
_handler.postDelayed(new Runnable() {
@Override
public void run() {
_listener.onExpiring();
_handler.postDelayed(this, getNextRun());
}
}, getInitialRun());
}
private long getInitialRun() {
long sum = _listener.getMillisTillNextRefresh() - VibrationPatterns.getLengthInMillis(VibrationPatterns.EXPIRING);
if (sum < 0) {
return getNextRun();
}
return sum;
}
private long getNextRun() {
return (_listener.getMillisTillNextRefresh() + _listener.getPeriodMillis()) - VibrationPatterns.getLengthInMillis(VibrationPatterns.EXPIRING);
}
public void stop() {
_handler.removeCallbacksAndMessages(null);
_running = false;
}
public interface Listener {
void onRefresh();
void onExpiring();
long getMillisTillNextRefresh();
long getPeriodMillis();
}
}

View file

@ -0,0 +1,17 @@
package com.beemdevelopment.aegis.helpers;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.NonNull;
import java.util.concurrent.Executor;
public class UiThreadExecutor implements Executor {
private final Handler _handler = new Handler(Looper.getMainLooper());
@Override
public void execute(@NonNull Runnable command) {
_handler.post(command);
}
}

View file

@ -0,0 +1,44 @@
package com.beemdevelopment.aegis.helpers;
import android.content.Context;
import android.os.Build;
import android.os.VibrationEffect;
import android.os.Vibrator;
import android.os.VibratorManager;
import com.beemdevelopment.aegis.Preferences;
public class VibrationHelper {
private Preferences _preferences;
public VibrationHelper(Context context) {
_preferences = new Preferences(context);
}
public void vibratePattern(Context context, long[] pattern) {
if (!isHapticFeedbackEnabled()) {
return;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
VibratorManager vibratorManager = (VibratorManager) context.getSystemService(Context.VIBRATOR_MANAGER_SERVICE);
if (vibratorManager != null) {
Vibrator vibrator = vibratorManager.getDefaultVibrator();
VibrationEffect effect = VibrationEffect.createWaveform(pattern, -1);
vibrator.vibrate(effect);
}
} else {
Vibrator vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
if (vibrator != null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
VibrationEffect effect = VibrationEffect.createWaveform(pattern, -1);
vibrator.vibrate(effect);
}
}
}
}
public boolean isHapticFeedbackEnabled() {
return _preferences.isHapticFeedbackEnabled();
}
}

View file

@ -0,0 +1,26 @@
package com.beemdevelopment.aegis.helpers;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import com.google.android.material.appbar.AppBarLayout;
public class ViewHelper {
private ViewHelper() {
}
public static void setupAppBarInsets(AppBarLayout appBar) {
ViewCompat.setOnApplyWindowInsetsListener(appBar, (targetView, windowInsets) -> {
Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
targetView.setPadding(
insets.left,
insets.top,
insets.right,
0
);
return WindowInsetsCompat.CONSUMED;
});
}
}

View file

@ -0,0 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class AccountNameComparator implements Comparator<VaultEntry> {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return a.getName().compareToIgnoreCase(b.getName());
}
}

View file

@ -0,0 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class FavoriteComparator implements Comparator<VaultEntry> {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return -1 * Boolean.compare(a.isFavorite(), b.isFavorite());
}
}

View file

@ -0,0 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class IssuerNameComparator implements Comparator<VaultEntry> {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return a.getIssuer().compareToIgnoreCase(b.getIssuer());
}
}

View file

@ -0,0 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class LastUsedComparator implements Comparator<VaultEntry> {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return Long.compare(a.getLastUsedTimestamp(), b.getLastUsedTimestamp());
}
}

View file

@ -0,0 +1,12 @@
package com.beemdevelopment.aegis.helpers.comparators;
import com.beemdevelopment.aegis.vault.VaultEntry;
import java.util.Comparator;
public class UsageCountComparator implements Comparator<VaultEntry> {
@Override
public int compare(VaultEntry a, VaultEntry b) {
return Integer.compare(a.getUsageCount(), b.getUsageCount());
}
}

View file

@ -0,0 +1,216 @@
package com.beemdevelopment.aegis.icons;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.google.common.base.Objects;
import com.google.common.io.Files;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
public class IconPack {
private UUID _uuid;
private String _name;
private int _version;
private List<Icon> _icons;
private File _dir;
private IconPack(UUID uuid, String name, int version, List<Icon> icons) {
_uuid = uuid;
_name = name;
_version = version;
_icons = icons;
}
public UUID getUUID() {
return _uuid;
}
public String getName() {
return _name;
}
public int getVersion() {
return _version;
}
public List<Icon> getIcons() {
return Collections.unmodifiableList(_icons);
}
/**
* Retrieves a list of icons suggested for the given issuer.
*/
public List<Icon> getSuggestedIcons(String issuer) {
if (issuer == null || issuer.isEmpty()) {
return new ArrayList<>();
}
List<Icon> icons = new ArrayList<>();
for (Icon icon : _icons) {
MatchType matchType = icon.getMatchFor(issuer);
if (matchType != null) {
// Inverse matches (entry issuer contains icon name) are less likely
// to be good, so position them at the end of the list.
if (matchType.equals(MatchType.NORMAL)) {
icons.add(0, icon);
} else if (matchType.equals(MatchType.INVERSE)) {
icons.add(icon);
}
}
}
return icons;
}
@Nullable
public File getDirectory() {
return _dir;
}
void setDirectory(@NonNull File dir) {
_dir = dir;
}
/**
* Indicates whether some other object is "equal to" this one. The object does not
* necessarily have to be the same instance. Equality of UUID and version will make
* this method return true;
*/
@Override
public boolean equals(Object o) {
if (!(o instanceof IconPack)) {
return false;
}
IconPack pack = (IconPack) o;
return super.equals(pack) || (getUUID().equals(pack.getUUID()) && getVersion() == pack.getVersion());
}
@Override
public int hashCode() {
return Objects.hashCode(_uuid, _version);
}
public static IconPack fromJson(JSONObject obj) throws JSONException {
UUID uuid;
String uuidString = obj.getString("uuid");
try {
uuid = UUID.fromString(uuidString);
} catch (IllegalArgumentException e) {
throw new JSONException(String.format("Bad UUID format: %s", uuidString));
}
String name = obj.getString("name");
int version = obj.getInt("version");
JSONArray array = obj.getJSONArray("icons");
List<Icon> icons = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
Icon icon = Icon.fromJson(array.getJSONObject(i));
icons.add(icon);
}
return new IconPack(uuid, name, version, icons);
}
public static IconPack fromBytes(byte[] data) throws JSONException {
JSONObject obj = new JSONObject(new String(data, StandardCharsets.UTF_8));
return IconPack.fromJson(obj);
}
public static class Icon implements Serializable {
private final String _relFilename;
private final String _name;
private final String _category;
private final List<String> _issuers;
private File _file;
protected Icon(String filename, String name, String category, List<String> issuers) {
_relFilename = filename;
_name = name;
_category = category;
_issuers = issuers;
}
public String getRelativeFilename() {
return _relFilename;
}
@Nullable
public File getFile() {
return _file;
}
void setFile(@NonNull File file) {
_file = file;
}
public IconType getIconType() {
return IconType.fromFilename(_relFilename);
}
public String getName() {
if (_name != null) {
return _name;
}
return Files.getNameWithoutExtension(new File(_relFilename).getName());
}
public String getCategory() {
return _category;
}
private MatchType getMatchFor(String issuer) {
String lowerEntryIssuer = issuer.toLowerCase();
boolean inverseMatch = false;
for (String is : _issuers) {
String lowerIconIssuer = is.toLowerCase();
if (lowerIconIssuer.contains(lowerEntryIssuer)) {
return MatchType.NORMAL;
}
if (lowerEntryIssuer.contains(lowerIconIssuer)) {
inverseMatch = true;
}
}
if (inverseMatch) {
return MatchType.INVERSE;
}
return null;
}
public static Icon fromJson(JSONObject obj) throws JSONException {
String filename = obj.getString("filename");
String name = JsonUtils.optString(obj, "name");
String category = obj.isNull("category") ? null : obj.getString("category");
JSONArray array = obj.getJSONArray("issuer");
List<String> issuers = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
String issuer = array.getString(i);
issuers.add(issuer);
}
return new Icon(filename, name, category, issuers);
}
}
private enum MatchType {
NORMAL,
INVERSE
}
}

View file

@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.icons;
public class IconPackException extends Exception {
public IconPackException(Throwable cause) {
super(cause);
}
public IconPackException(String message) {
super(message);
}
}

View file

@ -0,0 +1,14 @@
package com.beemdevelopment.aegis.icons;
public class IconPackExistsException extends IconPackException {
private IconPack _pack;
public IconPackExistsException(IconPack pack) {
super(String.format("Icon pack %s (%d) already exists", pack.getName(), pack.getVersion()));
_pack = pack;
}
public IconPack getIconPack() {
return _pack;
}
}

View file

@ -0,0 +1,223 @@
package com.beemdevelopment.aegis.icons;
import android.content.Context;
import androidx.annotation.Nullable;
import com.beemdevelopment.aegis.util.IOUtils;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.FileHeader;
import org.json.JSONException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
public class IconPackManager {
private static final String _packDefFilename = "pack.json";
private File _iconsBaseDir;
private List<IconPack> _iconPacks;
public IconPackManager(Context context) {
_iconPacks = new ArrayList<>();
_iconsBaseDir = new File(context.getFilesDir(), "icons");
rescanIconPacks();
}
private IconPack getIconPackByUUID(UUID uuid) {
List<IconPack> packs = _iconPacks.stream().filter(i -> i.getUUID().equals(uuid)).collect(Collectors.toList());
if (packs.size() == 0) {
return null;
}
return packs.get(0);
}
public boolean hasIconPack() {
return _iconPacks.size() > 0;
}
public List<IconPack> getIconPacks() {
return new ArrayList<>(_iconPacks);
}
public void removeIconPack(IconPack pack) throws IconPackException {
try {
File dir = getIconPackDir(pack);
deleteDir(dir);
} catch (IOException e) {
throw new IconPackException(e);
}
_iconPacks.remove(pack);
}
public IconPack importPack(File inFile) throws IconPackException {
try {
// read and parse the icon pack definition file of the icon pack
ZipFile zipFile = new ZipFile(inFile);
FileHeader packHeader = zipFile.getFileHeader(_packDefFilename);
if (packHeader == null) {
throw new IOException("Unable to find pack.json in the root of the ZIP file");
}
IconPack pack;
byte[] defBytes;
try (ZipInputStream inStream = zipFile.getInputStream(packHeader)) {
defBytes = IOUtils.readAll(inStream);
pack = IconPack.fromBytes(defBytes);
}
// create a new directory to store the icon pack, based on the UUID and version
File packDir = getIconPackDir(pack);
if (!packDir.getCanonicalPath().startsWith(_iconsBaseDir.getCanonicalPath() + File.separator)) {
throw new IOException("Attempted to write outside of the parent directory");
}
if (packDir.exists()) {
throw new IconPackExistsException(pack);
}
IconPack existingPack = getIconPackByUUID(pack.getUUID());
if (existingPack != null) {
throw new IconPackExistsException(existingPack);
}
if (!packDir.exists() && !packDir.mkdirs()) {
throw new IOException(String.format("Unable to create directories: %s", packDir.toString()));
}
// extract each of the defined icons to the icon pack directory
for (IconPack.Icon icon : pack.getIcons()) {
File destFile = new File(packDir, icon.getRelativeFilename());
FileHeader iconHeader = zipFile.getFileHeader(icon.getRelativeFilename());
if (iconHeader == null) {
throw new IOException(String.format("Unable to find %s relative to the root of the ZIP file", icon.getRelativeFilename()));
}
// create new directories for this file if needed
File parent = destFile.getParentFile();
if (parent != null && !parent.exists() && !parent.mkdirs()) {
throw new IOException(String.format("Unable to create directories: %s", packDir.toString()));
}
try (ZipInputStream inStream = zipFile.getInputStream(iconHeader);
FileOutputStream outStream = new FileOutputStream(destFile)) {
IOUtils.copy(inStream, outStream);
}
// after successful copy of the icon, store the new filename
icon.setFile(destFile);
}
// write the icon pack definition file to the newly created directory
try (FileOutputStream outStream = new FileOutputStream(new File(packDir, _packDefFilename))) {
outStream.write(defBytes);
}
// after successful extraction of the icon pack, store the new directory
pack.setDirectory(packDir);
_iconPacks.add(pack);
return pack;
} catch (IOException | JSONException e) {
throw new IconPackException(e);
}
}
private void rescanIconPacks() {
_iconPacks.clear();
File[] dirs = _iconsBaseDir.listFiles();
if (dirs == null) {
return;
}
for (File dir : dirs) {
if (!dir.isDirectory()) {
continue;
}
UUID uuid;
try {
uuid = UUID.fromString(dir.getName());
} catch (IllegalArgumentException e) {
e.printStackTrace();
continue;
}
File versionDir = getLatestVersionDir(dir);
if (versionDir != null) {
IconPack pack;
try (FileInputStream inStream = new FileInputStream(new File(versionDir, _packDefFilename))) {
byte[] bytes = IOUtils.readAll(inStream);
pack = IconPack.fromBytes(bytes);
pack.setDirectory(versionDir);
} catch (JSONException | IOException e) {
e.printStackTrace();
continue;
}
for (IconPack.Icon icon : pack.getIcons()) {
icon.setFile(new File(versionDir, icon.getRelativeFilename()));
}
// do a sanity check on the UUID and version
if (pack.getUUID().equals(uuid) && pack.getVersion() == Integer.parseInt(versionDir.getName())) {
_iconPacks.add(pack);
}
}
}
}
private File getIconPackDir(IconPack pack) {
return new File(_iconsBaseDir, pack.getUUID() + File.separator + pack.getVersion());
}
@Nullable
private static File getLatestVersionDir(File packDir) {
File[] dirs = packDir.listFiles();
if (dirs == null) {
return null;
}
int latestVersion = -1;
for (File versionDir : dirs) {
int version;
try {
version = Integer.parseInt(versionDir.getName());
} catch (NumberFormatException ignored) {
continue;
}
if (latestVersion == -1 || version > latestVersion) {
latestVersion = version;
}
}
if (latestVersion == -1) {
return null;
}
return new File(packDir, Integer.toString(latestVersion));
}
private static void deleteDir(File dir) throws IOException {
if (dir.isDirectory()) {
File[] children = dir.listFiles();
if (children != null) {
for (File child : children) {
deleteDir(child);
}
}
}
if (!dir.delete()) {
throw new IOException(String.format("Unable to delete directory: %s", dir));
}
}
}

View file

@ -0,0 +1,53 @@
package com.beemdevelopment.aegis.icons;
import com.google.common.io.Files;
import java.util.Locale;
public enum IconType {
INVALID,
SVG,
PNG,
JPEG;
public static IconType fromMimeType(String mimeType) {
switch (mimeType) {
case "image/svg+xml":
return SVG;
case "image/png":
return PNG;
case "image/jpeg":
return JPEG;
default:
return INVALID;
}
}
public static IconType fromFilename(String filename) {
switch (Files.getFileExtension(filename).toLowerCase(Locale.ROOT)) {
case "svg":
return SVG;
case "png":
return PNG;
case "jpg":
// intentional fallthrough
case "jpeg":
return JPEG;
default:
return INVALID;
}
}
public String toMimeType() {
switch (this) {
case SVG:
return "image/svg+xml";
case PNG:
return "image/png";
case JPEG:
return "image/jpeg";
default:
throw new RuntimeException(String.format("Can't convert icon type %s to MIME type", this));
}
}
}

View file

@ -0,0 +1,190 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.DialogInterface;
import androidx.annotation.Nullable;
import androidx.lifecycle.Lifecycle;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.ContextHelper;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.PasswordSlotDecryptTask;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultEntryException;
import com.beemdevelopment.aegis.vault.VaultFile;
import com.beemdevelopment.aegis.vault.VaultFileCredentials;
import com.beemdevelopment.aegis.vault.VaultFileException;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.SlotList;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.UUID;
public class AegisImporter extends DatabaseImporter {
public AegisImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
byte[] bytes = IOUtils.readAll(stream);
VaultFile file = VaultFile.fromBytes(bytes);
if (file.isEncrypted()) {
return new EncryptedState(file);
}
return new DecryptedState(file.getContent());
} catch (VaultFileException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class EncryptedState extends State {
private VaultFile _file;
private EncryptedState(VaultFile file) {
super(true);
_file = file;
}
public SlotList getSlots() {
return _file.getHeader().getSlots();
}
public State decrypt(VaultFileCredentials creds) throws DatabaseImporterException {
JSONObject obj;
try {
obj = _file.getContent(creds);
} catch (VaultFileException e) {
throw new DatabaseImporterException(e);
}
return new DecryptedState(obj, creds);
}
public State decrypt(char[] password) throws DatabaseImporterException {
List<PasswordSlot> slots = getSlots().findAll(PasswordSlot.class);
PasswordSlotDecryptTask.Result result = PasswordSlotDecryptTask.decrypt(slots, password);
VaultFileCredentials creds = new VaultFileCredentials(result.getKey(), getSlots());
return decrypt(creds);
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, (Dialogs.TextInputListener) password -> {
List<PasswordSlot> slots = getSlots().findAll(PasswordSlot.class);
PasswordSlotDecryptTask.Params params = new PasswordSlotDecryptTask.Params(slots, password);
PasswordSlotDecryptTask task = new PasswordSlotDecryptTask(context, result -> {
try {
if (result == null) {
throw new DatabaseImporterException("Password incorrect");
}
VaultFileCredentials creds = new VaultFileCredentials(result.getKey(), getSlots());
State state = decrypt(creds);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}, (DialogInterface.OnCancelListener) dialog -> listener.onCanceled());
}
}
public static class DecryptedState extends State {
private JSONObject _obj;
private VaultFileCredentials _creds;
private DecryptedState(JSONObject obj) {
this(obj, null);
}
private DecryptedState(JSONObject obj, VaultFileCredentials creds) {
super(false);
_obj = obj;
_creds = creds;
}
@Nullable
public VaultFileCredentials getCredentials() {
return _creds;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = new Result();
try {
if (_obj.has("groups")) {
JSONArray groupArray = _obj.getJSONArray("groups");
for (int i = 0; i < groupArray.length(); i++) {
JSONObject groupObj = groupArray.getJSONObject(i);
try {
VaultGroup group = convertGroup(groupObj);
if (!result.getGroups().has(group)) {
result.addGroup(group);
}
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
}
JSONArray entryArray = _obj.getJSONArray("entries");
for (int i = 0; i < entryArray.length(); i++) {
JSONObject entryObj = entryArray.getJSONObject(i);
try {
VaultEntry entry = convertEntry(entryObj);
for (UUID groupUuid : entry.getGroups()) {
if (!result.getGroups().has(groupUuid)) {
entry.getGroups().remove(groupUuid);
}
}
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return result;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
return VaultEntry.fromJson(obj);
} catch (VaultEntryException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
private static VaultGroup convertGroup(JSONObject obj) throws DatabaseImporterEntryException {
try {
return VaultGroup.fromJson(obj);
} catch (VaultEntryException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}

View file

@ -0,0 +1,275 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import androidx.appcompat.app.AlertDialog;
import androidx.lifecycle.Lifecycle;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptParameters;
import com.beemdevelopment.aegis.crypto.CryptResult;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.helpers.ContextHelper;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Locale;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
public class AndOtpImporter extends DatabaseImporter {
private static final int INT_SIZE = 4;
private static final int NONCE_SIZE = 12;
private static final int TAG_SIZE = 16;
private static final int SALT_SIZE = 12;
private static final int KEY_SIZE = 256; // bits
public AndOtpImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
byte[] bytes;
try {
bytes = IOUtils.readAll(stream);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
try {
return read(bytes);
} catch (JSONException e) {
// andOTP doesn't have a proper way to indicate whether a file is encrypted
// so, if we can't parse it as JSON, we'll have to assume it is
return new EncryptedState(bytes);
}
}
private static DecryptedState read(byte[] bytes) throws JSONException {
JSONArray array = new JSONArray(new String(bytes, StandardCharsets.UTF_8));
return new DecryptedState(array);
}
public static class EncryptedState extends DatabaseImporter.State {
private byte[] _data;
public EncryptedState(byte[] data) {
super(true);
_data = data;
}
private DecryptedState decryptContent(SecretKey key, int offset) throws DatabaseImporterException {
byte[] nonce = Arrays.copyOfRange(_data, offset, offset + NONCE_SIZE);
byte[] tag = Arrays.copyOfRange(_data, _data.length - TAG_SIZE, _data.length);
CryptParameters params = new CryptParameters(nonce, tag);
try {
Cipher cipher = CryptoUtils.createDecryptCipher(key, nonce);
int len = _data.length - offset - NONCE_SIZE - TAG_SIZE;
CryptResult result = CryptoUtils.decrypt(_data, offset + NONCE_SIZE, len, cipher, params);
return read(result.getData());
} catch (IOException | BadPaddingException | JSONException e) {
throw new DatabaseImporterException(e);
} catch (NoSuchAlgorithmException
| InvalidAlgorithmParameterException
| InvalidKeyException
| NoSuchPaddingException
| IllegalBlockSizeException e) {
throw new RuntimeException(e);
}
}
private PBKDFTask.Params getKeyDerivationParams(char[] password) throws DatabaseImporterException {
byte[] iterBytes = Arrays.copyOfRange(_data, 0, INT_SIZE);
int iterations = ByteBuffer.wrap(iterBytes).getInt();
if (iterations < 1) {
throw new DatabaseImporterException(String.format("Invalid number of iterations for PBKDF: %d", iterations));
}
// If number of iterations is this high, it's probably not an andOTP file, so
// abort early in order to prevent having to wait for an extremely long key derivation
// process, only to find out that the user picked the wrong file
if (iterations > 10_000_000L) {
throw new DatabaseImporterException(String.format("Unexpectedly high number of iterations: %d", iterations));
}
byte[] salt = Arrays.copyOfRange(_data, INT_SIZE, INT_SIZE + SALT_SIZE);
return new PBKDFTask.Params("PBKDF2WithHmacSHA1", KEY_SIZE, password, salt, iterations);
}
protected DecryptedState decryptOldFormat(char[] password) throws DatabaseImporterException {
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this exists solely to support the old andOTP backup format
// it is not a secure way to derive a key from a password
MessageDigest hash;
try {
hash = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
SecretKey key = new SecretKeySpec(keyBytes, "AES");
return decryptContent(key, 0);
}
protected DecryptedState decryptNewFormat(SecretKey key) throws DatabaseImporterException {
return decryptContent(key, INT_SIZE + SALT_SIZE);
}
protected DecryptedState decryptNewFormat(char[] password)
throws DatabaseImporterException {
PBKDFTask.Params params = getKeyDerivationParams(password);
SecretKey key = PBKDFTask.deriveKey(params);
return decryptNewFormat(key);
}
private void decrypt(Context context, char[] password, boolean oldFormat, DecryptListener listener) throws DatabaseImporterException {
if (oldFormat) {
DecryptedState state = decryptOldFormat(password);
listener.onStateDecrypted(state);
} else {
PBKDFTask.Params params = getKeyDerivationParams(password);
PBKDFTask task = new PBKDFTask(context, key -> {
try {
DecryptedState state = decryptNewFormat(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
String[] choices = new String[]{
context.getResources().getString(R.string.andotp_new_format),
context.getResources().getString(R.string.andotp_old_format)
};
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context)
.setTitle(R.string.choose_andotp_importer)
.setSingleChoiceItems(choices, 0, null)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
Dialogs.showPasswordInputDialog(context, password -> {
try {
decrypt(context, password, i != 0, listener);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
}, dialog1 -> listener.onCanceled());
})
.create());
}
}
public static class DecryptedState extends DatabaseImporter.State {
private JSONArray _obj;
private DecryptedState(JSONArray obj) {
super(false);
_obj = obj;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = new Result();
for (int i = 0; i < _obj.length(); i++) {
try {
JSONObject obj = _obj.getJSONObject(i);
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (JSONException e) {
throw new DatabaseImporterException(e);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
String type = obj.getString("type").toLowerCase(Locale.ROOT);
String algo = obj.getString("algorithm");
int digits = obj.getInt("digits");
byte[] secret = Base32.decode(obj.getString("secret"));
OtpInfo info;
switch (type) {
case "hotp":
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
break;
case "totp":
info = new TotpInfo(secret, algo, digits, obj.getInt("period"));
break;
case "steam":
info = new SteamInfo(secret, algo, digits, obj.optInt("period", TotpInfo.DEFAULT_PERIOD));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
}
String name;
String issuer = "";
if (obj.has("issuer")) {
name = obj.getString("label");
issuer = obj.getString("issuer");
} else {
String[] parts = obj.getString("label").split(" - ");
if (parts.length > 1) {
issuer = parts[0];
name = parts[1];
} else {
name = parts[0];
}
}
return new VaultEntry(info, name, issuer);
} catch (DatabaseImporterException | EncodingException | OtpInfoException |
JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}

View file

@ -0,0 +1,77 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.util.IOUtils;
import com.topjohnwu.superuser.io.SuFile;
import net.lingala.zip4j.io.inputstream.ZipInputStream;
import net.lingala.zip4j.model.LocalFileHeader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class AuthenticatorPlusImporter extends DatabaseImporter {
private static final String FILENAME = "Accounts.txt";
public AuthenticatorPlusImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
return new EncryptedState(IOUtils.readAll(stream));
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class EncryptedState extends DatabaseImporter.State {
private final byte[] _data;
private EncryptedState(byte[] data) {
super(true);
_data = data;
}
protected State decrypt(char[] password) throws DatabaseImporterException {
try (ByteArrayInputStream inStream = new ByteArrayInputStream(_data);
ZipInputStream zipStream = new ZipInputStream(inStream, password)) {
LocalFileHeader header;
while ((header = zipStream.getNextEntry()) != null) {
File file = new File(header.getFileName());
if (file.getName().equals(FILENAME)) {
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(null);
return importer.read(zipStream);
}
}
throw new FileNotFoundException(FILENAME);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, password -> {
try {
DatabaseImporter.State state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
}, dialog1 -> listener.onCanceled());
}
}
}

View file

@ -0,0 +1,308 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Xml;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class AuthyImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs";
private static final String _pkgName = "com.authy.authy";
private static final String _authFilename = "com.authy.storage.tokens.authenticator";
private static final String _authyFilename = "com.authy.storage.tokens.authy";
private static final int ITERATIONS = 1000;
private static final int KEY_SIZE = 256;
private static final byte[] IV = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
public AuthyImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
public State readFromApp(Shell shell) throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile path = getAppPath();
path.setShell(shell);
JSONArray array;
JSONArray authyArray;
try {
SuFile file1 = new SuFile(path, String.format("%s.xml", _authFilename));
file1.setShell(shell);
SuFile file2 = new SuFile(path, String.format("%s.xml", _authyFilename));
file2.setShell(shell);
array = readFile(file1, String.format("%s.key", _authFilename));
authyArray = readFile(file2, String.format("%s.key", _authyFilename));
} catch (IOException | XmlPullParserException e) {
throw new DatabaseImporterException(e);
}
try {
for (int i = 0; i < authyArray.length(); i++) {
array.put(authyArray.getJSONObject(i));
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return read(array);
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(stream, null);
parser.nextTag();
JSONArray array = new JSONArray();
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals(String.format("%s.key", _authFilename))
|| entry.Name.equals(String.format("%s.key", _authyFilename))) {
array = new JSONArray(entry.Value);
break;
}
}
return read(array);
} catch (XmlPullParserException | JSONException | IOException e) {
throw new DatabaseImporterException(e);
}
}
private State read(JSONArray array) throws DatabaseImporterException {
try {
for (int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
if (!obj.has("decryptedSecret") && !obj.has("secretSeed")) {
return new EncryptedState(array);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return new DecryptedState(array);
}
private JSONArray readFile(SuFile file, String key) throws IOException, XmlPullParserException {
try (InputStream inStream = SuFileInputStream.open(file)) {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(inStream, null);
parser.nextTag();
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals(key)) {
return new JSONArray(entry.Value);
}
}
} catch (JSONException ignored) {
}
return new JSONArray();
}
public static class EncryptedState extends DatabaseImporter.State {
private JSONArray _array;
private EncryptedState(JSONArray array) {
super(true);
_array = array;
}
protected DecryptedState decrypt(char[] password) throws DatabaseImporterException {
try {
for (int i = 0; i < _array.length(); i++) {
JSONObject obj = _array.getJSONObject(i);
String secretString = JsonUtils.optString(obj, "encryptedSecret");
if (secretString == null) {
continue;
}
byte[] encryptedSecret = Base64.decode(secretString);
byte[] salt = obj.getString("salt").getBytes(StandardCharsets.UTF_8);
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_SIZE);
SecretKey key = factory.generateSecret(spec);
key = new SecretKeySpec(key.getEncoded(), "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec ivSpec = new IvParameterSpec(IV);
cipher.init(Cipher.DECRYPT_MODE, key, ivSpec);
byte[] secret = cipher.doFinal(encryptedSecret);
obj.remove("encryptedSecret");
obj.remove("salt");
obj.put("decryptedSecret", new String(secret, StandardCharsets.UTF_8));
}
return new DecryptedState(_array);
} catch (JSONException
| EncodingException
| NoSuchAlgorithmException
| InvalidKeySpecException
| InvalidAlgorithmParameterException
| InvalidKeyException
| NoSuchPaddingException
| BadPaddingException
| IllegalBlockSizeException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_authy_message, password -> {
try {
DecryptedState state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
}, dialog1 -> listener.onCanceled());
}
}
public static class DecryptedState extends DatabaseImporter.State {
private JSONArray _array;
private DecryptedState(JSONArray array) {
super(false);
_array = array;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = new Result();
try {
for (int i = 0; i < _array.length(); i++) {
JSONObject entryObj = _array.getJSONObject(i);
try {
VaultEntry entry = convertEntry(entryObj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return result;
}
private static VaultEntry convertEntry(JSONObject entry) throws DatabaseImporterEntryException {
try {
AuthyEntryInfo authyEntryInfo = new AuthyEntryInfo();
authyEntryInfo.OriginalName = JsonUtils.optString(entry, "originalName");
authyEntryInfo.OriginalIssuer = JsonUtils.optString(entry, "originalIssuer");
authyEntryInfo.AccountType = JsonUtils.optString(entry, "accountType");
authyEntryInfo.Name = entry.optString("name");
boolean isAuthy = entry.has("secretSeed");
sanitizeEntryInfo(authyEntryInfo, isAuthy);
byte[] secret;
if (isAuthy) {
secret = Hex.decode(entry.getString("secretSeed"));
} else {
secret = Base32.decode(entry.getString("decryptedSecret"));
}
int digits = entry.getInt("digits");
OtpInfo info = new TotpInfo(secret, OtpInfo.DEFAULT_ALGORITHM, digits, isAuthy ? 10 : TotpInfo.DEFAULT_PERIOD);
return new VaultEntry(info, authyEntryInfo.Name, authyEntryInfo.Issuer);
} catch (OtpInfoException | JSONException | EncodingException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
private static void sanitizeEntryInfo(AuthyEntryInfo info, boolean isAuthy) {
if (!isAuthy) {
String separator = "";
if (info.OriginalIssuer != null) {
info.Issuer = info.OriginalIssuer;
} else if (info.OriginalName != null && info.OriginalName.contains(":")) {
info.Issuer = info.OriginalName.substring(0, info.OriginalName.indexOf(":"));
separator = ":";
} else if (info.Name.contains(" - ")) {
info.Issuer = info.Name.substring(0, info.Name.indexOf(" - "));
separator = " - ";
} else {
info.Issuer = info.AccountType.substring(0, 1).toUpperCase() + info.AccountType.substring(1);
}
info.Name = info.Name.replace(info.Issuer + separator, "");
} else {
info.Issuer = info.Name;
info.Name = "";
}
if (info.Name.startsWith(": ")) {
info.Name = info.Name.substring(2);
}
}
}
private static class AuthyEntryInfo {
String OriginalName;
String OriginalIssuer;
String AccountType;
String Issuer;
String Name;
}
}

View file

@ -0,0 +1,124 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Xml;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.common.base.Strings;
import com.topjohnwu.superuser.io.SuFile;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
public class BattleNetImporter extends DatabaseImporter {
private static final String _pkgName = "com.blizzard.messenger";
private static final String _subPath = "shared_prefs/com.blizzard.messenger.authenticator_preferences.xml";
private static final byte[] _key;
public BattleNetImporter(Context context) {
super(context);
}
static {
try {
_key = Hex.decode("398e27fc50276a656065b0e525f4c06c04c61075286b8e7aeda59da9813b5dd6c80d2fb38068773fa59ba47c17ca6c6479015c1d5b8b8f6b9a");
} catch (EncodingException e) {
throw new RuntimeException(e);
}
}
@Override
protected SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
final String serialKey = "com.blizzard.messenger.AUTHENTICATOR_SERIAL";
final String secretKey = "com.blizzard.messenger.AUTHENTICATOR_DEVICE_SECRET";
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(stream, null);
parser.nextTag();
String serial = "";
String secretValue = null;
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals(secretKey)) {
secretValue = entry.Value;
} else if (entry.Name.equals(serialKey)) {
serial = entry.Value;
}
}
if (secretValue == null) {
throw new DatabaseImporterException(String.format("Key not found: %s", secretKey));
}
return new BattleNetImporter.State(serial, secretValue);
} catch (XmlPullParserException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private final String _serial;
private final String _secretValue;
public State(String serial, String secretValue) {
super(false);
_serial = serial;
_secretValue = secretValue;
}
@Override
public Result convert() {
Result result = new Result();
try {
VaultEntry entry = convertEntry(_serial, _secretValue);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
return result;
}
private static VaultEntry convertEntry(String serial, String secretString) throws DatabaseImporterEntryException {
try {
if (!Strings.isNullOrEmpty(serial)) {
serial = unmask(serial);
}
byte[] secret = Hex.decode(unmask(secretString));
OtpInfo info = new TotpInfo(secret, OtpInfo.DEFAULT_ALGORITHM, 8, TotpInfo.DEFAULT_PERIOD);
return new VaultEntry(info, serial, "Battle.net");
} catch (OtpInfoException | EncodingException e) {
throw new DatabaseImporterEntryException(e, secretString);
}
}
private static String unmask(String s) throws EncodingException {
byte[] ds = Hex.decode(s);
StringBuilder sb = new StringBuilder();
for (int i = 0; i < ds.length; i++) {
char c = (char) (ds[i] ^ _key[i]);
sb.append(c);
}
return sb.toString();
}
}
}

View file

@ -0,0 +1,127 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.net.Uri;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.simpleflatmapper.csv.CsvParser;
import org.simpleflatmapper.lightningcsv.Row;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
public class BitwardenImporter extends DatabaseImporter {
public BitwardenImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
String fileString;
try {
fileString = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
try {
JSONObject obj = new JSONObject(fileString);
JSONArray array = obj.getJSONArray("items");
List<String> entries = new ArrayList<>();
String entry;
for (int i = 0; i < array.length(); i++) {
entry = array.getJSONObject(i).getJSONObject("login").getString("totp");
if (!entry.isEmpty()) {
entries.add(entry);
}
}
return new BitwardenImporter.State(entries);
} catch (JSONException e) {
try {
Iterator<Row> rowIterator = CsvParser.separator(',').rowIterator(fileString);
List<String> entries = new ArrayList<>();
rowIterator.forEachRemaining((row -> {
String entry = row.get("login_totp");
if (entry != null && !entry.isEmpty()) {
entries.add(entry);
}
}));
return new BitwardenImporter.State(entries);
} catch (IOException e2) {
throw new DatabaseImporterException(e2);
}
}
}
public static class State extends DatabaseImporter.State {
private final List<String> _entries;
public State(List<String> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
for (String obj : _entries) {
try {
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(String obj) throws DatabaseImporterEntryException {
try {
GoogleAuthInfo info = BitwardenImporter.parseUri(obj);
return new VaultEntry(info);
} catch (GoogleAuthInfoException | EncodingException | OtpInfoException | URISyntaxException e) {
throw new DatabaseImporterEntryException(e, obj);
}
}
}
private static GoogleAuthInfo parseUri(String s) throws EncodingException, OtpInfoException, URISyntaxException, GoogleAuthInfoException {
Uri uri = Uri.parse(s);
if (Objects.equals(uri.getScheme(), "steam")) {
String secretString = uri.getAuthority();
if (secretString == null) {
throw new GoogleAuthInfoException(uri, "Empty secret (empty authority)");
}
byte[] secret = Base32.decode(secretString);
return new GoogleAuthInfo(new SteamInfo(secret), "Steam account", "Steam");
}
return GoogleAuthInfo.parseUri(uri);
}
}

View file

@ -0,0 +1,207 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import androidx.annotation.StringRes;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.beemdevelopment.aegis.vault.VaultGroup;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public abstract class DatabaseImporter {
private Context _context;
private static List<Definition> _importers;
static {
// note: keep these lists sorted alphabetically
_importers = new ArrayList<>();
_importers.add(new Definition("2FAS Authenticator", TwoFASImporter.class, R.string.importer_help_2fas, false));
_importers.add(new Definition("Aegis", AegisImporter.class, R.string.importer_help_aegis, false));
_importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
_importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false));
_importers.add(new Definition("Authy", AuthyImporter.class, R.string.importer_help_authy, true));
_importers.add(new Definition("Battle.net Authenticator", BattleNetImporter.class, R.string.importer_help_battle_net_authenticator, true));
_importers.add(new Definition("Bitwarden", BitwardenImporter.class, R.string.importer_help_bitwarden, false));
_importers.add(new Definition("Duo", DuoImporter.class, R.string.importer_help_duo, true));
_importers.add(new Definition("Ente Auth", EnteAuthImporter.class, R.string.importer_help_ente_auth, false));
_importers.add(new Definition("FreeOTP", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
_importers.add(new Definition("FreeOTP+ (JSON)", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
_importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true));
_importers.add(new Definition("Microsoft Authenticator", MicrosoftAuthImporter.class, R.string.importer_help_microsoft_authenticator, true));
_importers.add(new Definition("Plain text", GoogleAuthUriImporter.class, R.string.importer_help_plain_text, false));
_importers.add(new Definition("Proton Authenticator", ProtonAuthenticatorImporter.class, R.string.importer_help_proton_authenticator, false));
_importers.add(new Definition("Steam", SteamImporter.class, R.string.importer_help_steam, true));
_importers.add(new Definition("Stratum (Authenticator Pro)", StratumImporter.class, R.string.importer_help_stratum, true));
_importers.add(new Definition("TOTP Authenticator", TotpAuthenticatorImporter.class, R.string.importer_help_totp_authenticator, true));
_importers.add(new Definition("WinAuth", WinAuthImporter.class, R.string.importer_help_winauth, false));
}
public DatabaseImporter(Context context) {
_context = context;
}
protected Context requireContext() {
return _context;
}
protected abstract SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException;
protected SuFile getAppPath(String pkgName, String subPath) throws PackageManager.NameNotFoundException {
PackageManager man = requireContext().getPackageManager();
return new SuFile(man.getApplicationInfo(pkgName, 0).dataDir, subPath);
}
public boolean isInstalledAppVersionSupported() {
return true;
}
protected abstract State read(InputStream stream, boolean isInternal) throws DatabaseImporterException;
public State read(InputStream stream) throws DatabaseImporterException {
return read(stream, false);
}
public State readFromApp(Shell shell) throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile file = getAppPath();
file.setShell(shell);
try (InputStream stream = SuFileInputStream.open(file)) {
return read(stream, true);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
public static DatabaseImporter create(Context context, Class<? extends DatabaseImporter> type) {
try {
return type.getConstructor(Context.class).newInstance(context);
} catch (IllegalAccessException | InstantiationException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
public static List<Definition> getImporters(boolean isDirect) {
if (isDirect) {
return Collections.unmodifiableList(_importers.stream().filter(Definition::supportsDirect).collect(Collectors.toList()));
}
return Collections.unmodifiableList(_importers);
}
public static class Definition implements Serializable {
private final String _name;
private final Class<? extends DatabaseImporter> _type;
private final @StringRes int _help;
private final boolean _supportsDirect;
/**
*
* @param name The name of the Authenticator the importer handles.
* @param type The class which does the importing.
* @param help The string that explains the type of file needed (and optionally where it can be obtained).
* @param supportsDirect Whether the importer can directly import the entries from the app's internal storage using root access.
*/
public Definition(String name, Class<? extends DatabaseImporter> type, @StringRes int help, boolean supportsDirect) {
_name = name;
_type = type;
_help = help;
_supportsDirect = supportsDirect;
}
public String getName() {
return _name;
}
public Class<? extends DatabaseImporter> getType() {
return _type;
}
public @StringRes int getHelp() {
return _help;
}
public boolean supportsDirect() {
return _supportsDirect;
}
}
public static abstract class State {
private boolean _encrypted;
public State(boolean encrypted) {
_encrypted = encrypted;
}
public boolean isEncrypted() {
return _encrypted;
}
public void decrypt(Context context, DecryptListener listener) throws DatabaseImporterException {
if (!_encrypted) {
throw new RuntimeException("Attempted to decrypt a plain text database");
}
throw new UnsupportedOperationException();
}
public Result convert() throws DatabaseImporterException {
if (_encrypted) {
throw new RuntimeException("Attempted to convert database before decrypting it");
}
throw new UnsupportedOperationException();
}
}
public static class Result {
private UUIDMap<VaultEntry> _entries = new UUIDMap<>();
private UUIDMap<VaultGroup> _groups = new UUIDMap<>();
private List<DatabaseImporterEntryException> _errors = new ArrayList<>();
public void addEntry(VaultEntry entry) {
_entries.add(entry);
}
public void addGroup(VaultGroup group) {
_groups.add(group);
}
public void addError(DatabaseImporterEntryException error) {
_errors.add(error);
}
public UUIDMap<VaultEntry> getEntries() {
return _entries;
}
public UUIDMap<VaultGroup> getGroups() {
return _groups;
}
public List<DatabaseImporterEntryException> getErrors() {
return _errors;
}
}
public static abstract class DecryptListener {
protected abstract void onStateDecrypted(State state);
protected abstract void onError(Exception e);
protected abstract void onCanceled();
}
}

View file

@ -0,0 +1,19 @@
package com.beemdevelopment.aegis.importers;
public class DatabaseImporterEntryException extends Exception {
private String _text;
public DatabaseImporterEntryException(String message, String text) {
super(message);
_text = text;
}
public DatabaseImporterEntryException(Throwable cause, String text) {
super(cause);
_text = text;
}
public String getText() {
return _text;
}
}

View file

@ -0,0 +1,11 @@
package com.beemdevelopment.aegis.importers;
public class DatabaseImporterException extends Exception {
public DatabaseImporterException(Throwable cause) {
super(cause);
}
public DatabaseImporterException(String message) {
super(message);
}
}

View file

@ -0,0 +1,99 @@
package com.beemdevelopment.aegis.importers;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.content.Context;
import android.content.pm.PackageManager.NameNotFoundException;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
public class DuoImporter extends DatabaseImporter {
private static final String _pkgName = "com.duosecurity.duomobile";
private static final String _subPath = "files/duokit/accounts.json";
public DuoImporter(Context context) {
super(context);
}
@Override
protected @NonNull SuFile getAppPath() throws DatabaseImporterException, NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
protected @NonNull State read(
@NonNull InputStream stream, boolean isInternal
) throws DatabaseImporterException {
try {
String contents = new String(IOUtils.readAll(stream), UTF_8);
return new DecryptedState(new JSONArray(contents));
} catch (JSONException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class DecryptedState extends DatabaseImporter.State {
private final JSONArray _array;
public DecryptedState(@NonNull JSONArray array) {
super(false);
_array = array;
}
@Override
public @NonNull Result convert() throws DatabaseImporterException {
Result result = new Result();
try {
for (int i = 0; i < _array.length(); i++) {
JSONObject entry = _array.getJSONObject(i);
try {
result.addEntry(convertEntry(entry));
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return result;
}
private static @NonNull VaultEntry convertEntry(
@NonNull JSONObject entry
) throws DatabaseImporterEntryException {
try {
String label = entry.optString("name");
JSONObject otpData = entry.getJSONObject("otpGenerator");
byte[] secret = Base32.decode(otpData.getString("otpSecret"));
Long counter = otpData.has("counter") ? otpData.getLong("counter") : null;
OtpInfo otp = counter == null
? new TotpInfo(secret)
: new HotpInfo(secret, counter);
return new VaultEntry(otp, label, "");
} catch (JSONException | OtpInfoException | EncodingException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
}
}

View file

@ -0,0 +1,32 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.util.IOUtils;
import com.topjohnwu.superuser.io.SuFile;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
public class EnteAuthImporter extends DatabaseImporter {
public EnteAuthImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
byte[] bytes = IOUtils.readAll(stream);
GoogleAuthUriImporter importer = new GoogleAuthUriImporter(requireContext());
return importer.read(new ByteArrayInputStream(bytes), isInternal);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
}

View file

@ -0,0 +1,471 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Xml;
import androidx.lifecycle.Lifecycle;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.ContextHelper;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.topjohnwu.superuser.io.SuFile;
import org.bouncycastle.asn1.ASN1Encodable;
import org.bouncycastle.asn1.ASN1OctetString;
import org.bouncycastle.asn1.ASN1Primitive;
import org.bouncycastle.asn1.ASN1Sequence;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class FreeOtpImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/tokens.xml";
private static final String _pkgName = "org.fedorahosted.freeotp";
public FreeOtpImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try (BufferedInputStream bufInStream = new BufferedInputStream(stream);
DataInputStream dataInStream = new DataInputStream(bufInStream)) {
dataInStream.mark(2);
int magic = dataInStream.readUnsignedShort();
dataInStream.reset();
if (magic == SerializedHashMapParser.MAGIC) {
return readV2(dataInStream);
} else {
return readV1(bufInStream);
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
}
private DecryptedStateV1 readV1(InputStream stream) throws DatabaseImporterException {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(stream, null);
parser.nextTag();
List<JSONObject> entries = new ArrayList<>();
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (!entry.Name.equals("tokenOrder")) {
entries.add(new JSONObject(entry.Value));
}
}
return new DecryptedStateV1(entries);
} catch (XmlPullParserException | IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
private EncryptedState readV2(DataInputStream stream) throws DatabaseImporterException {
try {
Map<String, String> entries = SerializedHashMapParser.parse(stream);
JSONObject mkObj = new JSONObject(entries.get("masterKey"));
return new EncryptedState(mkObj, entries);
} catch (IOException | JSONException | SerializedHashMapParser.ParseException e) {
throw new DatabaseImporterException(e);
}
}
public static class EncryptedState extends State {
private static final int MASTER_KEY_SIZE = 32 * 8;
private final String _mkAlgo;
private final String _mkCipher;
private final byte[] _mkCipherText;
private final byte[] _mkParameters;
private final byte[] _mkToken;
private final byte[] _mkSalt;
private final int _mkIterations;
private final Map<String, String> _entries;
private EncryptedState(JSONObject mkObj, Map<String, String> entries)
throws DatabaseImporterException, JSONException {
super(true);
_mkAlgo = mkObj.getString("mAlgorithm");
if (!_mkAlgo.equals("PBKDF2withHmacSHA1") && !_mkAlgo.equals("PBKDF2withHmacSHA512")) {
throw new DatabaseImporterException(String.format("Unexpected master key KDF: %s", _mkAlgo));
}
JSONObject keyObj = mkObj.getJSONObject("mEncryptedKey");
_mkCipher = keyObj.getString("mCipher");
if (!_mkCipher.equals("AES/GCM/NoPadding")) {
throw new DatabaseImporterException(String.format("Unexpected master key cipher: %s", _mkCipher));
}
_mkCipherText = toBytes(keyObj.getJSONArray("mCipherText"));
_mkParameters = toBytes(keyObj.getJSONArray("mParameters"));
_mkToken = keyObj.getString("mToken").getBytes(StandardCharsets.UTF_8);
_mkSalt = toBytes(mkObj.getJSONArray("mSalt"));
_mkIterations = mkObj.getInt("mIterations");
_entries = entries;
}
public State decrypt(char[] password) throws DatabaseImporterException {
PBKDFTask.Params params = new PBKDFTask.Params(_mkAlgo, MASTER_KEY_SIZE, password, _mkSalt, _mkIterations);
SecretKey passKey = PBKDFTask.deriveKey(params);
return decrypt(passKey);
}
public State decrypt(SecretKey passKey) throws DatabaseImporterException {
byte[] masterKeyBytes;
try {
byte[] nonce = parseNonce(_mkParameters);
IvParameterSpec spec = new IvParameterSpec(nonce);
Cipher cipher = Cipher.getInstance(_mkCipher);
cipher.init(Cipher.DECRYPT_MODE, passKey, spec);
cipher.updateAAD(_mkToken);
masterKeyBytes = cipher.doFinal(_mkCipherText);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException |
IllegalBlockSizeException | InvalidKeyException |
InvalidAlgorithmParameterException | IOException e) {
throw new DatabaseImporterException(e);
}
SecretKey masterKey = new SecretKeySpec(masterKeyBytes, 0, masterKeyBytes.length, "AES");
return new DecryptedStateV2(_entries, masterKey);
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context, R.style.ThemeOverlay_Aegis_AlertDialog_Warning)
.setTitle(R.string.importer_warning_title_freeotp2)
.setMessage(R.string.importer_warning_message_freeotp2)
.setIconAttribute(android.R.attr.alertDialogIcon)
.setCancelable(false)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, password -> {
PBKDFTask.Params params = getKeyDerivationParams(password, _mkAlgo);
PBKDFTask task = new PBKDFTask(context, key -> {
try {
State state = decrypt(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}, dialog1 -> listener.onCanceled());
})
.create());
}
private PBKDFTask.Params getKeyDerivationParams(char[] password, String algo) {
return new PBKDFTask.Params(algo, MASTER_KEY_SIZE, password, _mkSalt, _mkIterations);
}
}
public static class DecryptedStateV2 extends DatabaseImporter.State {
private final Map<String, String> _entries;
private final SecretKey _masterKey;
public DecryptedStateV2(Map<String, String> entries, SecretKey masterKey) {
super(false);
_entries = entries;
_masterKey = masterKey;
}
@Override
public Result convert() throws DatabaseImporterException {
Result result = new Result();
for (Map.Entry<String, String> entry : _entries.entrySet()) {
if (entry.getKey().endsWith("-token") || entry.getKey().equals("masterKey")) {
continue;
}
try {
JSONObject encObj = new JSONObject(entry.getValue());
String tokenKey = String.format("%s-token", entry.getKey());
JSONObject tokenObj = new JSONObject(_entries.get(tokenKey));
VaultEntry vaultEntry = convertEntry(encObj, tokenObj);
result.addEntry(vaultEntry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
} catch (JSONException ignored) {
}
}
return result;
}
private VaultEntry convertEntry(JSONObject encObj, JSONObject tokenObj)
throws DatabaseImporterEntryException {
try {
JSONObject keyObj = new JSONObject(encObj.getString("key"));
String cipherName = keyObj.getString("mCipher");
if (!cipherName.equals("AES/GCM/NoPadding")) {
throw new DatabaseImporterException(String.format("Unexpected cipher: %s", cipherName));
}
byte[] cipherText = toBytes(keyObj.getJSONArray("mCipherText"));
byte[] parameters = toBytes(keyObj.getJSONArray("mParameters"));
byte[] token = keyObj.getString("mToken").getBytes(StandardCharsets.UTF_8);
byte[] nonce = parseNonce(parameters);
IvParameterSpec spec = new IvParameterSpec(nonce);
Cipher cipher = Cipher.getInstance(cipherName);
cipher.init(Cipher.DECRYPT_MODE, _masterKey, spec);
cipher.updateAAD(token);
byte[] secretBytes = cipher.doFinal(cipherText);
JSONArray secretArray = new JSONArray();
for (byte b : secretBytes) {
secretArray.put(b);
}
tokenObj.put("secret", secretArray);
return DecryptedStateV1.convertEntry(tokenObj);
} catch (DatabaseImporterException | JSONException | NoSuchAlgorithmException |
NoSuchPaddingException | InvalidAlgorithmParameterException |
InvalidKeyException | BadPaddingException | IllegalBlockSizeException |
IOException e) {
throw new DatabaseImporterEntryException(e, tokenObj.toString());
}
}
}
public static class DecryptedStateV1 extends DatabaseImporter.State {
private final List<JSONObject> _entries;
public DecryptedStateV1(List<JSONObject> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
for (JSONObject obj : _entries) {
try {
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
String type = obj.getString("type").toLowerCase(Locale.ROOT);
String algo = obj.optString("algo", OtpInfo.DEFAULT_ALGORITHM);
int digits = obj.optInt("digits", OtpInfo.DEFAULT_DIGITS);
byte[] secret = toBytes(obj.getJSONArray("secret"));
String issuer = obj.getString("issuerExt");
String name = obj.optString("label");
OtpInfo info;
switch (type) {
case "totp":
int period = obj.optInt("period", TotpInfo.DEFAULT_PERIOD);
if (issuer.equals("Steam")) {
info = new SteamInfo(secret, algo, digits, period);
} else {
info = new TotpInfo(secret, algo, digits, period);
}
break;
case "hotp":
info = new HotpInfo(secret, algo, digits, obj.getLong("counter"));
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + type);
}
return new VaultEntry(info, name, issuer);
} catch (DatabaseImporterException | OtpInfoException | JSONException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
private static byte[] parseNonce(byte[] parameters) throws IOException {
ASN1Primitive prim = ASN1Sequence.fromByteArray(parameters);
if (prim instanceof ASN1OctetString) {
return ((ASN1OctetString) prim).getOctets();
}
if (prim instanceof ASN1Sequence) {
for (ASN1Encodable enc : (ASN1Sequence) prim) {
if (enc instanceof ASN1OctetString) {
return ((ASN1OctetString) enc).getOctets();
}
}
}
throw new IOException("Unable to find nonce in parameters");
}
private static byte[] toBytes(JSONArray array) throws JSONException {
byte[] bytes = new byte[array.length()];
for (int i = 0; i < array.length(); i++) {
bytes[i] = (byte)array.getInt(i);
}
return bytes;
}
private static class SerializedHashMapParser {
private static final int MAGIC = 0xaced;
private static final int VERSION = 5;
private static final long SERIAL_VERSION_UID = 362498820763181265L;
private static final byte TC_NULL = 0x70;
private static final byte TC_CLASSDESC = 0x72;
private static final byte TC_OBJECT = 0x73;
private static final byte TC_STRING = 0x74;
private SerializedHashMapParser() {
}
public static Map<String, String> parse(DataInputStream inStream)
throws IOException, ParseException {
Map<String, String> map = new HashMap<>();
// Read/validate the magic number and version
int magic = inStream.readUnsignedShort();
int version = inStream.readUnsignedShort();
if (magic != MAGIC || version != VERSION) {
throw new ParseException("Not a serialized Java Object");
}
// Read the class descriptor info for HashMap
byte b = inStream.readByte();
if (b != TC_OBJECT) {
throw new ParseException("Expected an object, found: " + b);
}
b = inStream.readByte();
if (b != TC_CLASSDESC) {
throw new ParseException("Expected a class desc, found: " + b);
}
parseClassDescriptor(inStream);
// Not interested in the capacity of the map
inStream.readInt();
// Read the number of elements in the HashMap
int size = inStream.readInt();
// Parse each key-value pair in the map
for (int i = 0; i < size; i++) {
String key = parseStringObject(inStream);
String value = parseStringObject(inStream);
map.put(key, value);
}
return map;
}
private static void parseClassDescriptor(DataInputStream inputStream)
throws IOException, ParseException {
// Check whether we're dealing with a HashMap and a version we support
String className = parseUTF(inputStream);
if (!className.equals(HashMap.class.getName())) {
throw new ParseException(String.format("Unexpected class name: %s", className));
}
long serialVersionUID = inputStream.readLong();
if (serialVersionUID != SERIAL_VERSION_UID) {
throw new ParseException(String.format("Unexpected serial version UID: %d", serialVersionUID));
}
// Read past all of the fields in the class
byte fieldDescriptor = inputStream.readByte();
if (fieldDescriptor == TC_NULL) {
return;
}
int totalFieldSkip = 0;
int fieldCount = inputStream.readUnsignedShort();
for (int i = 0; i < fieldCount; i++) {
char fieldType = (char) inputStream.readByte();
parseUTF(inputStream);
switch (fieldType) {
case 'F': // float (4 bytes)
case 'I': // int (4 bytes)
totalFieldSkip += 4;
break;
default:
throw new ParseException(String.format("Unexpected field type: %s", fieldType));
}
}
inputStream.skipBytes(totalFieldSkip);
// Not sure what these bytes are, just skip them
inputStream.skipBytes(4);
}
private static String parseStringObject(DataInputStream inputStream)
throws IOException, ParseException {
byte objectType = inputStream.readByte();
if (objectType != TC_STRING) {
throw new ParseException(String.format("Expected a string object, found: %d", objectType));
}
int length = inputStream.readUnsignedShort();
byte[] strBytes = new byte[length];
inputStream.readFully(strBytes);
return new String(strBytes, StandardCharsets.UTF_8);
}
private static String parseUTF(DataInputStream inputStream) throws IOException {
int length = inputStream.readUnsignedShort();
byte[] strBytes = new byte[length];
inputStream.readFully(strBytes);
return new String(strBytes, StandardCharsets.UTF_8);
}
private static class ParseException extends Exception {
public ParseException(String message) {
super(message);
}
}
}
}

View file

@ -0,0 +1,57 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import com.beemdevelopment.aegis.util.IOUtils;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
public class FreeOtpPlusImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/tokens.xml";
private static final String _pkgName = "org.liberty.android.freeotpplus";
public FreeOtpPlusImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
State state;
if (isInternal) {
state = new FreeOtpImporter(requireContext()).read(stream);
} else {
try {
String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
JSONObject obj = new JSONObject(json);
JSONArray array = obj.getJSONArray("tokens");
List<JSONObject> entries = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
entries.add(array.getJSONObject(i));
}
state = new FreeOtpImporter.DecryptedStateV1(entries);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
return state;
}
}

View file

@ -0,0 +1,171 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import java.io.InputStream;
import java.util.List;
public class GoogleAuthImporter extends DatabaseImporter {
private static final int TYPE_TOTP = 0;
private static final int TYPE_HOTP = 1;
private static final String _subPath = "databases/databases";
private static final String _pkgName = "com.google.android.apps.authenticator2";
public GoogleAuthImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
SuFile file = getAppPath(_pkgName, _subPath);
return file;
}
@Override
public boolean isInstalledAppVersionSupported() {
PackageInfo info;
try {
info = requireContext().getPackageManager().getPackageInfo(_pkgName, 0);
} catch (PackageManager.NameNotFoundException e) {
return false;
}
return info.versionCode <= 5000100;
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
final Context context = requireContext();
SqlImporterHelper helper = new SqlImporterHelper(context);
List<Entry> entries = helper.read(Entry.class, stream, "accounts");
return new State(entries, context);
}
@Override
public DatabaseImporter.State readFromApp(Shell shell) throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile path = getAppPath();
path.setShell(shell);
final Context context = requireContext();
SqlImporterHelper helper = new SqlImporterHelper(context);
List<Entry> entries = helper.read(Entry.class, path, "accounts");
return new State(entries, context);
}
public static class State extends DatabaseImporter.State {
private List<Entry> _entries;
private Context _context;
private State(List<Entry> entries, Context context) {
super(false);
_entries = entries;
_context = context;
}
@Override
public Result convert() {
Result result = new Result();
for (Entry sqlEntry : _entries) {
try {
VaultEntry entry = convertEntry(sqlEntry, _context);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(Entry entry, Context context) throws DatabaseImporterEntryException {
try {
if (entry.isEncrypted()) {
throw new DatabaseImporterException(context.getString(R.string.importer_encrypted_exception_google_authenticator, entry.getEmail()));
}
byte[] secret = GoogleAuthInfo.parseSecret(entry.getSecret());
OtpInfo info;
switch (entry.getType()) {
case TYPE_TOTP:
info = new TotpInfo(secret);
break;
case TYPE_HOTP:
info = new HotpInfo(secret, entry.getCounter());
break;
default:
throw new DatabaseImporterException("unsupported otp type: " + entry.getType());
}
String name = entry.getEmail();
String[] parts = name.split(":");
if (parts.length == 2) {
name = parts[1];
}
return new VaultEntry(info, name, entry.getIssuer());
} catch (EncodingException | OtpInfoException | DatabaseImporterException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
}
private static class Entry extends SqlImporterHelper.Entry {
private int _type;
private boolean _isEncrypted;
private String _secret;
private String _email;
private String _issuer;
private long _counter;
public Entry(Cursor cursor) {
super(cursor);
_type = SqlImporterHelper.getInt(cursor, "type");
_secret = SqlImporterHelper.getString(cursor, "secret");
_email = SqlImporterHelper.getString(cursor, "email", "");
_issuer = SqlImporterHelper.getString(cursor, "issuer", "");
_counter = SqlImporterHelper.getLong(cursor, "counter");
_isEncrypted = (cursor.getColumnIndex("isencrypted") != -1 && SqlImporterHelper.getInt(cursor, "isencrypted") > 0);
}
public int getType() {
return _type;
}
public boolean isEncrypted() {
return _isEncrypted;
}
public String getSecret() {
return _secret;
}
public String getEmail() {
return _email;
}
public String getIssuer() {
return _issuer;
}
public long getCounter() {
return _counter;
}
}
}

View file

@ -0,0 +1,78 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
public class GoogleAuthUriImporter extends DatabaseImporter {
public GoogleAuthUriImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public GoogleAuthUriImporter.State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
ArrayList<String> lines = new ArrayList<>();
try (InputStreamReader streamReader = new InputStreamReader(stream);
BufferedReader bufferedReader = new BufferedReader(streamReader)) {
String line;
while ((line = bufferedReader.readLine()) != null) {
if (!line.isEmpty()) {
lines.add(line);
}
}
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
return new GoogleAuthUriImporter.State(lines);
}
public static class State extends DatabaseImporter.State {
private ArrayList<String> _lines;
private State(ArrayList<String> lines) {
super(false);
_lines = lines;
}
@Override
public DatabaseImporter.Result convert() {
DatabaseImporter.Result result = new DatabaseImporter.Result();
for (String line : _lines) {
try {
VaultEntry entry = convertEntry(line);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(String line) throws DatabaseImporterEntryException {
try {
GoogleAuthInfo info = GoogleAuthInfo.parseUri(line);
return new VaultEntry(info);
} catch (GoogleAuthInfoException e) {
throw new DatabaseImporterEntryException(e, line);
}
}
}
}

View file

@ -0,0 +1,135 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.Shell;
import com.topjohnwu.superuser.io.SuFile;
import java.io.InputStream;
import java.util.List;
public class MicrosoftAuthImporter extends DatabaseImporter {
private static final String _subPath = "databases/PhoneFactor";
private static final String _pkgName = "com.azure.authenticator";
private static final int TYPE_TOTP = 0;
private static final int TYPE_MICROSOFT = 1;
public MicrosoftAuthImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
SqlImporterHelper helper = new SqlImporterHelper(requireContext());
List<Entry> entries = helper.read(Entry.class, stream, "accounts");
return new State(entries);
}
@Override
public DatabaseImporter.State readFromApp(Shell shell) throws PackageManager.NameNotFoundException, DatabaseImporterException {
SuFile path = getAppPath();
path.setShell(shell);
SqlImporterHelper helper = new SqlImporterHelper(requireContext());
List<Entry> entries = helper.read(Entry.class, path, "accounts");
return new State(entries);
}
public static class State extends DatabaseImporter.State {
private List<Entry> _entries;
private State(List<Entry> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
for (Entry sqlEntry : _entries) {
try {
int type = sqlEntry.getType();
if (type == TYPE_TOTP || type == TYPE_MICROSOFT) {
VaultEntry entry = convertEntry(sqlEntry);
result.addEntry(entry);
}
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(Entry entry) throws DatabaseImporterEntryException {
try {
byte[] secret;
int digits = 6;
switch (entry.getType()) {
case TYPE_TOTP:
secret = GoogleAuthInfo.parseSecret(entry.getSecret());
break;
case TYPE_MICROSOFT:
digits = 8;
secret = Base64.decode(entry.getSecret());
break;
default:
throw new DatabaseImporterEntryException(String.format("Unsupported OTP type: %d", entry.getType()), entry.toString());
}
OtpInfo info = new TotpInfo(secret, OtpInfo.DEFAULT_ALGORITHM, digits, TotpInfo.DEFAULT_PERIOD);
return new VaultEntry(info, entry.getUserName(), entry.getIssuer());
} catch (EncodingException | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
}
private static class Entry extends SqlImporterHelper.Entry {
private int _type;
private String _secret;
private String _issuer;
private String _userName;
public Entry(Cursor cursor) {
super(cursor);
_type = SqlImporterHelper.getInt(cursor, "account_type");
_secret = SqlImporterHelper.getString(cursor, "oath_secret_key");
_issuer = SqlImporterHelper.getString(cursor, "name");
_userName = SqlImporterHelper.getString(cursor, "username");
}
public int getType() {
return _type;
}
public String getSecret() {
return _secret;
}
public String getIssuer() {
return _issuer;
}
public String getUserName() {
return _userName;
}
}
}

View file

@ -0,0 +1,96 @@
package com.beemdevelopment.aegis.importers;
import static java.nio.charset.StandardCharsets.UTF_8;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
public class ProtonAuthenticatorImporter extends DatabaseImporter {
public ProtonAuthenticatorImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
protected @NonNull State read(@NonNull InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
String contents = new String(IOUtils.readAll(stream), UTF_8);
JSONObject json = new JSONObject(contents);
return new DecryptedState(json);
} catch (JSONException | IOException e) {
throw new DatabaseImporterException(e);
}
}
public static class DecryptedState extends DatabaseImporter.State {
private final JSONObject _json;
public DecryptedState(@NonNull JSONObject json) {
super(false);
_json = json;
}
@Override
public @NonNull Result convert() throws DatabaseImporterException {
Result result = new Result();
try {
JSONArray entries = _json.getJSONArray("entries");
for (int i = 0; i < entries.length(); i++) {
JSONObject entry = entries.getJSONObject(i);
try {
result.addEntry(convertEntry(entry));
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return result;
}
private static @NonNull VaultEntry convertEntry(@NonNull JSONObject entry) throws DatabaseImporterEntryException {
try {
JSONObject content = entry.getJSONObject("content");
String name = content.getString("name");
String uriString = content.getString("uri");
Uri uri = Uri.parse(uriString);
try {
GoogleAuthInfo info = GoogleAuthInfo.parseUri(uri);
OtpInfo otp = info.getOtpInfo();
return new VaultEntry(otp, name, info.getIssuer());
} catch (GoogleAuthInfoException e) {
throw new DatabaseImporterEntryException(e, uriString);
}
} catch (JSONException e) {
throw new DatabaseImporterEntryException(e, entry.toString());
}
}
}
}

View file

@ -0,0 +1,150 @@
package com.beemdevelopment.aegis.importers;
import static android.database.sqlite.SQLiteDatabase.OPEN_READONLY;
import android.annotation.SuppressLint;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import com.beemdevelopment.aegis.util.IOUtils;
import com.google.common.io.Files;
import com.topjohnwu.superuser.io.SuFile;
import com.topjohnwu.superuser.io.SuFileInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
public class SqlImporterHelper {
private Context _context;
public SqlImporterHelper(Context context) {
_context = context;
}
public <T extends Entry> List<T> read(Class<T> type, SuFile path, String table) throws DatabaseImporterException {
File dir = Files.createTempDir();
File mainFile = new File(dir, path.getName());
List<File> fileCopies = new ArrayList<>();
for (SuFile file : SqlImporterHelper.findDatabaseFiles(path)) {
// create temporary copies of the database files so that SQLiteDatabase can open them
File fileCopy = null;
try (InputStream inStream = SuFileInputStream.open(file)) {
fileCopy = new File(dir, file.getName());
try (FileOutputStream out = new FileOutputStream(fileCopy)) {
IOUtils.copy(inStream, out);
}
fileCopies.add(fileCopy);
} catch (IOException e) {
if (fileCopy != null) {
fileCopy.delete();
}
for (File fileCopy2 : fileCopies) {
fileCopy2.delete();
}
throw new DatabaseImporterException(e);
}
}
try {
return read(type, mainFile, table);
} finally {
for (File fileCopy : fileCopies) {
fileCopy.delete();
}
}
}
private static SuFile[] findDatabaseFiles(SuFile path) throws DatabaseImporterException {
SuFile[] files = path.getParentFile().listFiles((d, name) -> name.startsWith(path.getName()));
if (files == null || files.length == 0) {
throw new DatabaseImporterException(String.format("File does not exist: %s", path.getAbsolutePath()));
}
return files;
}
public <T extends Entry> List<T> read(Class<T> type, InputStream inStream, String table) throws DatabaseImporterException {
File file = null;
try {
// create a temporary copy of the database so that SQLiteDatabase can open it
file = File.createTempFile("db-import-", "", _context.getCacheDir());
try (FileOutputStream out = new FileOutputStream(file)) {
IOUtils.copy(inStream, out);
}
} catch (IOException e) {
if (file != null) {
file.delete();
}
throw new DatabaseImporterException(e);
}
try {
return read(type, file, table);
} finally {
// always delete the temporary file
file.delete();
}
}
private <T extends Entry> List<T> read(Class<T> type, File file, String table) throws DatabaseImporterException {
try (SQLiteDatabase db = SQLiteDatabase.openDatabase(file.getAbsolutePath(), null, OPEN_READONLY)) {
try (Cursor cursor = db.rawQuery(String.format("SELECT * FROM %s", table), null)) {
List<T> entries = new ArrayList<>();
if (cursor.moveToFirst()) {
do {
T entry = type.getDeclaredConstructor(Cursor.class).newInstance(cursor);
entries.add(entry);
} while (cursor.moveToNext());
}
return entries;
} catch (InstantiationException | IllegalAccessException
| NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
} catch (SQLiteException e) {
throw new DatabaseImporterException(e);
}
}
@SuppressLint("Range")
public static String getString(Cursor cursor, String columnName) {
return cursor.getString(cursor.getColumnIndex(columnName));
}
@SuppressLint("Range")
public static String getString(Cursor cursor, String columnName, String def) {
String res = cursor.getString(cursor.getColumnIndex(columnName));
if (res == null) {
return def;
}
return res;
}
@SuppressLint("Range")
public static int getInt(Cursor cursor, String columnName) {
return cursor.getInt(cursor.getColumnIndex(columnName));
}
@SuppressLint("Range")
public static long getLong(Cursor cursor, String columnName) {
return cursor.getLong(cursor.getColumnIndex(columnName));
}
public static abstract class Entry {
public Entry(Cursor cursor) {
}
}
}

View file

@ -0,0 +1,118 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.sql.Array;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
public class SteamImporter extends DatabaseImporter {
private static final String _subDir = "files";
private static final String _pkgName = "com.valvesoftware.android.steam.community";
public SteamImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
// NOTE: this assumes that a global root shell has already been obtained by the caller
SuFile path = getAppPath(_pkgName, _subDir);
SuFile[] files = path.listFiles((d, name) -> name.startsWith("Steamguard-"));
if (files == null || files.length == 0) {
throw new DatabaseImporterException(String.format("Empty directory: %s", path.getAbsolutePath()));
}
// TODO: handle multiple files (can this even occur?)
return files[0];
}
@Override
public boolean isInstalledAppVersionSupported() {
PackageInfo info;
try {
info = requireContext().getPackageManager().getPackageInfo(_pkgName, 0);
} catch (PackageManager.NameNotFoundException e) {
return false;
}
return info.versionCode < 7460894;
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
byte[] bytes = IOUtils.readAll(stream);
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
List<JSONObject> objs = new ArrayList<>();
if (obj.has("accounts")) {
JSONObject accounts = obj.getJSONObject("accounts");
Iterator<String> keys = accounts.keys();
while (keys.hasNext()) {
String key = keys.next();
objs.add(accounts.getJSONObject(key));
}
} else {
objs.add(obj);
}
return new State(objs);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
public static class State extends DatabaseImporter.State {
private final List<JSONObject> _objs;
private State(List<JSONObject> objs) {
super(false);
_objs = objs;
}
@Override
public Result convert() {
Result result = new Result();
for (JSONObject obj : _objs) {
try {
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
byte[] secret = Base64.decode(obj.getString("shared_secret"));
SteamInfo info = new SteamInfo(secret);
String account = obj.getString("account_name");
return new VaultEntry(info, account, "Steam");
} catch (JSONException | EncodingException | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}

View file

@ -0,0 +1,380 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.database.Cursor;
import androidx.lifecycle.Lifecycle;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.helpers.ContextHelper;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.ui.tasks.Argon2Task;
import com.beemdevelopment.aegis.ui.tasks.PBKDFTask;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
import org.bouncycastle.crypto.params.Argon2Parameters;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UTFDataFormatException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
public class StratumImporter extends DatabaseImporter {
private static final String HEADER = "AUTHENTICATORPRO";
private static final String HEADER_LEGACY = "AuthenticatorPro";
private static final String PKG_NAME = "com.stratumauth.app";
private static final String PKG_DB_PATH = "databases/authenticator.db3";
private enum Algorithm {
SHA1,
SHA256,
SHA512
}
public StratumImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws DatabaseImporterException, PackageManager.NameNotFoundException {
return getAppPath(PKG_NAME, PKG_DB_PATH);
}
@Override
protected State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
return isInternal ? readInternal(stream) : readExternal(stream);
}
private State readInternal(InputStream stream) throws DatabaseImporterException {
List<SqlEntry> entries = new SqlImporterHelper(requireContext()).read(SqlEntry.class, stream, "authenticator");
return new SqlState(entries);
}
private static State readExternal(InputStream stream) throws DatabaseImporterException {
byte[] data;
try {
data = IOUtils.readAll(stream);
} catch (IOException e) {
throw new DatabaseImporterException(e);
}
try {
return new JsonState(new JSONObject(new String(data, StandardCharsets.UTF_8)));
} catch (JSONException e) {
return readEncrypted(new DataInputStream(new ByteArrayInputStream(data)));
}
}
private static State readEncrypted(DataInputStream stream) throws DatabaseImporterException {
try {
byte[] headerBytes = new byte[HEADER.getBytes(StandardCharsets.UTF_8).length];
stream.readFully(headerBytes);
String header = new String(headerBytes, StandardCharsets.UTF_8);
switch (header) {
case HEADER:
return EncryptedState.parseHeader(stream);
case HEADER_LEGACY:
return LegacyEncryptedState.parseHeader(stream);
default:
throw new DatabaseImporterException("Invalid file header");
}
} catch (UTFDataFormatException e) {
throw new DatabaseImporterException("Invalid file header");
} catch (IOException | NoSuchPaddingException | NoSuchAlgorithmException e) {
throw new DatabaseImporterException(e);
}
}
private static OtpInfo parseOtpInfo(int type, byte[] secret, Algorithm algo, int digits, int period, int counter)
throws OtpInfoException, DatabaseImporterEntryException {
switch (type) {
case 1:
return new HotpInfo(secret, algo.name(), digits, counter);
case 2:
return new TotpInfo(secret, algo.name(), digits, period);
case 4:
return new SteamInfo(secret, algo.name(), digits, period);
default:
throw new DatabaseImporterEntryException(String.format("Unsupported otp type: %d", type), null);
}
}
static class EncryptedState extends State {
private static final int KEY_SIZE = 32;
private static final int MEMORY_COST = 16; // 2^16 KiB = 64 MiB
private static final int PARALLELISM = 4;
private static final int ITERATIONS = 3;
private static final int SALT_SIZE = 16;
private static final int IV_SIZE = 12;
private final Cipher _cipher;
private final byte[] _salt;
private final byte[] _iv;
private final byte[] _data;
public EncryptedState(Cipher cipher, byte[] salt, byte[] iv, byte[] data) {
super(true);
_cipher = cipher;
_salt = salt;
_iv = iv;
_data = data;
}
public JsonState decrypt(char[] password) throws DatabaseImporterException {
Argon2Task.Params params = getKeyDerivationParams(password);
SecretKey key = Argon2Task.deriveKey(params);
return decrypt(key);
}
public JsonState decrypt(SecretKey key) throws DatabaseImporterException {
try {
_cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(_iv));
byte[] decrypted = _cipher.doFinal(_data);
return new JsonState(new JSONObject(new String(decrypted, StandardCharsets.UTF_8)));
} catch (InvalidAlgorithmParameterException | IllegalBlockSizeException
| JSONException | InvalidKeyException | BadPaddingException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) throws DatabaseImporterException {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, (Dialogs.TextInputListener) password -> {
Argon2Task.Params params = getKeyDerivationParams(password);
Argon2Task task = new Argon2Task(context, key -> {
try {
StratumImporter.JsonState state = decrypt(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}, dialog -> listener.onCanceled());
}
private Argon2Task.Params getKeyDerivationParams(char[] password) {
Argon2Parameters argon2Params = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)
.withIterations(ITERATIONS)
.withParallelism(PARALLELISM)
.withMemoryPowOfTwo(MEMORY_COST)
.withSalt(_salt)
.build();
return new Argon2Task.Params(password, argon2Params, KEY_SIZE);
}
private static EncryptedState parseHeader(DataInputStream stream)
throws IOException, NoSuchPaddingException, NoSuchAlgorithmException {
byte[] salt = new byte[SALT_SIZE];
stream.readFully(salt);
byte[] iv = new byte[IV_SIZE];
stream.readFully(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
return new EncryptedState(cipher, salt, iv, IOUtils.readAll(stream));
}
}
static class LegacyEncryptedState extends State {
private static final int ITERATIONS = 64000;
private static final int KEY_SIZE = 32 * Byte.SIZE;
private static final int SALT_SIZE = 20;
private final Cipher _cipher;
private final byte[] _salt;
private final byte[] _iv;
private final byte[] _data;
public LegacyEncryptedState(Cipher cipher, byte[] salt, byte[] iv, byte[] data) {
super(true);
_cipher = cipher;
_salt = salt;
_iv = iv;
_data = data;
}
public JsonState decrypt(char[] password) throws DatabaseImporterException {
PBKDFTask.Params params = getKeyDerivationParams(password);
SecretKey key = PBKDFTask.deriveKey(params);
return decrypt(key);
}
public JsonState decrypt(SecretKey key) throws DatabaseImporterException {
try {
_cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(_iv));
byte[] decrypted = _cipher.doFinal(_data);
return new JsonState(new JSONObject(new String(decrypted, StandardCharsets.UTF_8)));
} catch (InvalidAlgorithmParameterException | IllegalBlockSizeException
| JSONException | InvalidKeyException | BadPaddingException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) throws DatabaseImporterException {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_aegis_title, 0, (Dialogs.TextInputListener) password -> {
PBKDFTask.Params params = getKeyDerivationParams(password);
PBKDFTask task = new PBKDFTask(context, key -> {
try {
StratumImporter.JsonState state = decrypt(key);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
});
Lifecycle lifecycle = ContextHelper.getLifecycle(context);
task.execute(lifecycle, params);
}, dialog -> listener.onCanceled());
}
private PBKDFTask.Params getKeyDerivationParams(char[] password) {
return new PBKDFTask.Params("PBKDF2WithHmacSHA1", KEY_SIZE, password, _salt, ITERATIONS);
}
private static LegacyEncryptedState parseHeader(DataInputStream stream)
throws IOException, NoSuchPaddingException, NoSuchAlgorithmException {
byte[] salt = new byte[SALT_SIZE];
stream.readFully(salt);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
int ivSize = cipher.getBlockSize();
byte[] iv = new byte[ivSize];
stream.readFully(iv);
return new LegacyEncryptedState(cipher, salt, iv, IOUtils.readAll(stream));
}
}
private static class JsonState extends State {
private final JSONObject _obj;
public JsonState(JSONObject obj) {
super(false);
_obj = obj;
}
@Override
public Result convert() throws DatabaseImporterException {
Result res = new Result();
try {
JSONArray array = _obj.getJSONArray("Authenticators");
for (int i = 0; i < array.length(); i++) {
JSONObject obj = array.getJSONObject(i);
try {
res.addEntry(convertEntry(obj));
} catch (DatabaseImporterEntryException e) {
res.addError(e);
}
}
} catch (JSONException e) {
throw new DatabaseImporterException(e);
}
return res;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
int type = obj.getInt("Type");
String issuer = obj.getString("Issuer");
Object nullableUsername = obj.get("Username");
String username = nullableUsername == JSONObject.NULL ? "" : nullableUsername.toString();
byte[] secret = Base32.decode(obj.getString("Secret"));
Algorithm algo = Algorithm.values()[obj.getInt("Algorithm")];
int digits = obj.getInt("Digits");
int period = obj.getInt("Period");
int counter = obj.getInt("Counter");
OtpInfo info = parseOtpInfo(type, secret, algo, digits, period, counter);
return new VaultEntry(info, username, issuer);
} catch (OtpInfoException | EncodingException | JSONException e) {
throw new DatabaseImporterEntryException(e, null);
}
}
}
private static class SqlState extends State {
private final List<SqlEntry> _entries;
public SqlState(List<SqlEntry> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() throws DatabaseImporterException {
Result res = new Result();
for (SqlEntry entry : _entries) {
try {
res.addEntry(entry.convert());
} catch (DatabaseImporterEntryException e) {
res.addError(e);
}
}
return res;
}
}
private static class SqlEntry extends SqlImporterHelper.Entry {
private final int _type;
private final String _issuer;
private final String _username;
private final String _secret;
private final Algorithm _algo;
private final int _digits;
private final int _period;
private final int _counter;
public SqlEntry(Cursor cursor) {
super(cursor);
_type = SqlImporterHelper.getInt(cursor, "type");
_issuer = SqlImporterHelper.getString(cursor, "issuer");
_username = SqlImporterHelper.getString(cursor, "username");
_secret = SqlImporterHelper.getString(cursor, "secret");
_algo = Algorithm.values()[SqlImporterHelper.getInt(cursor, "algorithm")];
_digits = SqlImporterHelper.getInt(cursor, "digits");
_period = SqlImporterHelper.getInt(cursor, "period");
_counter = SqlImporterHelper.getInt(cursor, "counter");
}
public VaultEntry convert() throws DatabaseImporterEntryException {
try {
byte[] secret = Base32.decode(_secret);
OtpInfo info = parseOtpInfo(_type, secret, _algo, _digits, _period, _counter);
return new VaultEntry(info, _username, _issuer);
} catch (EncodingException | OtpInfoException e) {
throw new DatabaseImporterEntryException(e, null);
}
}
}
}

View file

@ -0,0 +1,233 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import android.util.Xml;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.encoding.Base32;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.encoding.Hex;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.util.PreferenceParser;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class TotpAuthenticatorImporter extends DatabaseImporter {
private static final String _subPath = "shared_prefs/TOTP_Authenticator_Preferences.xml";
private static final String _pkgName = "com.authenticator.authservice2";
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this is a hardcoded password and nonce, used solely to decrypt TOTP Authenticator backups
private static final char[] PASSWORD = "TotpAuthenticator".toCharArray();
private static final byte[] IV = new byte[]{
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
private static final String PREF_KEY = "STATIC_TOTP_CODES_LIST";
public TotpAuthenticatorImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() throws PackageManager.NameNotFoundException {
return getAppPath(_pkgName, _subPath);
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
if (isInternal) {
XmlPullParser parser = Xml.newPullParser();
parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
parser.setInput(stream, null);
parser.nextTag();
String data = null;
for (PreferenceParser.XmlEntry entry : PreferenceParser.parse(parser)) {
if (entry.Name.equals(PREF_KEY)) {
data = entry.Value;
}
}
if (data == null) {
throw new DatabaseImporterException(String.format("Key %s not found in shared preference file", PREF_KEY));
}
List<JSONObject> entries = parse(data);
return new DecryptedState(entries);
} else {
byte[] base64 = IOUtils.readAll(stream);
byte[] cipherText = Base64.decode(base64);
return new EncryptedState(cipherText);
}
} catch (IOException | XmlPullParserException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
private static List<JSONObject> parse(String data) throws JSONException {
JSONArray array = new JSONArray(data);
List<JSONObject> entries = new ArrayList<>();
for (int i = 0; i < array.length(); ++i) {
JSONObject obj = array.getJSONObject(i);
entries.add(obj);
}
return entries;
}
public static class EncryptedState extends DatabaseImporter.State {
private byte[] _data;
public EncryptedState(byte[] data) {
super(true);
_data = data;
}
protected DecryptedState decrypt(char[] password) throws DatabaseImporterException {
try {
// WARNING: DON'T DO THIS IN YOUR OWN CODE
// this is not a secure way to derive a key from a password
MessageDigest hash = MessageDigest.getInstance("SHA-256");
byte[] keyBytes = hash.digest(CryptoUtils.toBytes(password));
SecretKey key = new SecretKeySpec(keyBytes, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec spec = new IvParameterSpec(IV);
cipher.init(Cipher.DECRYPT_MODE, key, spec);
byte[] bytes = cipher.doFinal(_data);
JSONObject obj = new JSONObject(new String(bytes, StandardCharsets.UTF_8));
JSONArray keys = obj.names();
List<JSONObject> entries = new ArrayList<>();
if (keys != null && keys.length() > 0) {
entries = parse((String) keys.get(0));
}
return new DecryptedState(entries);
} catch (NoSuchAlgorithmException
| NoSuchPaddingException
| InvalidAlgorithmParameterException
| InvalidKeyException
| BadPaddingException
| IllegalBlockSizeException
| JSONException e) {
throw new DatabaseImporterException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showSecureDialog(new MaterialAlertDialogBuilder(context)
.setMessage(R.string.choose_totpauth_importer)
.setPositiveButton(R.string.yes, (dialog, which) -> {
Dialogs.showPasswordInputDialog(context, password -> {
decrypt(password, listener);
}, dialog1 -> listener.onCanceled());
})
.setNegativeButton(R.string.no, (dialog, which) -> {
decrypt(PASSWORD, listener);
})
.create());
}
private void decrypt(char[] password, DecryptListener listener) {
try {
DecryptedState state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
}
}
public static class DecryptedState extends DatabaseImporter.State {
private List<JSONObject> _objs;
private DecryptedState(List<JSONObject> objs) {
super(false);
_objs = objs;
}
@Override
public Result convert() {
Result result = new Result();
for (JSONObject obj : _objs) {
try {
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
int base = obj.getInt("base");
String secretString = obj.getString("key");
byte[] secret;
switch (base) {
case 16:
secret = Hex.decode(secretString);
break;
case 32:
secret = Base32.decode(secretString);
break;
case 64:
secret = Base64.decode(secretString);
break;
default:
throw new DatabaseImporterEntryException(String.format("Unsupported secret encoding: base %d", base), obj.toString());
}
TotpInfo info = new TotpInfo(secret);
String name = obj.optString("name");
String issuer = obj.optString("issuer");
return new VaultEntry(info, name, issuer);
} catch (JSONException | OtpInfoException | EncodingException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}

View file

@ -0,0 +1,208 @@
package com.beemdevelopment.aegis.importers;
import android.content.Context;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.crypto.CryptoUtils;
import com.beemdevelopment.aegis.encoding.Base64;
import com.beemdevelopment.aegis.encoding.EncodingException;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.HotpInfo;
import com.beemdevelopment.aegis.otp.OtpInfo;
import com.beemdevelopment.aegis.otp.OtpInfoException;
import com.beemdevelopment.aegis.otp.SteamInfo;
import com.beemdevelopment.aegis.otp.TotpInfo;
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
import com.beemdevelopment.aegis.util.IOUtils;
import com.beemdevelopment.aegis.util.JsonUtils;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.google.common.base.Strings;
import com.topjohnwu.superuser.io.SuFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.ArrayList;
import java.util.List;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
public class TwoFASImporter extends DatabaseImporter {
private static final int ITERATION_COUNT = 10_000;
private static final int KEY_SIZE = 256; // bits
public TwoFASImporter(Context context) {
super(context);
}
@Override
protected SuFile getAppPath() {
throw new UnsupportedOperationException();
}
@Override
public State read(InputStream stream, boolean isInternal) throws DatabaseImporterException {
try {
String json = new String(IOUtils.readAll(stream), StandardCharsets.UTF_8);
JSONObject obj = new JSONObject(json);
int version = obj.getInt("schemaVersion");
if (version > 4) {
throw new DatabaseImporterException(String.format("Unsupported schema version: %d", version));
}
String encryptedString = JsonUtils.optString(obj, "servicesEncrypted");
if (encryptedString == null) {
JSONArray array = obj.getJSONArray("services");
List<JSONObject> entries = arrayToList(array);
return new DecryptedState(entries);
}
String[] parts = encryptedString.split(":");
if (parts.length < 3) {
throw new DatabaseImporterException(String.format("Unexpected format of encrypted data (parts: %d)", parts.length));
}
byte[] data = Base64.decode(parts[0]);
byte[] salt = Base64.decode(parts[1]);
byte[] iv = Base64.decode(parts[2]);
return new EncryptedState(data, salt, iv);
} catch (IOException | JSONException e) {
throw new DatabaseImporterException(e);
}
}
private static List<JSONObject> arrayToList(JSONArray array) throws JSONException {
List<JSONObject> list = new ArrayList<>();
for (int i = 0; i < array.length(); i++) {
list.add(array.getJSONObject(i));
}
return list;
}
public static class EncryptedState extends State {
private final byte[] _data;
private final byte[] _salt;
private final byte[] _iv;
private EncryptedState(byte[] data, byte[] salt, byte[] iv) {
super(true);
_data = data;
_salt = salt;
_iv = iv;
}
private SecretKey deriveKey(char[] password)
throws NoSuchAlgorithmException, InvalidKeySpecException {
SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
KeySpec spec = new PBEKeySpec(password, _salt, ITERATION_COUNT, KEY_SIZE);
SecretKey key = factory.generateSecret(spec);
return new SecretKeySpec(key.getEncoded(), "AES");
}
public DecryptedState decrypt(char[] password) throws DatabaseImporterException {
try {
SecretKey key = deriveKey(password);
Cipher cipher = CryptoUtils.createDecryptCipher(key, _iv);
byte[] decrypted = cipher.doFinal(_data);
String json = new String(decrypted, StandardCharsets.UTF_8);
return new DecryptedState(arrayToList(new JSONArray(json)));
} catch (BadPaddingException | JSONException e) {
throw new DatabaseImporterException(e);
} catch (NoSuchAlgorithmException
| InvalidKeySpecException
| InvalidAlgorithmParameterException
| NoSuchPaddingException
| InvalidKeyException
| IllegalBlockSizeException e) {
throw new RuntimeException(e);
}
}
@Override
public void decrypt(Context context, DecryptListener listener) {
Dialogs.showPasswordInputDialog(context, R.string.enter_password_2fas_message, 0, password -> {
try {
DecryptedState state = decrypt(password);
listener.onStateDecrypted(state);
} catch (DatabaseImporterException e) {
listener.onError(e);
}
}, dialog -> listener.onCanceled());
}
}
public static class DecryptedState extends DatabaseImporter.State {
private final List<JSONObject> _entries;
public DecryptedState(List<JSONObject> entries) {
super(false);
_entries = entries;
}
@Override
public Result convert() {
Result result = new Result();
for (JSONObject obj : _entries) {
try {
VaultEntry entry = convertEntry(obj);
result.addEntry(entry);
} catch (DatabaseImporterEntryException e) {
result.addError(e);
}
}
return result;
}
private static VaultEntry convertEntry(JSONObject obj) throws DatabaseImporterEntryException {
try {
byte[] secret = GoogleAuthInfo.parseSecret(obj.getString("secret"));
JSONObject info = obj.getJSONObject("otp");
String issuer = obj.optString("name");
if (Strings.isNullOrEmpty(issuer)) {
issuer = info.optString("issuer");
}
String name = info.optString("account");
int digits = info.optInt("digits", TotpInfo.DEFAULT_DIGITS);
String algorithm = info.optString("algorithm", TotpInfo.DEFAULT_ALGORITHM);
OtpInfo otp;
String tokenType = JsonUtils.optString(info, "tokenType");
if (tokenType == null || tokenType.equals("TOTP")) {
int period = info.optInt("period", TotpInfo.DEFAULT_PERIOD);
otp = new TotpInfo(secret, algorithm, digits, period);
} else if (tokenType.equals("HOTP")) {
long counter = info.optLong("counter", 0);
otp = new HotpInfo(secret, algorithm, digits, counter);
} else if (tokenType.equals("STEAM")) {
int period = info.optInt("period", TotpInfo.DEFAULT_PERIOD);
otp = new SteamInfo(secret, algorithm, digits, period);
} else {
throw new DatabaseImporterEntryException(String.format("Unrecognized tokenType: %s", tokenType), obj.toString());
}
return new VaultEntry(otp, name, issuer);
} catch (OtpInfoException | JSONException | EncodingException e) {
throw new DatabaseImporterEntryException(e, obj.toString());
}
}
}
}

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