/*
* Copyright (C) 2014 Nullbyte <http://nullbyte.eu>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.liato.bankdroid.banking;
import com.crashlytics.android.answers.CustomEvent;
import com.liato.bankdroid.banking.exceptions.BankException;
import com.liato.bankdroid.db.Crypto;
import com.liato.bankdroid.db.DBAdapter;
import com.liato.bankdroid.db.Database;
import com.liato.bankdroid.db.DatabaseHelper;
import com.liato.bankdroid.utils.LoggingUtils;
import net.sf.andhsli.hotspotlogin.SimpleCrypto;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.Nullable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import timber.log.Timber;
public class BankFactory {
private static Bank fromBanktypeId(int id, Context context) throws BankException {
return LegacyBankFactory.fromBanktypeId(id, context);
}
public static List<Bank> listBanks(Context context) {
return LegacyBankFactory.listBanks(context);
}
@Nullable
public static Bank bankFromDb(long id, Context context, boolean loadAccounts) {
Bank bank = null;
DBAdapter db = new DBAdapter(context);
Cursor c = db.getBank(id);
if (c != null && c.getCount() > 0) {
try {
bank = fromBanktypeId(c.getInt(c.getColumnIndex("banktype")), context);
bank.setProperties(loadProperties(id, context));
bank.setData(new BigDecimal(c.getString(c.getColumnIndex("balance"))),
(c.getInt(c.getColumnIndex("disabled")) != 0),
c.getLong(c.getColumnIndex("_id")),
c.getString(c.getColumnIndex("currency")),
c.getString(c.getColumnIndex("custname")),
c.getInt(c.getColumnIndex("hideAccounts")));
if (loadAccounts) {
bank.setAccounts(accountsFromDb(context, bank.getDbId()));
}
} catch (BankException e) {
Timber.w(e, "Failed getting bank from database");
} finally {
c.close();
}
}
return bank;
}
public static ArrayList<Bank> banksFromDb(Context context, boolean loadAccounts) {
ArrayList<Bank> banks = new ArrayList<>();
DBAdapter db = new DBAdapter(context);
Cursor c = db.fetchBanks();
try {
if (c == null || c.getCount() == 0) {
return banks;
}
while (!c.isLast() && !c.isAfterLast()) {
c.moveToNext();
try {
Bank bank = fromBanktypeId(c.getInt(c.getColumnIndex("banktype")), context);
long id = c.getLong(c.getColumnIndex("_id"));
bank.setProperties(loadProperties(id, context));
bank.setData(new BigDecimal(c.getString(c.getColumnIndex("balance"))),
(c.getInt(c.getColumnIndex("disabled")) != 0),
id,
c.getString(c.getColumnIndex("currency")),
c.getString(c.getColumnIndex("custname")),
c.getInt(c.getColumnIndex("hideAccounts")));
if (loadAccounts) {
bank.setAccounts(accountsFromDb(context, bank.getDbId()));
}
banks.add(bank);
} catch (BankException e) {
Timber.w(e, "BankFactory.banksFromDb()");
}
}
} finally {
if (c != null) {
c.close();
}
}
return banks;
}
@Nullable
public static Account accountFromDb(Context context, String accountId,
boolean loadTransactions) {
DBAdapter db = new DBAdapter(context);
Cursor ac = db.getAccount(accountId);
Account account;
try {
if (ac == null || ac.isClosed() || (ac.isBeforeFirst() && ac.isAfterLast())) {
return null;
}
account = new Account(ac.getString(ac.getColumnIndex("name")),
new BigDecimal(ac.getString(ac.getColumnIndex("balance"))),
ac.getString(ac.getColumnIndex("id")).split("_", 2)[1],
ac.getLong(ac.getColumnIndex("bankid")),
ac.getInt(ac.getColumnIndex("acctype")));
account.setHidden(ac.getInt(ac.getColumnIndex("hidden")) == 1);
account.setNotify(ac.getInt(ac.getColumnIndex("notify")) == 1);
account.setCurrency(ac.getString(ac.getColumnIndex("currency")));
account.setAliasfor(ac.getString(ac.getColumnIndex("aliasfor")));
} finally {
if (ac != null) {
ac.close();
}
}
if (loadTransactions) {
ArrayList<Transaction> transactions = new ArrayList<>();
String fromAccount = accountId;
if (account.getAliasfor() != null && account.getAliasfor().length() > 0) {
fromAccount = Long.toString(account.getBankDbId()) + "_" + account.getAliasfor();
}
Cursor tc = db.fetchTransactions(fromAccount);
try {
if (!(tc == null || tc.isClosed() || (tc.isBeforeFirst() && tc.isAfterLast()))) {
while (!tc.isLast() && !tc.isAfterLast()) {
tc.moveToNext();
transactions.add(new Transaction(tc.getString(tc.getColumnIndex("transdate")),
tc.getString(tc.getColumnIndex("btransaction")),
new BigDecimal(tc.getString(tc.getColumnIndex("amount"))),
tc.getString(tc.getColumnIndex("currency"))));
}
}
} finally {
if (tc != null) {
tc.close();
}
}
account.setTransactions(transactions);
}
return account;
}
private static ArrayList<Account> accountsFromDb(Context context, long bankId) {
ArrayList<Account> accounts = new ArrayList<>();
DBAdapter db = new DBAdapter(context);
Cursor c = db.fetchAccounts(bankId);
try {
if (c == null || c.getCount() == 0) {
return accounts;
}
while (!c.isLast() && !c.isAfterLast()) {
c.moveToNext();
try {
Account account = new Account(c.getString(c.getColumnIndex("name")),
new BigDecimal(c.getString(c.getColumnIndex("balance"))),
c.getString(c.getColumnIndex("id")).split("_", 2)[1],
c.getLong(c.getColumnIndex("bankid")),
c.getInt(c.getColumnIndex("acctype")));
account.setHidden(c.getInt(c.getColumnIndex("hidden")) == 1);
account.setNotify(c.getInt(c.getColumnIndex("notify")) == 1);
account.setCurrency(c.getString(c.getColumnIndex("currency")));
account.setAliasfor(c.getString(c.getColumnIndex("aliasfor")));
accounts.add(account);
} catch (ArrayIndexOutOfBoundsException e) {
// Probably an old Avanza account
Timber.w(e, "Attempted to load an account without an ID: %d", bankId);
}
}
} finally {
if (c != null) {
c.close();
}
}
return accounts;
}
private static Map<String, String> loadProperties(long bankId, Context context) {
Map<String, String> properties = new HashMap<>();
Map<String, String> decryptedProperties = new HashMap<>();
DBAdapter db = new DBAdapter(context);
Cursor c = db.fetchProperties(Long.toString(bankId));
try {
if (c == null || c.getCount() == 0) {
return properties;
}
while (!c.isLast() && !c.isAfterLast()) {
c.moveToNext();
String key = c.getString(c.getColumnIndex(Database.PROPERTY_KEY));
String value = c.getString(c.getColumnIndex(Database.PROPERTY_VALUE));
if (LegacyProviderConfiguration.PASSWORD.equals(key)) {
try {
value = SimpleCrypto.decrypt(Crypto.getKey(), value);
decryptedProperties.put(key, value);
} catch (Exception e) {
Timber.i("%s %s",
"Failed decrypting bank properties.",
"This usually means they are unencrypted, which is exactly what we want them to be.");
}
}
properties.put(key, value);
}
} finally {
if (c != null) {
c.close();
}
}
storeDecryptedProperties(context, bankId, decryptedProperties);
return properties;
}
/**
* Stores decrypted passwords on disk.
* <p/>
* This is a step in removing password encryption alltogether.
* <p/>
* The background is that it's broken on Androin Nougat anyway, and that it
* didn't provide any extra security before that either.
* <p/>
* Since Bankdroid needs to send plain text passwords to the banks, it must
* be possible to retrieve the plain text passwords automatically. And if the
* passwords are encrypted on disk, Bankdroid needs to have the key. And if
* Bankdroid stores both the key and the encrypted password on the phone, a
* determined attacker could get both anyway, and the encryption is useless.
* <p/>
* The only thing the encryption has protected against is a using rooting
* their own device and retrieving their own plain text passwords. This would
* enable the attacker to reaa their own account balance from the bank. Which
* they likely already could even before this change...
*/
private static void storeDecryptedProperties(
Context context, long bankId, Map<String, String> decryptedProperties)
{
if (decryptedProperties.isEmpty()) {
return;
}
Timber.i("Storing %d decrypted properties...", decryptedProperties.size());
SQLiteDatabase db = DatabaseHelper.getHelper(context).getWritableDatabase();
for (Map.Entry<String, String> property : decryptedProperties.entrySet()) {
String value = property.getValue();
if (value != null && !value.isEmpty()) {
ContentValues propertyValues = new ContentValues();
propertyValues.put(Database.PROPERTY_KEY, property.getKey());
propertyValues.put(Database.PROPERTY_VALUE, value);
propertyValues.put(Database.PROPERTY_CONNECTION_ID, bankId);
db.insertWithOnConflict(
Database.PROPERTY_TABLE_NAME, null, propertyValues,
SQLiteDatabase.CONFLICT_REPLACE);
}
}
Timber.i("%d decrypted properties stored", decryptedProperties.size());
LoggingUtils.logCustom(new CustomEvent("Passwords Decrypted"));
}
}