commit acc594e5d1a1a6c430dd0aa50d56bd1de6715958
parent c872a3c4202082772f05056bcaf89ef32f8707e9
Author: ThomasV <thomasv@gitorious>
Date: Sun, 1 Mar 2015 13:27:18 +0100
add android authenticator script
Diffstat:
2 files changed, 366 insertions(+), 8 deletions(-)
diff --git a/contrib/make_android b/contrib/make_android
@@ -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
diff --git a/scripts/authenticator.py b/scripts/authenticator.py
@@ -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()