diff --git a/bin/pwnagotchi b/bin/pwnagotchi index 56300af1..f72d84c1 100755 --- a/bin/pwnagotchi +++ b/bin/pwnagotchi @@ -10,6 +10,7 @@ if __name__ == '__main__': import pwnagotchi.plugins as plugins from pwnagotchi.log import SessionParser + from pwnagotchi.identity import KeyPair from pwnagotchi.agent import Agent from pwnagotchi.ui.display import Display @@ -34,10 +35,11 @@ if __name__ == '__main__': plugins.load(config) + keypair = KeyPair() display = Display(config=config, state={'name': '%s>' % pwnagotchi.name()}) - agent = Agent(view=display, config=config) + agent = Agent(view=display, config=config, keypair=keypair) - logging.info("%s@%s (v%s)" % (pwnagotchi.name(), agent._identity, pwnagotchi.version)) + logging.info("%s@%s (v%s)" % (pwnagotchi.name(), agent._keypair.fingerprint, pwnagotchi.version)) for _, plugin in plugins.loaded.items(): logging.debug("plugin '%s' v%s loaded from %s" % (plugin.__name__, plugin.__version__, plugin.__file__)) @@ -64,7 +66,7 @@ if __name__ == '__main__': time.sleep(1) if Agent.is_connected(): - plugins.on('internet_available', display, config, log) + plugins.on('internet_available', display, keypair, config, log) else: logging.info("entering auto mode ...") diff --git a/pwnagotchi/agent.py b/pwnagotchi/agent.py index 98f8aabe..0e897632 100644 --- a/pwnagotchi/agent.py +++ b/pwnagotchi/agent.py @@ -17,13 +17,13 @@ RECOVERY_DATA_FILE = '/root/.pwnagotchi-recovery' class Agent(Client, AsyncAdvertiser, AsyncTrainer): - def __init__(self, view, config): + def __init__(self, view, config, keypair): Client.__init__(self, config['bettercap']['hostname'], config['bettercap']['scheme'], config['bettercap']['port'], config['bettercap']['username'], config['bettercap']['password']) - AsyncAdvertiser.__init__(self, config, view) + AsyncAdvertiser.__init__(self, config, view, keypair) AsyncTrainer.__init__(self, config) self._started_at = time.time() diff --git a/pwnagotchi/defaults.yml b/pwnagotchi/defaults.yml index 813b97fb..dff6b3bf 100644 --- a/pwnagotchi/defaults.yml +++ b/pwnagotchi/defaults.yml @@ -6,6 +6,8 @@ main: custom_plugins: # which plugins to load and enable plugins: + api: + enabled: false auto-update: enabled: false interval: 1 # every day diff --git a/pwnagotchi/identity.py b/pwnagotchi/identity.py new file mode 100644 index 00000000..ae12490c --- /dev/null +++ b/pwnagotchi/identity.py @@ -0,0 +1,47 @@ +from Crypto.Signature import PKCS1_PSS +from Crypto.PublicKey import RSA +import Crypto.Hash.SHA256 as SHA256 +import base64 +import hashlib +import os +import logging + +DefaultPath = "/etc/pwnagotchi/" + + +class KeyPair(object): + def __init__(self, path=DefaultPath): + self.path = path + self.priv_path = os.path.join(path, "id_rsa") + self.priv_key = None + self.pub_path = "%s.pub" % self.priv_path + self.pub_key = None + + if not os.path.exists(self.path): + os.makedirs(self.path) + + if not os.path.exists(self.priv_path) or not os.path.exists(self.pub_path): + logging.info("generating %s ..." % self.priv_path) + os.system("/usr/bin/ssh-keygen -t rsa -m PEM -b 4096 -N '' -f '%s'" % self.priv_path) + + with open(self.priv_path) as fp: + self.priv_key = RSA.importKey(fp.read()) + + with open(self.pub_path) as fp: + self.pub_key = RSA.importKey(fp.read()) + self.pub_key_pem = self.pub_key.exportKey('PEM').decode("ascii") + # python is special + if 'RSA PUBLIC KEY' not in self.pub_key_pem: + self.pub_key_pem = self.pub_key_pem.replace('PUBLIC KEY', 'RSA PUBLIC KEY') + + pem = self.pub_key_pem.encode("ascii") + + self.pub_key_pem_b64 = base64.b64encode(pem).decode("ascii") + self.fingerprint = hashlib.sha256(pem).hexdigest() + + def sign(self, message): + hasher = SHA256.new(message.encode("ascii")) + signer = PKCS1_PSS.new(self.priv_key, saltLen=16) + signature = signer.sign(hasher) + signature_b64 = base64.b64encode(signature).decode("ascii") + return signature, signature_b64 \ No newline at end of file diff --git a/pwnagotchi/mesh/__init__.py b/pwnagotchi/mesh/__init__.py index 9e68dc81..e1c033fe 100644 --- a/pwnagotchi/mesh/__init__.py +++ b/pwnagotchi/mesh/__init__.py @@ -1,14 +1,4 @@ import os -from Crypto.PublicKey import RSA -import hashlib - def new_session_id(): return ':'.join(['%02x' % b for b in os.urandom(6)]) - - -def get_identity(config): - pubkey = None - with open(config['main']['pubkey']) as fp: - pubkey = RSA.importKey(fp.read()) - return pubkey, hashlib.sha256(pubkey.exportKey('DER')).hexdigest() diff --git a/pwnagotchi/mesh/utils.py b/pwnagotchi/mesh/utils.py index 3b63d092..a775ad40 100644 --- a/pwnagotchi/mesh/utils.py +++ b/pwnagotchi/mesh/utils.py @@ -3,14 +3,13 @@ import logging import pwnagotchi import pwnagotchi.plugins as plugins -from pwnagotchi.mesh import get_identity class AsyncAdvertiser(object): - def __init__(self, config, view): + def __init__(self, config, view, keypair): self._config = config self._view = view - self._public_key, self._identity = get_identity(config) + self._keypair = keypair self._advertiser = None def start_advertising(self): @@ -24,7 +23,7 @@ class AsyncAdvertiser(object): self._config['main']['iface'], pwnagotchi.name(), pwnagotchi.version, - self._identity, + self._keypair.fingerprint, period=0.3, data=self._config['personality']) diff --git a/pwnagotchi/plugins/default/api.py b/pwnagotchi/plugins/default/api.py new file mode 100644 index 00000000..afd882c5 --- /dev/null +++ b/pwnagotchi/plugins/default/api.py @@ -0,0 +1,51 @@ +__author__ = 'evilsocket@gmail.com' +__version__ = '1.0.0' +__name__ = 'api' +__license__ = 'GPL3' +__description__ = 'This plugin signals the unit cryptographic identity to api.pwnagotchi.ai' + +import logging +import json +import requests +import pwnagotchi +from pwnagotchi.utils import StatusFile + +OPTIONS = dict() +READY = False +STATUS = StatusFile('/root/.api-enrollment.json') + + +def on_loaded(): + logging.info("api plugin loaded.") + + +def on_internet_available(ui, keypair, config, log): + global STATUS + + if STATUS.newer_then_minutes(10): + return + + try: + logging.info("api: signign enrollment request ...") + identity = "%s@%s" % (pwnagotchi.name(), keypair.fingerprint) + _, signature_b64 = keypair.sign(identity) + + api_address = 'https://api.pwnagotchi.ai/api/v1/unit/enroll' + enroll = { + 'identity': identity, + 'public_key': keypair.pub_key_pem_b64, + 'signature': signature_b64 + } + + logging.info("api: enrolling unit to %s ..." % api_address) + + r = requests.post(api_address, json=enroll) + if r.status_code == 200: + token = r.json() + logging.info("api: enrolled") + STATUS.update(data=json.dumps(token)) + else: + logging.error("error %d: %s" % (r.status_code, r.json())) + + except Exception as e: + logging.exception("error while enrolling the unit") diff --git a/pwnagotchi/plugins/default/auto-backup.py b/pwnagotchi/plugins/default/auto-backup.py index 731a40ff..48d85976 100644 --- a/pwnagotchi/plugins/default/auto-backup.py +++ b/pwnagotchi/plugins/default/auto-backup.py @@ -33,7 +33,7 @@ def on_loaded(): logging.info("AUTO-BACKUP: Successfuly loaded.") -def on_internet_available(display, config, log): +def on_internet_available(display, keypair, config, log): global STATUS if READY: diff --git a/pwnagotchi/plugins/default/auto-update.py b/pwnagotchi/plugins/default/auto-update.py index 4a1f1352..dab44b3c 100644 --- a/pwnagotchi/plugins/default/auto-update.py +++ b/pwnagotchi/plugins/default/auto-update.py @@ -23,7 +23,7 @@ def on_loaded(): READY = True -def on_internet_available(display, config, log): +def on_internet_available(display, keypair, config, log): global STATUS if READY: diff --git a/pwnagotchi/plugins/default/example.py b/pwnagotchi/plugins/default/example.py index c18177c2..2a82a18d 100644 --- a/pwnagotchi/plugins/default/example.py +++ b/pwnagotchi/plugins/default/example.py @@ -20,7 +20,7 @@ def on_loaded(): # called in manual mode when there's internet connectivity -def on_internet_available(ui, config, log): +def on_internet_available(ui, keypair, config, log): pass diff --git a/pwnagotchi/plugins/default/onlinehashcrack.py b/pwnagotchi/plugins/default/onlinehashcrack.py index 2319d967..9771b885 100644 --- a/pwnagotchi/plugins/default/onlinehashcrack.py +++ b/pwnagotchi/plugins/default/onlinehashcrack.py @@ -55,7 +55,7 @@ def _upload_to_ohc(path, timeout=30): raise e -def on_internet_available(display, config, log): +def on_internet_available(display, keypair, config, log): """ Called in manual mode when there's internet connectivity """ diff --git a/pwnagotchi/plugins/default/twitter.py b/pwnagotchi/plugins/default/twitter.py index 560903a0..8f21f256 100644 --- a/pwnagotchi/plugins/default/twitter.py +++ b/pwnagotchi/plugins/default/twitter.py @@ -14,7 +14,7 @@ def on_loaded(): # called in manual mode when there's internet connectivity -def on_internet_available(ui, config, log): +def on_internet_available(ui, keypair, config, log): if log.is_new() and log.handshakes > 0: try: import tweepy diff --git a/pwnagotchi/plugins/default/wigle.py b/pwnagotchi/plugins/default/wigle.py index cba5745b..da57f947 100644 --- a/pwnagotchi/plugins/default/wigle.py +++ b/pwnagotchi/plugins/default/wigle.py @@ -12,7 +12,6 @@ import csv from datetime import datetime import requests from pwnagotchi.mesh.wifi import freq_to_channel -from scapy.all import RadioTap, Dot11Elt, Dot11Beacon, rdpcap, Scapy_Exception, Dot11, Dot11ProbeResp, Dot11AssoReq, Dot11ReassoReq, Dot11EltRSN, Dot11EltVendorSpecific, Dot11EltMicrosoftWPA READY = False ALREADY_UPLOADED = None @@ -26,6 +25,8 @@ AKMSUITE_TYPES = { } def _handle_packet(packet, result): + from scapy.all import RadioTap, Dot11Elt, Dot11Beacon, rdpcap, Scapy_Exception, Dot11, Dot11ProbeResp, Dot11AssoReq, \ + Dot11ReassoReq, Dot11EltRSN, Dot11EltVendorSpecific, Dot11EltMicrosoftWPA """ Analyze each packet and extract the data from Dot11 layers """ @@ -76,6 +77,8 @@ def _handle_packet(packet, result): def _analyze_pcap(pcap): + from scapy.all import RadioTap, Dot11Elt, Dot11Beacon, rdpcap, Scapy_Exception, Dot11, Dot11ProbeResp, Dot11AssoReq, \ + Dot11ReassoReq, Dot11EltRSN, Dot11EltVendorSpecific, Dot11EltMicrosoftWPA """ Iterate over the packets and extract data """ @@ -192,7 +195,9 @@ def _send_to_wigle(lines, api_key, timeout=30): raise re_e -def on_internet_available(display, config, log): +def on_internet_available(display, keypair, config, log): + from scapy.all import RadioTap, Dot11Elt, Dot11Beacon, rdpcap, Scapy_Exception, Dot11, Dot11ProbeResp, Dot11AssoReq, \ + Dot11ReassoReq, Dot11EltRSN, Dot11EltVendorSpecific, Dot11EltMicrosoftWPA """ Called in manual mode when there's internet connectivity """ diff --git a/pwnagotchi/plugins/default/wpa-sec.py b/pwnagotchi/plugins/default/wpa-sec.py index cfe35da9..7d61b7b6 100644 --- a/pwnagotchi/plugins/default/wpa-sec.py +++ b/pwnagotchi/plugins/default/wpa-sec.py @@ -54,7 +54,7 @@ def _upload_to_wpasec(path, timeout=30): raise e -def on_internet_available(display, config, log): +def on_internet_available(display, keypair, config, log): """ Called in manual mode when there's internet connectivity """ diff --git a/pwnagotchi/utils.py b/pwnagotchi/utils.py index d1b2ba0d..28346641 100644 --- a/pwnagotchi/utils.py +++ b/pwnagotchi/utils.py @@ -89,6 +89,9 @@ class StatusFile(object): if os.path.exists(path): self._updated = datetime.fromtimestamp(os.path.getmtime(path)) + def newer_then_minutes(self, minutes): + return self._updated is not None and ((datetime.now() - self._updated).seconds / 60) < minutes + def newer_then_days(self, days): return self._updated is not None and (datetime.now() - self._updated).days < days diff --git a/setup.py b/setup.py index 98ee45cd..f8e22b31 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +# -*- coding: utf-8 -*- from setuptools import setup, find_packages import pwnagotchi