package cy.agorise.crystalwallet.manager; import android.os.Environment; import com.google.common.primitives.Bytes; import com.google.gson.GsonBuilder; import org.bitcoinj.core.ECKey; import org.tukaani.xz.CorruptedInputException; import org.tukaani.xz.LZMAInputStream; import org.tukaani.xz.XZInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.EOFException; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import cy.agorise.crystalwallet.dao.AccountSeedDao; import cy.agorise.crystalwallet.dao.CrystalDatabase; import cy.agorise.crystalwallet.enums.CryptoNet; import cy.agorise.crystalwallet.enums.SeedType; import cy.agorise.crystalwallet.models.AccountSeed; import cy.agorise.crystalwallet.models.CryptoNetAccount; import cy.agorise.crystalwallet.models.GrapheneAccount; import cy.agorise.crystalwallet.network.CryptoNetManager; import cy.agorise.crystalwallet.requestmanagers.CreateBackupRequest; import cy.agorise.crystalwallet.requestmanagers.CryptoNetInfoRequestListener; import cy.agorise.crystalwallet.requestmanagers.CryptoNetInfoRequests; import cy.agorise.crystalwallet.requestmanagers.FileServiceRequest; import cy.agorise.crystalwallet.requestmanagers.FileServiceRequestsListener; import cy.agorise.crystalwallet.requestmanagers.ImportBackupRequest; import cy.agorise.crystalwallet.requestmanagers.ValidateImportBitsharesAccountRequest; import cy.agorise.graphenej.Address; import cy.agorise.graphenej.BrainKey; import cy.agorise.graphenej.FileBin; import cy.agorise.graphenej.Util; import cy.agorise.graphenej.api.GetKeyReferences; import cy.agorise.graphenej.models.backup.LinkedAccount; import cy.agorise.graphenej.models.backup.PrivateKeyBackup; import cy.agorise.graphenej.models.backup.Wallet; import cy.agorise.graphenej.models.backup.WalletBackup; /** * Created by henry on 21/3/2018. */ public class FileBackupManager implements FileServiceRequestsListener { @Override public void onNewRequest(FileServiceRequest request) { if(request instanceof CreateBackupRequest){ createBackupBinFile((CreateBackupRequest) request); } else if (request instanceof ImportBackupRequest){ readBinFile((ImportBackupRequest) request); } } private void createBackupBinFile(CreateBackupRequest request) { CrystalDatabase db = CrystalDatabase.getAppDatabase(request.getContext()); List seedNames = new ArrayList<>(); List seeds = db.accountSeedDao().getAllNoLiveData(); for(AccountSeed seed : seeds) { List accounts = db.cryptoNetAccountDao().getAllCryptoNetAccountBySeed(seed.getId()); for(CryptoNetAccount account : accounts){ if(account.getCryptoNet().equals(CryptoNet.BITSHARES)){ seedNames.add(new BitsharesSeedName(account.getName(), seed.getMasterSeed())); } } } getBinBytesFromBrainkey(seedNames,request); //TODO make funcion for non-bitshares accounts } private void getBinBytesFromBrainkey(List bitsharesSeedNames, CreateBackupRequest request) { try { ArrayList wallets = new ArrayList<>(); ArrayList accounts = new ArrayList<>(); ArrayList keys = new ArrayList<>(); String fileName = null; //TODO choice a good name, now we use the first bitshares account as the bin backup for(BitsharesSeedName bitsharesSeedName : bitsharesSeedNames) { if(fileName == null){ fileName = bitsharesSeedName.accountName; } int sequence = 0; //TODO adapt CHAIN ID Wallet wallet = new Wallet(bitsharesSeedName.accountName, bitsharesSeedName.accountSeed, sequence, CryptoNetManager.getChaindId(CryptoNet.BITSHARES), request.getPassword()); wallets.add(wallet); BrainKey brainKey = new BrainKey(bitsharesSeedName.accountSeed, sequence); //TODO chain to use BIP39 PrivateKeyBackup keyBackup = new PrivateKeyBackup(brainKey.getPrivateKey().getPrivKeyBytes(), sequence, sequence, wallet.getEncryptionKey(request.getPassword())); keys.add(keyBackup); LinkedAccount linkedAccount = new LinkedAccount(bitsharesSeedName.accountName, CryptoNetManager.getChaindId(CryptoNet.BITSHARES)); accounts.add(linkedAccount); } WalletBackup backup = new WalletBackup(wallets, keys, accounts); byte[] results = FileBin.serializeWalletBackup(backup, request.getPassword()); List resultFile = new ArrayList<>(); for(byte result: results){ resultFile.add(result & 0xff); } saveBinContentToFile(resultFile, fileName, request); } catch (Exception e) { e.printStackTrace(); request.setStatus(CreateBackupRequest.StatusCode.FAILED); //TODO error exception } } private void saveBinContentToFile(List content, String fileName, CreateBackupRequest request ) { SimpleDateFormat df = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss"); String dateHourString = df.format(new Date()); String folder = Environment.getExternalStorageDirectory() + File.separator + "Crystal"; //TODO make constant String path = folder + File.separator + fileName + dateHourString +".bin"; File folderFile = new File(folder); if (!folderFile.exists()) { if(folderFile.mkdirs()){ System.out.println("folder created"); }else{ System.out.println("couldn't create folder"); request.setStatus(CreateBackupRequest.StatusCode.FAILED); return; } } boolean success = saveBinFile(path,content,request); if(success) { request.setFilePath(path); request.setStatus(CreateBackupRequest.StatusCode.SUCCEEDED); }else{ request.setStatus(CreateBackupRequest.StatusCode.FAILED); } } private static boolean saveBinFile (String filePath , List content, CreateBackupRequest request) { boolean success = false; try { //TODO permissions // PermissionManager Manager = new PermissionManager(); // Manager.verifyStoragePermissions(_activity); File file = new File(filePath); byte[] fileData = new byte[content.size()]; BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file)); for ( int i = 0 ; i < content.size() ; i++ ) { fileData[i] = content.get(i).byteValue(); } bos.write(fileData); bos.flush(); bos.close(); success = true; } catch (Exception e) { e.printStackTrace(); //TODO handle error } return success; } private void readBinFile(final ImportBackupRequest request){ try { File file = new File(request.getFilePath()); DataInputStream dis = new DataInputStream(new FileInputStream(file)); ArrayList readBytes = new ArrayList<>(); for ( int i = 0 ; i < file.length() ; i++ ) { int val = unsignedToBytes(dis.readByte()); readBytes.add(val); } dis.close(); byte[] byteArray = new byte[readBytes.size()]; for(int i = 0 ; i < readBytes.size();i++){ byteArray[i] = readBytes.get(i).byteValue(); } WalletBackup walletBackup = deserializeWalletBackup(byteArray,request.getPassword()); if(walletBackup == null){ //TODO handle error System.out.println("FileBackupManager error walletBackup null"); request.setStatus(ImportBackupRequest.StatusCode.FAILED); return; } List seedNames = new ArrayList<>(); for(int i = 0; i < walletBackup.getKeyCount(); i++){ String brainKey = walletBackup.getWallet(i).decryptBrainKey(request.getPassword()); int sequence = walletBackup.getWallet(i).getBrainkeySequence(); String accountName = walletBackup.getWallet(i).getPrivateName(); seedNames.add(new BitsharesSeedName(accountName,brainKey)); } //TODO handle more than one account final CrystalDatabase db = CrystalDatabase.getAppDatabase(request.getContext()); final AccountSeedDao accountSeedDao = db.accountSeedDao(); for(BitsharesSeedName seedName : seedNames) { final ValidateImportBitsharesAccountRequest validatorRequest = new ValidateImportBitsharesAccountRequest(seedName.accountName, seedName.accountSeed, request.getContext(), true); validatorRequest.setListener(new CryptoNetInfoRequestListener() { @Override public void onCarryOut() { if (!validatorRequest.getStatus().equals(ValidateImportBitsharesAccountRequest.StatusCode.SUCCEEDED)) { request.setStatus(ImportBackupRequest.StatusCode.FAILED); // TODO reason bad seed } else { /*AccountSeed seed = new AccountSeed(); seed.setName(validatorRequest.getAccountName()); seed.setType(validatorRequest.getSeedType()); seed.setMasterSeed(validatorRequest.getMnemonic()); long idSeed = accountSeedDao.insertAccountSeed(seed); if(idSeed >= 0) { GrapheneAccount account = new GrapheneAccount(); account.setCryptoNet(CryptoNet.BITSHARES); account.setAccountIndex(0); account.setSeedId(idSeed); account.setName(validatorRequest.getAccountName()); BitsharesAccountManager bManger = new BitsharesAccountManager(); bManger.importAccountFromSeed(account,request.getContext()); request.setStatus(ImportBackupRequest.StatusCode.SUCCEEDED); }else{ request.setStatus(ImportBackupRequest.StatusCode.FAILED); //TODO reason couldn't insert seed }*/ request.setStatus(ImportBackupRequest.StatusCode.SUCCEEDED); } } }); CryptoNetInfoRequests.getInstance().addRequest(validatorRequest); } } catch (Exception e) { e.printStackTrace(); request.setStatus(ImportBackupRequest.StatusCode.FAILED); //TODO handle exception } } private int unsignedToBytes(byte b) { return b & 0xFF; } public class BitsharesSeedName{ String accountName; String accountSeed; public BitsharesSeedName(String accountName, String accountSeed) { this.accountName = accountName; this.accountSeed = accountSeed; } } /** * This part is copied from the graphenej library, to edit possible error. * TODO in graphenej library to catch EOFException reading files */ public static WalletBackup deserializeWalletBackup(byte[] input, String password){ try{ byte[] publicKey = new byte[33]; byte[] rawDataEncripted = new byte[input.length - 33]; System.arraycopy(input, 0, publicKey, 0, 33); System.arraycopy(input, 33, rawDataEncripted, 0, rawDataEncripted.length); MessageDigest md = MessageDigest.getInstance("SHA-256"); ECKey randomECKey = ECKey.fromPublicOnly(publicKey); byte[] finalKey = randomECKey.getPubKeyPoint().multiply(ECKey.fromPrivate(md.digest(password.getBytes("UTF-8"))).getPrivKey()).normalize().getXCoord().getEncoded(); MessageDigest md1 = MessageDigest.getInstance("SHA-512"); finalKey = md1.digest(finalKey); byte[] decryptedData = Util.decryptAES(rawDataEncripted, Util.bytesToHex(finalKey).getBytes()); byte[] checksum = new byte[4]; System.arraycopy(decryptedData, 0, checksum, 0, 4); byte[] compressedData = new byte[decryptedData.length - 4]; System.arraycopy(decryptedData, 4, compressedData, 0, compressedData.length); byte[] decompressedData = decompress(compressedData, Util.LZMA); String walletString = new String(decompressedData, "UTF-8"); System.out.println("Wallet str: "+walletString); return new GsonBuilder().create().fromJson(walletString, WalletBackup.class); }catch(NoSuchAlgorithmException e){ System.out.println("NoSuchAlgorithmException. Msg: "+e.getMessage()); } catch (UnsupportedEncodingException e) { System.out.println("UnsupportedEncodingException. Msg: "+e.getMessage()); } return null; } public static byte[] decompress(byte[] inputBytes, int which) { InputStream in = null; try { ByteArrayInputStream input = new ByteArrayInputStream(inputBytes); ByteArrayOutputStream output = new ByteArrayOutputStream(16*2048); if(which == Util.XZ) { in = new XZInputStream(input); }else if(which == Util.LZMA){ in = new LZMAInputStream(input); } int size; try{ while ((size = in.read()) != -1) { output.write(size); } }catch(CorruptedInputException e){ // Taking property byte byte[] properties = Arrays.copyOfRange(inputBytes, 0, 1); // Taking dict size bytes byte[] dictSize = Arrays.copyOfRange(inputBytes, 1, 5); // Taking uncompressed size bytes byte[] uncompressedSize = Arrays.copyOfRange(inputBytes, 5, 13); // Reversing bytes in header byte[] header = Bytes.concat(properties, Util.revertBytes(dictSize), Util.revertBytes(uncompressedSize)); byte[] payload = Arrays.copyOfRange(inputBytes, 13, inputBytes.length); // Trying again input = new ByteArrayInputStream(Bytes.concat(header, payload)); output = new ByteArrayOutputStream(2048); if(which == Util.XZ) { in = new XZInputStream(input); }else if(which == Util.LZMA){ in = new LZMAInputStream(input); } try{ while ((size = in.read()) != -1) { output.write(size); } }catch(CorruptedInputException ex){ System.out.println("CorruptedInputException. Msg: "+ex.getMessage()); } }catch(EOFException e){ } in.close(); return output.toByteArray(); } catch (IOException ex) { Logger.getLogger(Util.class.getName()).log(Level.SEVERE, null, ex); } return null; } }