2020-08-05 12:05:39 +00:00
|
|
|
__filename__ = "devices.py"
|
|
|
|
__author__ = "Bob Mottram"
|
|
|
|
__license__ = "AGPL3+"
|
2021-01-26 10:07:42 +00:00
|
|
|
__version__ = "1.2.0"
|
2020-08-05 12:05:39 +00:00
|
|
|
__maintainer__ = "Bob Mottram"
|
|
|
|
__email__ = "bob@freedombone.net"
|
|
|
|
__status__ = "Production"
|
2021-06-26 11:16:41 +00:00
|
|
|
__module_group__ = "Security"
|
2020-08-05 12:05:39 +00:00
|
|
|
|
2020-08-06 16:33:19 +00:00
|
|
|
# REST API overview
|
|
|
|
#
|
|
|
|
# To support Olm, the following APIs are required:
|
|
|
|
#
|
|
|
|
# * Uploading keys for a device (current app)
|
|
|
|
# POST /api/v1/crypto/keys/upload
|
|
|
|
#
|
|
|
|
# * Querying available devices of people you want to establish a session with
|
|
|
|
# POST /api/v1/crypto/keys/query
|
|
|
|
#
|
|
|
|
# * Claiming a pre-key (one-time-key) for each device you want to establish
|
|
|
|
# a session with
|
|
|
|
# POST /api/v1/crypto/keys/claim
|
|
|
|
#
|
|
|
|
# * Sending encrypted messages directly to specific devices of other people
|
|
|
|
# POST /api/v1/crypto/delivery
|
|
|
|
#
|
|
|
|
# * Collect encrypted messages addressed to the current device
|
|
|
|
# GET /api/v1/crypto/encrypted_messages
|
|
|
|
#
|
|
|
|
# * Clear all encrypted messages addressed to the current device
|
|
|
|
# POST /api/v1/crypto/encrypted_messages/clear
|
|
|
|
|
2020-08-05 12:05:39 +00:00
|
|
|
import os
|
|
|
|
from utils import loadJson
|
2020-08-05 14:06:04 +00:00
|
|
|
from utils import saveJson
|
2021-07-13 21:59:53 +00:00
|
|
|
from utils import acctDir
|
2021-08-14 11:13:39 +00:00
|
|
|
from utils import localActorUrl
|
2020-08-05 14:06:04 +00:00
|
|
|
|
|
|
|
|
2020-08-06 20:16:42 +00:00
|
|
|
def E2EEremoveDevice(baseDir: str, nickname: str, domain: str,
|
|
|
|
deviceId: str) -> bool:
|
2020-08-05 14:06:04 +00:00
|
|
|
"""Unregisters a device for e2ee
|
|
|
|
"""
|
2021-07-13 21:59:53 +00:00
|
|
|
personDir = acctDir(baseDir, nickname, domain)
|
2020-08-05 14:06:04 +00:00
|
|
|
deviceFilename = personDir + '/devices/' + deviceId + '.json'
|
|
|
|
if os.path.isfile(deviceFilename):
|
|
|
|
os.remove(deviceFilename)
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
2020-08-06 20:56:14 +00:00
|
|
|
def E2EEvalidDevice(deviceJson: {}) -> bool:
|
|
|
|
"""Returns true if the given json contains valid device keys
|
|
|
|
"""
|
|
|
|
if not isinstance(deviceJson, dict):
|
|
|
|
return False
|
|
|
|
if not deviceJson.get('deviceId'):
|
|
|
|
return False
|
|
|
|
if not isinstance(deviceJson['deviceId'], str):
|
|
|
|
return False
|
|
|
|
if not deviceJson.get('type'):
|
|
|
|
return False
|
|
|
|
if not isinstance(deviceJson['type'], str):
|
|
|
|
return False
|
2020-08-06 21:23:17 +00:00
|
|
|
if not deviceJson.get('name'):
|
|
|
|
return False
|
|
|
|
if not isinstance(deviceJson['name'], str):
|
|
|
|
return False
|
2020-08-06 20:56:14 +00:00
|
|
|
if deviceJson['type'] != 'Device':
|
|
|
|
return False
|
|
|
|
if not deviceJson.get('claim'):
|
|
|
|
return False
|
|
|
|
if not isinstance(deviceJson['claim'], str):
|
|
|
|
return False
|
|
|
|
if not deviceJson.get('fingerprintKey'):
|
|
|
|
return False
|
|
|
|
if not isinstance(deviceJson['fingerprintKey'], dict):
|
|
|
|
return False
|
|
|
|
if not deviceJson['fingerprintKey'].get('type'):
|
|
|
|
return False
|
|
|
|
if not isinstance(deviceJson['fingerprintKey']['type'], str):
|
|
|
|
return False
|
|
|
|
if not deviceJson['fingerprintKey'].get('publicKeyBase64'):
|
|
|
|
return False
|
|
|
|
if not isinstance(deviceJson['fingerprintKey']['publicKeyBase64'], str):
|
|
|
|
return False
|
|
|
|
if not deviceJson.get('identityKey'):
|
|
|
|
return False
|
|
|
|
if not isinstance(deviceJson['identityKey'], dict):
|
|
|
|
return False
|
|
|
|
if not deviceJson['identityKey'].get('type'):
|
|
|
|
return False
|
|
|
|
if not isinstance(deviceJson['identityKey']['type'], str):
|
|
|
|
return False
|
|
|
|
if not deviceJson['identityKey'].get('publicKeyBase64'):
|
|
|
|
return False
|
|
|
|
if not isinstance(deviceJson['identityKey']['publicKeyBase64'], str):
|
|
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
2020-08-06 20:16:42 +00:00
|
|
|
def E2EEaddDevice(baseDir: str, nickname: str, domain: str,
|
|
|
|
deviceId: str, name: str, claimUrl: str,
|
|
|
|
fingerprintPublicKey: str,
|
|
|
|
identityPublicKey: str,
|
|
|
|
fingerprintKeyType="Ed25519Key",
|
|
|
|
identityKeyType="Curve25519Key") -> bool:
|
2020-08-05 14:06:04 +00:00
|
|
|
"""Registers a device for e2ee
|
|
|
|
claimUrl could be something like:
|
|
|
|
http://localhost:3000/users/admin/claim?id=11119
|
|
|
|
"""
|
|
|
|
if ' ' in deviceId or '/' in deviceId or \
|
|
|
|
'?' in deviceId or '#' in deviceId or \
|
|
|
|
'.' in deviceId:
|
|
|
|
return False
|
2021-07-13 21:59:53 +00:00
|
|
|
personDir = acctDir(baseDir, nickname, domain)
|
2020-08-05 14:06:04 +00:00
|
|
|
if not os.path.isdir(personDir):
|
|
|
|
return False
|
|
|
|
if not os.path.isdir(personDir + '/devices'):
|
|
|
|
os.mkdir(personDir + '/devices')
|
|
|
|
deviceDict = {
|
|
|
|
"deviceId": deviceId,
|
|
|
|
"type": "Device",
|
|
|
|
"name": name,
|
|
|
|
"claim": claimUrl,
|
|
|
|
"fingerprintKey": {
|
|
|
|
"type": fingerprintKeyType,
|
|
|
|
"publicKeyBase64": fingerprintPublicKey
|
|
|
|
},
|
|
|
|
"identityKey": {
|
|
|
|
"type": identityKeyType,
|
|
|
|
"publicKeyBase64": identityPublicKey
|
|
|
|
}
|
|
|
|
}
|
|
|
|
deviceFilename = personDir + '/devices/' + deviceId + '.json'
|
|
|
|
return saveJson(deviceDict, deviceFilename)
|
2020-08-05 12:05:39 +00:00
|
|
|
|
|
|
|
|
2020-08-06 20:16:42 +00:00
|
|
|
def E2EEdevicesCollection(baseDir: str, nickname: str, domain: str,
|
|
|
|
domainFull: str, httpPrefix: str) -> {}:
|
2020-08-05 12:05:39 +00:00
|
|
|
"""Returns a list of registered devices
|
|
|
|
"""
|
2021-07-13 21:59:53 +00:00
|
|
|
personDir = acctDir(baseDir, nickname, domain)
|
2020-08-05 12:05:39 +00:00
|
|
|
if not os.path.isdir(personDir):
|
|
|
|
return {}
|
2021-08-14 11:13:39 +00:00
|
|
|
personId = localActorUrl(httpPrefix, nickname, domainFull)
|
2020-08-05 12:05:39 +00:00
|
|
|
if not os.path.isdir(personDir + '/devices'):
|
|
|
|
os.mkdir(personDir + '/devices')
|
|
|
|
deviceList = []
|
|
|
|
for subdir, dirs, files in os.walk(personDir + '/devices/'):
|
|
|
|
for dev in files:
|
|
|
|
if not dev.endswith('.json'):
|
|
|
|
continue
|
|
|
|
deviceFilename = os.path.join(personDir + '/devices', dev)
|
|
|
|
devJson = loadJson(deviceFilename)
|
|
|
|
if devJson:
|
|
|
|
deviceList.append(devJson)
|
2020-12-13 22:13:45 +00:00
|
|
|
break
|
2020-08-05 12:05:39 +00:00
|
|
|
|
|
|
|
devicesDict = {
|
|
|
|
'id': personId + '/collections/devices',
|
|
|
|
'type': 'Collection',
|
|
|
|
'totalItems': len(deviceList),
|
|
|
|
'items': deviceList
|
|
|
|
}
|
|
|
|
return devicesDict
|
2020-08-05 12:47:15 +00:00
|
|
|
|
|
|
|
|
2020-08-06 20:16:42 +00:00
|
|
|
def E2EEdecryptMessageFromDevice(messageJson: {}) -> str:
|
2020-08-05 12:47:15 +00:00
|
|
|
"""Locally decrypts a message on the device.
|
|
|
|
This should probably be a link to a local script
|
|
|
|
or native app, such that what the user sees isn't
|
|
|
|
something which the server could get access to.
|
|
|
|
"""
|
|
|
|
# TODO
|
|
|
|
# {
|
|
|
|
# "type": "EncryptedMessage",
|
|
|
|
# "messageType": 0,
|
|
|
|
# "cipherText": "...",
|
|
|
|
# "digest": {
|
|
|
|
# "type": "Digest",
|
|
|
|
# "digestAlgorithm": "http://www.w3.org/2000/09/xmldsig#hmac-sha256",
|
|
|
|
# "digestValue": "5f6ad31acd64995483d75c7..."
|
|
|
|
# },
|
|
|
|
# "messageFranking": "...",
|
|
|
|
# "attributedTo": {
|
|
|
|
# "type": "Device",
|
|
|
|
# "deviceId": "11119"
|
|
|
|
# },
|
|
|
|
# "to": {
|
|
|
|
# "type": "Device",
|
|
|
|
# "deviceId": "11876"
|
|
|
|
# }
|
|
|
|
# }
|
|
|
|
return ''
|