connect from secureput iphone app

This commit is contained in:
Keyvan Fatehi
2023-01-16 01:17:09 -08:00
parent 84df7d4feb
commit 9bde383511
10 changed files with 299 additions and 292 deletions

1
.gitignore vendored
View File

@@ -1 +1,2 @@
__pycache__
secureput_identity.shelve

2
TODO
View File

@@ -1,4 +1,4 @@
- iphone support (swift client? or html?)
- [x] iphone
- control interface
- battery view
- camera switcher

0
secureput/__init__.py Normal file
View File

36
secureput/aes.py Normal file
View File

@@ -0,0 +1,36 @@
import os
import base64
from typing import Tuple
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
def encrypt(key: bytes, insecure: bytes) -> bytes:
plain_text = insecure.encode()
iv = os.urandom(16)
key_bytes = key.encode()
cipher = Cipher(algorithms.AES(key_bytes), modes.CBC(iv), backend=default_backend())
encryptor = cipher.encryptor()
padded_plain_text = PKCS7Padding(plain_text, 16)
cipher_text = iv + encryptor.update(padded_plain_text) + encryptor.finalize()
return base64.b64encode(cipher_text).decode()
def decrypt(key: bytes, secure: bytes) -> bytes:
cipher_text = base64.b64decode(secure)
key_bytes = key.encode()
iv = cipher_text[:16]
cipher_text = cipher_text[16:]
cipher = Cipher(algorithms.AES(key_bytes), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
decrypted_text = decryptor.update(cipher_text) + decryptor.finalize()
padding_length = decrypted_text[-1]
decrypted_text = decrypted_text[:-padding_length]
return decrypted_text
def PKCS7Padding(data: bytes, block_size: int) -> bytes:
padding_length = block_size - (len(data) % block_size)
padding = bytes([padding_length]) * padding_length
return data + padding

36
secureput/app.py Normal file
View File

@@ -0,0 +1,36 @@
import shelve
from uuid import uuid4
from socket import gethostname
import json
import pyqrcode
from secureput.secret import generate_secret_key
class App:
def __init__(self):
self.config = shelve.open("secureput_identity.shelve")
self.__init_config_default("deviceName", lambda: gethostname())
self.__init_config_default("deviceUUID", lambda: str(uuid4()))
self.__init_config_default("accountUUID", lambda: None)
def __init_config_default(self, key, default_value_lambda):
try:
self.config[key]
except KeyError:
self.config[key] = default_value_lambda()
def paired(self):
return self.config["accountUUID"] != None
def gen_pair_info(self):
pairing = {}
pairing["secret"] = generate_secret_key()
pairing["uuid"] = self.config["deviceUUID"]
self.config["deviceSecret"] = pairing["secret"]
url = pyqrcode.create(json.dumps(pairing))
print(url.terminal(quiet_zone=1))
if __name__ == "__main__":
app = App()
print(app.config["deviceName"])
print(app.config["deviceUUID"])
print(app.gen_pair_info())

12
secureput/secret.py Normal file
View File

@@ -0,0 +1,12 @@
import random
runes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
def rand_seq(n):
b = []
for i in range(n):
b.append(random.choice(runes))
return "".join(b)
def generate_secret_key():
return rand_seq(32)

View File

@@ -0,0 +1,117 @@
import asyncio
from aiortc.contrib.signaling import RTCSessionDescription, RTCIceCandidate, candidate_from_sdp, candidate_to_sdp
import json
from secureput.websocket_signaling import WebsocketSignaling
import secureput.aes as aes
from secureput.app import App
import traceback
class SecureputSignaling(WebsocketSignaling):
def __init__(self, server):
super().__init__(server)
self._app = App()
if not self._app.paired():
self._app.gen_pair_info()
async def connect(self):
await super().connect()
await self.sendIdentity()
async def close(self):
if self._websocket is not None and self._websocket.open is True:
await self._websocket.close()
def secret(self):
return self._app.config["deviceSecret"]
def encrypt(self, data):
return aes.encrypt(self.secret(), data)
def decrypt(self, data):
return aes.decrypt(self.secret(), data)
def __object_from_string(self, message_str):
message = json.loads(message_str)
if message["type"] == "wrapped":
message = json.loads(self.decrypt(message["payload"]["data"]))
return message
def __object_to_string(self, obj):
if isinstance(obj, RTCSessionDescription):
message = self.forwardWrap({
"type": "SessionDescription",
"payload": {"sdp": obj.sdp, "type": obj.type}
})
elif isinstance(obj, RTCIceCandidate):
message = self.forwardWrap({
"type": "IceCandidate",
"payload": {
"sdp": "candidate:" + candidate_to_sdp(obj),
"sdpMid": obj.sdpMid,
"sdpMLineIndex": obj.sdpMLineIndex,
}
})
else:
message = obj
return json.dumps(message, sort_keys=True)
async def send(self, descr):
data = self.__object_to_string(descr)
print("Websocket send: %s" % data)
await self._websocket.send(data + '\n')
async def receive(self):
try:
data = await self._websocket.recv()
except asyncio.IncompleteReadError:
return
ret = self.__object_from_string(data)
if ret == None:
print("remote host says good bye!")
elif isinstance(ret, dict):
if ret['type'] == 'claim':
await self.claim(ret['payload']['account'])
else:
if ret["type"] == "SessionDescription":
sdp = ret["payload"]["sdp"]
type = ret["payload"]["type"]
return RTCSessionDescription(sdp=sdp, type=type)
elif ret["type"] == "IceCandidate":
candidate = candidate_from_sdp(ret["payload"]["sdp"].split(":", 1)[1])
candidate.sdpMid = ret["payload"]["sdpMid"]
candidate.sdpMLineIndex = ret["payload"]["sdpMLineIndex"]
return candidate
return ret
async def sendIdentity(self):
await self._websocket.send(json.dumps({
"type": "identify-target",
"payload": {
"name": self._app.config["deviceName"],
"device": self._app.config["deviceUUID"],
"account": self._app.config["accountUUID"]
}
}))
async def claim(self, account):
self._app.config["accountUUID"] = account
print("claimed to account %s" % account)
await self.sendIdentity()
def forwardWrap(self, json_data: dict) -> str:
msg = {
'to': self._app.config["accountUUID"],
'type': 'forward-wrapped',
}
# Encrypt the plaintext
ciphertext = self.encrypt(json.dumps(json_data))
# Install the ciphertext into the payload
msg['body'] = ciphertext
return msg

View File

@@ -0,0 +1,32 @@
import asyncio
import websockets
from aiortc.contrib.signaling import object_from_string, object_to_string
class WebsocketSignaling:
def __init__(self, server):
self._server = server
self._websocket = None
async def connect(self):
self._websocket = await websockets.connect(self._server)
async def close(self):
if self._websocket is not None and self._websocket.open is True:
await self.send(None)
await self._websocket.close()
async def receive(self):
try:
data = await self._websocket.recv()
except asyncio.IncompleteReadError:
return
ret = object_from_string(data)
if ret == None:
print("remote host says good bye!")
return ret
async def send(self, descr):
data = object_to_string(descr)
await self._websocket.send(data + '\n')

126
server.py
View File

@@ -6,104 +6,94 @@ import logging
import os
import ssl
from typing import OrderedDict
from aiohttp import web
from aiortc import RTCPeerConnection, RTCSessionDescription, RTCRtpCodecCapability
from aiortc import RTCPeerConnection, RTCSessionDescription, RTCIceCandidate, RTCRtpCodecCapability, RTCConfiguration, RTCIceServer
from compressed_vipc_track import VisionIpcTrack
from desktop_stream_track import DesktopStreamTrack
from aiortc.contrib.signaling import BYE
from secureput.secureput_signaling import SecureputSignaling
ROOT = os.path.dirname(__file__)
# optional, for better performance
# try:
# import uvloop
# except ImportError:
# uvloop = None
cams = ["roadEncodeData","wideRoadEncodeData","driverEncodeData"]
cam = 1
async def index(request):
content = open(os.path.join(ROOT, "index.html"), "r").read()
return web.Response(content_type="text/html", text=content)
async def signal(pc, signaling):
await signaling.connect()
async def javascript(request):
content = open(os.path.join(ROOT, "client.js"), "r").read()
return web.Response(content_type="application/javascript", text=content)
while True:
obj = await signaling.receive()
# The peer trickles, but aiortc doesn't https://github.com/aiortc/aiortc/issues/227
# > aioice, the library which aiortc uses for ICE does not trickle ICE candidates:
# > you get all the candidates in one go. As such once you have called setLocalDescription()
# > for your offer or answer, all your ICE candidates are listed in pc.localDescription.
if isinstance(obj, RTCIceCandidate):
pc.addIceCandidate(obj)
async def offer(request):
params = await request.json()
offer = RTCSessionDescription(sdp=params["sdp"], type=params["type"])
pc = RTCPeerConnection()
pcs.add(pc)
if isinstance(obj, RTCSessionDescription):
if pc != None and pc.iceConnectionState != "failed":
await pc.close()
pc = RTCPeerConnection(configuration=RTCConfiguration([RTCIceServer("stun:stun.secureput.com:3478")]))
@pc.on("connectionstatechange")
async def on_connectionstatechange():
print("Connection state is %s" % pc.connectionState)
if pc.connectionState == "failed":
print("Connection state is %s" % pc.iceConnectionState)
if pc.iceConnectionState == "failed":
await pc.close()
pcs.discard(pc)
@pc.on('datachannel')
def on_datachannel(channel):
print("data channel!")
@channel.on('message')
async def on_message(message):
print("message!!")
await pc.setRemoteDescription(obj)
if obj.type == 'offer':
# TODO: stream the microphone
audio = None
# video = VisionIpcTrack(cams[int(args.cam)], args.addr)
video = DesktopStreamTrack()
video_sender = pc.addTrack(video)
await pc.setRemoteDescription(offer)
video = VisionIpcTrack(cams[int(cam)], "tici")
# video = DesktopStreamTrack()
pc.addTrack(video)
answer = await pc.createAnswer()
await pc.setLocalDescription(answer)
return web.Response(
content_type="application/json",
text=json.dumps(
{"sdp": pc.localDescription.sdp, "type": pc.localDescription.type}
),
)
pcs = set()
async def on_shutdown(app):
# close peer connections
coros = [pc.close() for pc in pcs]
await asyncio.gather(*coros)
pcs.clear()
await signaling.send(pc.localDescription)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Decode video streams and broadcast via WebRTC")
parser = argparse.ArgumentParser(description="Comma Body WebRTC Service")
parser.add_argument("--addr", default='tici', help="Address of comma three")
# Not implemented (yet?). Geo already made the PoC for this, it should be possible.
# parser.add_argument("--nvidia", action="store_true", help="Use nvidia instead of ffmpeg")
parser.add_argument("--cam", default="0", help="Camera to stream")
parser.add_argument("--cert-file", help="SSL certificate file (for HTTPS)")
parser.add_argument("--key-file", help="SSL key file (for HTTPS)")
parser.add_argument(
"--host", default="0.0.0.0", help="Host for HTTP server (default: 0.0.0.0)"
)
parser.add_argument(
"--port", type=int, default=8080, help="Port for HTTP server (default: 8080)"
)
parser.add_argument("--signaling-server", default="wss://signal.secureput.com", help="Signaling server to use")
parser.add_argument("--stun-server", default="stun:stun.secureput.com:3478", help="STUN server to use")
parser.add_argument("--verbose", "-v", action="count")
args = parser.parse_args()
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
else:
logging.basicConfig(level=logging.INFO)
if args.cert_file:
ssl_context = ssl.SSLContext()
ssl_context.load_cert_chain(args.cert_file, args.key_file)
else:
ssl_context = None
# if uvloop is not None:
# asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
app = web.Application()
app.on_shutdown.append(on_shutdown)
app.router.add_get("/", index)
app.router.add_get("/client.js", javascript)
app.router.add_post("/offer", offer)
web.run_app(app, host=args.host, port=args.port, ssl_context=ssl_context)
pc = RTCPeerConnection(configuration=RTCConfiguration([RTCIceServer(args.stun_server)]))
signaling = SecureputSignaling(args.signaling_server)
coro = signal(pc, signaling)
# run event loop
loop = asyncio.get_event_loop()
try:
loop.run_until_complete(coro)
except KeyboardInterrupt:
pass
finally:
loop.run_until_complete(pc.close())
loop.run_until_complete(signaling.close())

View File

@@ -1,217 +0,0 @@
import asyncio
import json
import logging
import os
import sys
from aiortc import RTCIceCandidate, RTCSessionDescription
from aiortc.sdp import candidate_from_sdp, candidate_to_sdp
logger = logging.getLogger(__name__)
BYE = object()
def object_from_string(message_str):
message = json.loads(message_str)
if message["type"] in ["answer", "offer"]:
return RTCSessionDescription(**message)
elif message["type"] == "candidate" and message["candidate"]:
candidate = candidate_from_sdp(message["candidate"].split(":", 1)[1])
candidate.sdpMid = message["id"]
candidate.sdpMLineIndex = message["label"]
return candidate
elif message["type"] == "bye":
return BYE
def object_to_string(obj):
if isinstance(obj, RTCSessionDescription):
message = {"sdp": obj.sdp, "type": obj.type}
elif isinstance(obj, RTCIceCandidate):
message = {
"candidate": "candidate:" + candidate_to_sdp(obj),
"id": obj.sdpMid,
"label": obj.sdpMLineIndex,
"type": "candidate",
}
else:
assert obj is BYE
message = {"type": "bye"}
return json.dumps(message, sort_keys=True)
class CopyAndPasteSignaling:
def __init__(self):
self._read_pipe = sys.stdin
self._read_transport = None
self._reader = None
self._write_pipe = sys.stdout
async def connect(self):
loop = asyncio.get_event_loop()
self._reader = asyncio.StreamReader(loop=loop)
self._read_transport, _ = await loop.connect_read_pipe(
lambda: asyncio.StreamReaderProtocol(self._reader), self._read_pipe
)
async def close(self):
if self._reader is not None:
await self.send(BYE)
self._read_transport.close()
self._reader = None
async def receive(self):
print("-- Please enter a message from remote party --")
data = await self._reader.readline()
print()
return object_from_string(data.decode(self._read_pipe.encoding))
async def send(self, descr):
print("-- Please send this message to the remote party --")
self._write_pipe.write(object_to_string(descr) + "\n")
self._write_pipe.flush()
print()
class TcpSocketSignaling:
def __init__(self, host, port):
self._host = host
self._port = port
self._server = None
self._reader = None
self._writer = None
async def connect(self):
pass
async def _connect(self, server):
if self._writer is not None:
return
if server:
connected = asyncio.Event()
def client_connected(reader, writer):
self._reader = reader
self._writer = writer
connected.set()
self._server = await asyncio.start_server(
client_connected, host=self._host, port=self._port
)
await connected.wait()
else:
self._reader, self._writer = await asyncio.open_connection(
host=self._host, port=self._port
)
async def close(self):
if self._writer is not None:
await self.send(BYE)
self._writer.close()
self._reader = None
self._writer = None
if self._server is not None:
self._server.close()
self._server = None
async def receive(self):
await self._connect(False)
try:
data = await self._reader.readuntil()
except asyncio.IncompleteReadError:
return
return object_from_string(data.decode("utf8"))
async def send(self, descr):
await self._connect(True)
data = object_to_string(descr).encode("utf8")
self._writer.write(data + b"\n")
class UnixSocketSignaling:
def __init__(self, path):
self._path = path
self._server = None
self._reader = None
self._writer = None
async def connect(self):
pass
async def _connect(self, server):
if self._writer is not None:
return
if server:
connected = asyncio.Event()
def client_connected(reader, writer):
self._reader = reader
self._writer = writer
connected.set()
self._server = await asyncio.start_unix_server(
client_connected, path=self._path
)
await connected.wait()
else:
self._reader, self._writer = await asyncio.open_unix_connection(self._path)
async def close(self):
if self._writer is not None:
await self.send(BYE)
self._writer.close()
self._reader = None
self._writer = None
if self._server is not None:
self._server.close()
self._server = None
os.unlink(self._path)
async def receive(self):
await self._connect(False)
try:
data = await self._reader.readuntil()
except asyncio.IncompleteReadError:
return
return object_from_string(data.decode("utf8"))
async def send(self, descr):
await self._connect(True)
data = object_to_string(descr).encode("utf8")
self._writer.write(data + b"\n")
def add_signaling_arguments(parser):
"""
Add signaling method arguments to an argparse.ArgumentParser.
"""
parser.add_argument(
"--signaling",
"-s",
choices=["copy-and-paste", "tcp-socket", "unix-socket"],
)
parser.add_argument(
"--signaling-host", default="127.0.0.1", help="Signaling host (tcp-socket only)"
)
parser.add_argument(
"--signaling-port", default=1234, help="Signaling port (tcp-socket only)"
)
parser.add_argument(
"--signaling-path",
default="aiortc.socket",
help="Signaling socket path (unix-socket only)",
)
def create_signaling(args):
"""
Create a signaling method based on command-line arguments.
"""
if args.signaling == "tcp-socket":
return TcpSocketSignaling(args.signaling_host, args.signaling_port)
elif args.signaling == "unix-socket":
return UnixSocketSignaling(args.signaling_path)
else:
return CopyAndPasteSignaling()