new: fixed rsa identity generation and implemented api enrollment plugin

This commit is contained in:
Simone Margaritelli 2019-10-06 23:25:02 +02:00
parent 107eb57f26
commit 1c251fc093
16 changed files with 127 additions and 27 deletions

View File

@ -10,6 +10,7 @@ if __name__ == '__main__':
import pwnagotchi.plugins as plugins import pwnagotchi.plugins as plugins
from pwnagotchi.log import SessionParser from pwnagotchi.log import SessionParser
from pwnagotchi.identity import KeyPair
from pwnagotchi.agent import Agent from pwnagotchi.agent import Agent
from pwnagotchi.ui.display import Display from pwnagotchi.ui.display import Display
@ -34,10 +35,11 @@ if __name__ == '__main__':
plugins.load(config) plugins.load(config)
keypair = KeyPair()
display = Display(config=config, state={'name': '%s>' % pwnagotchi.name()}) 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(): for _, plugin in plugins.loaded.items():
logging.debug("plugin '%s' v%s loaded from %s" % (plugin.__name__, plugin.__version__, plugin.__file__)) 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) time.sleep(1)
if Agent.is_connected(): if Agent.is_connected():
plugins.on('internet_available', display, config, log) plugins.on('internet_available', display, keypair, config, log)
else: else:
logging.info("entering auto mode ...") logging.info("entering auto mode ...")

View File

@ -17,13 +17,13 @@ RECOVERY_DATA_FILE = '/root/.pwnagotchi-recovery'
class Agent(Client, AsyncAdvertiser, AsyncTrainer): class Agent(Client, AsyncAdvertiser, AsyncTrainer):
def __init__(self, view, config): def __init__(self, view, config, keypair):
Client.__init__(self, config['bettercap']['hostname'], Client.__init__(self, config['bettercap']['hostname'],
config['bettercap']['scheme'], config['bettercap']['scheme'],
config['bettercap']['port'], config['bettercap']['port'],
config['bettercap']['username'], config['bettercap']['username'],
config['bettercap']['password']) config['bettercap']['password'])
AsyncAdvertiser.__init__(self, config, view) AsyncAdvertiser.__init__(self, config, view, keypair)
AsyncTrainer.__init__(self, config) AsyncTrainer.__init__(self, config)
self._started_at = time.time() self._started_at = time.time()

View File

@ -6,6 +6,8 @@ main:
custom_plugins: custom_plugins:
# which plugins to load and enable # which plugins to load and enable
plugins: plugins:
api:
enabled: false
auto-update: auto-update:
enabled: false enabled: false
interval: 1 # every day interval: 1 # every day

47
pwnagotchi/identity.py Normal file
View File

@ -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

View File

@ -1,14 +1,4 @@
import os import os
from Crypto.PublicKey import RSA
import hashlib
def new_session_id(): def new_session_id():
return ':'.join(['%02x' % b for b in os.urandom(6)]) 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()

View File

@ -3,14 +3,13 @@ import logging
import pwnagotchi import pwnagotchi
import pwnagotchi.plugins as plugins import pwnagotchi.plugins as plugins
from pwnagotchi.mesh import get_identity
class AsyncAdvertiser(object): class AsyncAdvertiser(object):
def __init__(self, config, view): def __init__(self, config, view, keypair):
self._config = config self._config = config
self._view = view self._view = view
self._public_key, self._identity = get_identity(config) self._keypair = keypair
self._advertiser = None self._advertiser = None
def start_advertising(self): def start_advertising(self):
@ -24,7 +23,7 @@ class AsyncAdvertiser(object):
self._config['main']['iface'], self._config['main']['iface'],
pwnagotchi.name(), pwnagotchi.name(),
pwnagotchi.version, pwnagotchi.version,
self._identity, self._keypair.fingerprint,
period=0.3, period=0.3,
data=self._config['personality']) data=self._config['personality'])

View File

@ -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")

View File

@ -33,7 +33,7 @@ def on_loaded():
logging.info("AUTO-BACKUP: Successfuly loaded.") logging.info("AUTO-BACKUP: Successfuly loaded.")
def on_internet_available(display, config, log): def on_internet_available(display, keypair, config, log):
global STATUS global STATUS
if READY: if READY:

View File

@ -23,7 +23,7 @@ def on_loaded():
READY = True READY = True
def on_internet_available(display, config, log): def on_internet_available(display, keypair, config, log):
global STATUS global STATUS
if READY: if READY:

View File

@ -20,7 +20,7 @@ def on_loaded():
# called in manual mode when there's internet connectivity # 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 pass

View File

@ -55,7 +55,7 @@ def _upload_to_ohc(path, timeout=30):
raise e 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 Called in manual mode when there's internet connectivity
""" """

View File

@ -14,7 +14,7 @@ def on_loaded():
# called in manual mode when there's internet connectivity # 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: if log.is_new() and log.handshakes > 0:
try: try:
import tweepy import tweepy

View File

@ -12,7 +12,6 @@ import csv
from datetime import datetime from datetime import datetime
import requests import requests
from pwnagotchi.mesh.wifi import freq_to_channel 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 READY = False
ALREADY_UPLOADED = None ALREADY_UPLOADED = None
@ -26,6 +25,8 @@ AKMSUITE_TYPES = {
} }
def _handle_packet(packet, result): 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 Analyze each packet and extract the data from Dot11 layers
""" """
@ -76,6 +77,8 @@ def _handle_packet(packet, result):
def _analyze_pcap(pcap): 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 Iterate over the packets and extract data
""" """
@ -192,7 +195,9 @@ def _send_to_wigle(lines, api_key, timeout=30):
raise re_e 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 Called in manual mode when there's internet connectivity
""" """

View File

@ -54,7 +54,7 @@ def _upload_to_wpasec(path, timeout=30):
raise e 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 Called in manual mode when there's internet connectivity
""" """

View File

@ -89,6 +89,9 @@ class StatusFile(object):
if os.path.exists(path): if os.path.exists(path):
self._updated = datetime.fromtimestamp(os.path.getmtime(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): def newer_then_days(self, days):
return self._updated is not None and (datetime.now() - self._updated).days < days return self._updated is not None and (datetime.now() - self._updated).days < days

View File

@ -1,4 +1,5 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*-
from setuptools import setup, find_packages from setuptools import setup, find_packages
import pwnagotchi import pwnagotchi