new: fixed rsa identity generation and implemented api enrollment plugin
This commit is contained in:
parent
107eb57f26
commit
1c251fc093
@ -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 ...")
|
||||||
|
@ -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()
|
||||||
|
@ -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
47
pwnagotchi/identity.py
Normal 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
|
@ -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()
|
|
||||||
|
@ -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'])
|
||||||
|
|
||||||
|
51
pwnagotchi/plugins/default/api.py
Normal file
51
pwnagotchi/plugins/default/api.py
Normal 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")
|
@ -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:
|
||||||
|
@ -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:
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
"""
|
"""
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user