package com.dgex.offspring.wallet;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.log4j.Logger;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.JSONValue;
import com.dgex.offspring.config.Config;
import com.dgex.offspring.wallet.Cryptos.DecryptException;
import com.dgex.offspring.wallet.Cryptos.EncryptException;
@SuppressWarnings("serial")
public class Wallet implements IWallet {
public static class WalletDecodeException extends Exception {}
public static class WalletEncodeException extends Exception {}
private final static Logger logger = Logger.getLogger(Wallet.class);
private final static String KEY_ACCOUNTS = "walletAccounts";
private final static String KEY_PADDING = "padding";
private final List<IWalletAccount> walletAccounts = new ArrayList<IWalletAccount>();
private File file = null;
private String password = null;
private boolean initialized = false;
private boolean fileExists = false;
public Wallet() {
this.file = getDefaultWalletFile();
}
public Wallet(File file) {
this.file = file;
}
@Override
public File getDefaultWalletFile() {
return Config.getAppPath("offspring.wallet");
}
@Override
public void initialize(String password) throws WalletInvalidPassword {
if (initialized)
return;
this.password = password;
if (file.exists()) {
String content;
if (password == null)
content = new String(readBytes(file));
else {
try {
content = Cryptos.decrypt(password, readBytes(file));
}
catch (DecryptException e) {
throw new WalletInvalidPassword(e);
}
}
try {
decode(content, walletAccounts);
}
catch (WalletDecodeException e) {
clear();
throw new WalletInvalidPassword(e);
}
fileExists = true;
}
initialized = true;
}
@Override
public List<IWalletAccount> getAccounts()
throws WalletNotInitializedException {
if (!initialized)
throw new WalletNotInitializedException();
return walletAccounts;
}
@Override
public IWalletStatus addAccount(IWalletAccount walletAccount)
throws WalletNotInitializedException, DuplicateAccountException,
WalletBackupException {
if (!initialized)
throw new WalletNotInitializedException();
if (walletAccounts.contains(walletAccount))
throw new DuplicateAccountException();
WalletStatus status = new WalletStatus(this);
if (fileExists) {
try {
File backup = createBackupFile();
backup(backup);
status.backupFile = backup;
}
catch (IOException ex) {
throw new WalletBackupException();
}
}
walletAccounts.add(walletAccount);
try {
save(file);
if (fileExists)
status.backupFile.deleteOnExit();
fileExists = true;
}
catch (WalletSaveException e) {
status.throwable = e;
status.status = IWalletStatus.FAILURE;
}
return status;
}
@Override
public IWalletStatus removeAccount(IWalletAccount walletAccount)
throws WalletNotInitializedException, AccountNotFoundException,
WalletBackupException {
if (!initialized)
throw new WalletNotInitializedException();
if (!walletAccounts.contains(walletAccount))
throw new AccountNotFoundException();
WalletStatus status = new WalletStatus(this);
if (fileExists) {
try {
File backup = createBackupFile();
backup(backup);
status.backupFile = backup;
}
catch (IOException ex) {
throw new WalletBackupException();
}
}
walletAccounts.remove(walletAccount);
try {
save(file);
if (fileExists)
status.backupFile.deleteOnExit();
fileExists = true;
}
catch (WalletSaveException e) {
status.throwable = e;
status.status = IWalletStatus.FAILURE;
}
return status;
}
private void backup(File backupFile) throws WalletBackupException {
try {
FileUtils.copyFile(file, backupFile);
if (!FileUtils.contentEquals(file, backupFile))
throw new WalletBackupException();
}
catch (IOException e) {
throw new WalletBackupException(e);
}
try {
synchronized (walletAccounts) {
verify(backupFile);
}
}
catch (WalletVerifyException e) {
throw new WalletBackupException(e);
}
}
private void save(File file) throws WalletSaveException {
String content;
try {
content = toJSONObject().toJSONString();
}
catch (WalletNotInitializedException e) {
throw new WalletSaveException(e);
}
byte[] bytes = null;
try {
bytes = content.getBytes(Config.offspring_charset);
}
catch (UnsupportedEncodingException e) {
logger.error("CHARACTER SET NOT SUPPORTED!!", e);
throw new WalletSaveException(e);
}
if (password != null) {
try {
bytes = Cryptos.encrypt(password, bytes);
}
catch (EncryptException e) {
throw new WalletSaveException(e);
}
}
if (!writeBytes(file, bytes))
throw new WalletSaveException();
try {
synchronized (walletAccounts) { // this doesnt make so much sense
verify(file);
}
}
catch (WalletVerifyException e) {
throw new WalletSaveException(e);
}
}
private void verify(File file) throws WalletVerifyException {
String content = null;
try {
content = readString(file);
}
catch (DecryptException e) {
throw new WalletVerifyException(e);
}
List<IWalletAccount> list = new ArrayList<IWalletAccount>();
try {
decode(content, list);
}
catch (WalletDecodeException e) {
throw new WalletVerifyException(e);
}
if (list.size() != walletAccounts.size())
throw new WalletVerifyException();
for (int i = 0; i < list.size(); i++) {
if (!walletAccounts.get(i).equals(list.get(i))) {
logger.warn("These Dont match");
logger.warn("(1) "
+ walletAccounts.get(i).toJSONObject().toJSONString());
logger.warn("(2) " + list.get(i).toJSONObject().toJSONString());
throw new WalletVerifyException();
}
}
}
private int generateRandomNumber(int low, int high) {
Random random = new Random();
return random.nextInt(high - low) + low;
}
private String generateRandomText() {
String symbols = "!$%^&*()-_=+[{]};:@#~|,<.>"; //$NON-NLS-1$
String alphaNum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890"; //$NON-NLS-1$
return RandomStringUtils.random(generateRandomNumber(70, 90), symbols
+ alphaNum);
}
@SuppressWarnings("unchecked")
public JSONObject toJSONObject() throws WalletNotInitializedException {
JSONObject obj = new JSONObject();
JSONArray array = new JSONArray();
obj.put(KEY_ACCOUNTS, array);
for (IWalletAccount walletAccount : getAccounts()) {
array.add(walletAccount.toJSONObject());
}
// TODO all padding is in a single block of the text file. this should be
// scattered all over the file.
/* add the padding under the "padding" key */
array = new JSONArray();
obj.put(KEY_PADDING, array);
int count = generateRandomNumber(30, 60);
for (int i = 0; i < count; i++) {
array.add(generateRandomText());
}
return obj;
}
@SuppressWarnings("rawtypes")
private void decode(String content, List<IWalletAccount> walletAccounts)
throws WalletDecodeException {
if (content == null || content.isEmpty())
throw new WalletDecodeException();
JSONObject obj = (JSONObject) JSONValue.parse(content);
if (obj == null)
throw new WalletDecodeException();
if (!(obj.get(KEY_ACCOUNTS) instanceof List))
throw new WalletDecodeException();
for (Object acc : (List) obj.get(KEY_ACCOUNTS)) {
if (!(acc instanceof JSONObject))
throw new WalletDecodeException();
try {
walletAccounts.add(NXTAccount.create((JSONObject) acc));
}
catch (IllegalArgumentException ex) {
throw new WalletDecodeException();
}
}
}
private File createBackupFile() throws IOException {
File backupdir = new File(file.getParentFile() + File.separator + "wallet");
if (!backupdir.exists())
backupdir.mkdir();
return File.createTempFile(file.getName(), "bak", backupdir);
}
private String readString(File file) throws DecryptException {
if (password == null)
return new String(readBytes(file));
return Cryptos.decrypt(password, readBytes(file));
}
private static byte[] readBytes(File file) {
FileInputStream in = null;
byte[] result = new byte[(int) file.length()];
try {
in = new FileInputStream(file);
in.read(result);
}
catch (Exception e) {
logger.error("Could not read FileInputStream", e);
}
finally {
try {
if (in != null)
in.close();
}
catch (Exception e) {
logger.error("Could not close FileInputStream", e);
}
}
return result;
}
private static boolean writeBytes(File file, byte[] contents) {
FileOutputStream out = null;
try {
out = new FileOutputStream(file);
if (!file.exists())
file.createNewFile();
out.write(contents);
out.flush();
}
catch (IOException e) {
logger.error("Could not write contents", e);
return false;
}
finally {
try {
if (out != null)
out.close();
}
catch (IOException e) {
logger.error("Could not close Outputstream", e);
return false;
}
}
return true;
}
@Override
public File getWalletFile() {
return file.getAbsoluteFile();
}
@Override
public void setWalletFile(File file) throws WalletInitializedException {
if (initialized)
throw new WalletInitializedException();
this.file = file;
}
@Override
public void clear() {
this.password = null;
this.file = null;
this.fileExists = false;
this.initialized = false;
this.walletAccounts.clear();
}
@Override
public void createWalletFile() throws WalletSaveException {
save(file);
}
}