tadd android authenticator script - electrum - Electrum Bitcoin wallet HTML git clone https://git.parazyd.org/electrum DIR Log DIR Files DIR Refs DIR Submodules --- DIR commit acc594e5d1a1a6c430dd0aa50d56bd1de6715958 DIR parent c872a3c4202082772f05056bcaf89ef32f8707e9 HTML Author: ThomasV <thomasv@gitorious> Date: Sun, 1 Mar 2015 13:27:18 +0100 add android authenticator script Diffstat: M contrib/make_android | 17 +++++++++-------- A scripts/authenticator.py | 357 +++++++++++++++++++++++++++++++ 2 files changed, 366 insertions(+), 8 deletions(-) --- DIR diff --git a/contrib/make_android b/contrib/make_android t@@ -14,17 +14,18 @@ if __name__ == '__main__': print "The packages directory is missing." sys.exit() - os.system('rm -rf dist/e4a-%s'%version) - os.mkdir('dist/e4a-%s'%version) - shutil.copyfile("electrum",'dist/e4a-%s/e4a.py'%version) + target = 'dist/e4a-%s'%version + os.system('rm -rf %s'%target) + os.mkdir(target) + shutil.copyfile('electrum', target + '/e4a.py') + shutil.copyfile('scripts/authenticator.py', target + '/authenticator.py') shutil.copytree("packages",'dist/e4a-%s/packages'%version, ignore=shutil.ignore_patterns('*.pyc')) shutil.copytree("lib",'dist/e4a-%s/lib'%version, ignore=shutil.ignore_patterns('*.pyc')) # dns is not used by android app - os.system('rm -rf dist/e4a-%s/packages/dns') - os.mkdir('dist/e4a-%s/gui'%version) - for n in ['android.py']: - shutil.copy("gui/%s"%n,'dist/e4a-%s/gui'%version) - open('dist/e4a-%s/gui/__init__.py'%version,'w').close() + os.system('rm -rf %s/packages/dns'%target) + os.mkdir(target + '/gui') + shutil.copyfile('gui/android.py', target + '/gui/android.py') + open(target + '/gui/__init__.py','w').close() os.chdir("dist") # create the zip file DIR diff --git a/scripts/authenticator.py b/scripts/authenticator.py t@@ -0,0 +1,357 @@ +#!/usr/bin/env python +# +# Electrum - lightweight Bitcoin client +# Copyright (C) 2015 Thomas Voegtlin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + + + + +from __future__ import absolute_import + +import android +import sys +import os +import imp +import base64 + +script_dir = os.path.dirname(os.path.realpath(__file__)) +sys.path.insert(0, os.path.join(script_dir, 'packages')) + +import qrcode + +imp.load_module('electrum', *imp.find_module('lib')) + +from electrum import SimpleConfig, Wallet, WalletStorage, format_satoshis +from electrum import util +from electrum.transaction import Transaction +from electrum.bitcoin import base_encode, base_decode + +def modal_dialog(title, msg = None): + droid.dialogCreateAlert(title,msg) + droid.dialogSetPositiveButtonText('OK') + droid.dialogShow() + droid.dialogGetResponse() + droid.dialogDismiss() + +def modal_input(title, msg, value = None, etype=None): + droid.dialogCreateInput(title, msg, value, etype) + droid.dialogSetPositiveButtonText('OK') + droid.dialogSetNegativeButtonText('Cancel') + droid.dialogShow() + response = droid.dialogGetResponse() + result = response.result + droid.dialogDismiss() + + if result is None: + return modal_input(title, msg, value, etype) + + if result.get('which') == 'positive': + return result.get('value') + +def modal_question(q, msg, pos_text = 'OK', neg_text = 'Cancel'): + droid.dialogCreateAlert(q, msg) + droid.dialogSetPositiveButtonText(pos_text) + droid.dialogSetNegativeButtonText(neg_text) + droid.dialogShow() + response = droid.dialogGetResponse() + result = response.result + droid.dialogDismiss() + + if result is None: + return modal_question(q, msg, pos_text, neg_text) + + return result.get('which') == 'positive' + + + + + +def make_layout(s): + content = """ + + <LinearLayout + android:id="@+id/zz" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="#ff222222"> + + <TextView + android:id="@+id/textElectrum" + android:text="Electrum Authenticator" + android:textSize="7pt" + android:textColor="#ff4444ff" + android:gravity="left" + android:layout_height="wrap_content" + android:layout_width="match_parent" + /> + </LinearLayout> + + %s """%s + + + return """<?xml version="1.0" encoding="utf-8"?> + <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/background" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="#ff000022"> + + %s + </LinearLayout>"""%content + + + + + + +def qr_layout(title): + title_view= """ + <TextView android:id="@+id/addrTextView" + android:layout_width="match_parent" + android:layout_height="50" + android:text="%s" + android:textAppearance="?android:attr/textAppearanceLarge" + android:gravity="center_vertical|center_horizontal|center"> + </TextView>"""%title + + image_view=""" + <ImageView + android:id="@+id/qrView" + android:gravity="center" + android:layout_width="match_parent" + android:antialias="false" + android:src="" + /> + """ + return make_layout(title_view + image_view) + + + + + + + + + + +def add_menu(): + droid.clearOptionsMenu() + droid.addOptionsMenuItem("Seed", "seed", None,"") + droid.addOptionsMenuItem("Public Key", "xpub", None,"") + droid.addOptionsMenuItem("Transaction", "scan", None,"") + droid.addOptionsMenuItem("Password", "password", None,"") + + + +def make_bitmap(data): + # fixme: this is highly inefficient + import qrcode + from electrum import bmp + qr = qrcode.QRCode() + qr.add_data(data) + bmp.save_qrcode(qr,"/sdcard/sl4a/qrcode.bmp") + + +droid = android.Android() +wallet = None + +class Authenticator: + + def __init__(self): + global wallet + self.qr_data = None + storage = WalletStorage({'wallet_path':'/sdcard/electrum/authenticator'}) + if not storage.file_exists: + + action = self.restore_or_create() + if not action: + exit() + password = droid.dialogGetPassword('Choose a password').result + if password: + password2 = droid.dialogGetPassword('Confirm password').result + if password != password2: + modal_dialog('Error', 'Passwords do not match') + exit() + else: + password = None + if action == 'create': + wallet = Wallet(storage) + seed = wallet.make_seed() + modal_dialog('Your seed is:', seed) + elif action == 'import': + seed = self.seed_dialog() + if not seed: + exit() + if not Wallet.is_seed(seed): + exit() + wallet = Wallet.from_seed(seed, storage) + else: + exit() + + wallet.add_seed(seed, password) + wallet.create_master_keys(password) + wallet.create_main_account(password) + else: + wallet = Wallet(storage) + + def restore_or_create(self): + droid.dialogCreateAlert("Seed not found", "Do you want to create a new seed, or to import it?") + droid.dialogSetPositiveButtonText('Create') + droid.dialogSetNeutralButtonText('Import') + droid.dialogSetNegativeButtonText('Cancel') + droid.dialogShow() + response = droid.dialogGetResponse().result + droid.dialogDismiss() + if not response: return + if response.get('which') == 'negative': + return + return 'import' if response.get('which') == 'neutral' else 'create' + + def seed_dialog(self): + if modal_question("Enter your seed", "Input method", 'QR Code', 'mnemonic'): + code = droid.scanBarcode() + r = code.result + if r: + seed = r['extras']['SCAN_RESULT'] + else: + return + else: + seed = modal_input('Mnemonic', 'Please enter your seed phrase') + return str(seed) + + def show_qr(self, data): + path = "/sdcard/sl4a/qrcode.bmp" + if data: + droid.dialogCreateSpinnerProgress("please wait") + droid.dialogShow() + try: + make_bitmap(data) + finally: + droid.dialogDismiss() + else: + with open(path, 'w') as f: f.write('') + droid.fullSetProperty("qrView", "src", 'file://'+path) + self.qr_data = data + + def show_title(self, title): + droid.fullSetProperty("addrTextView","text", title) + + def get_password(self): + if wallet.use_encryption: + password = droid.dialogGetPassword('Password').result + try: + wallet.check_password(password) + except: + return False + return password + + def main(self): + add_menu() + welcome = 'Use the menu to scan a transaction.' + droid.fullShow(qr_layout(welcome)) + while True: + event = droid.eventWait().result + if not event: + continue + elif event["name"] == "key": + if event["data"]["key"] == '4': + if self.qr_data: + self.show_qr(None) + self.show_title(welcome) + else: + break + + elif event["name"] == "seed": + password = self.get_password() + if password is False: + modal_dialog('Error','incorrect password') + continue + seed = wallet.get_mnemonic(password) + modal_dialog('Your seed is', seed) + + elif event["name"] == "password": + self.change_password_dialog() + + elif event["name"] == "xpub": + mpk = wallet.get_master_public_key() + self.show_qr(mpk) + self.show_title('master public key') + + elif event["name"] == "scan": + r = droid.scanBarcode() + r = r.result + if not r: + continue + data = r['extras']['SCAN_RESULT'] + data = base_decode(data.encode('utf8'), None, base=43) + data = ''.join(chr(ord(b)) for b in data).encode('hex') + tx = Transaction.deserialize(data) + #except: + # modal_dialog('Error', 'Cannot parse transaction') + # continue + if not wallet.can_sign(tx): + modal_dialog('Error', 'Cannot sign this transaction') + continue + lines = map(lambda x: x[0] + u'\t\t' + format_satoshis(x[1]) if x[1] else x[0], tx.get_outputs()) + if not modal_question('Sign?', '\n'.join(lines)): + continue + password = self.get_password() + if password is False: + modal_dialog('Error','incorrect password') + continue + droid.dialogCreateSpinnerProgress("Signing") + droid.dialogShow() + wallet.sign_transaction(tx, password) + droid.dialogDismiss() + data = base_encode(str(tx).decode('hex'), base=43) + self.show_qr(data) + self.show_title('Signed Transaction') + + droid.makeToast("Bye!") + + + def change_password_dialog(self): + if wallet.use_encryption: + password = droid.dialogGetPassword('Your seed is encrypted').result + if password is None: + return + else: + password = None + try: + wallet.check_password(password) + except Exception: + modal_dialog('Error', 'Incorrect password') + return + new_password = droid.dialogGetPassword('Choose a password').result + if new_password == None: + return + if new_password != '': + password2 = droid.dialogGetPassword('Confirm new password').result + if new_password != password2: + modal_dialog('Error', 'passwords do not match') + return + wallet.update_password(password, new_password) + if new_password: + modal_dialog('Password updated', 'Your seed is encrypted') + else: + modal_dialog('No password', 'Your seed is not encrypted') + + + +if __name__ == "__main__": + a = Authenticator() + a.main()