213 lines
6.8 KiB
Python
213 lines
6.8 KiB
Python
import os
|
|
import pickle
|
|
import string
|
|
import hmac
|
|
from cryptography.hazmat.primitives import hashes, serialization
|
|
from cryptography.hazmat.primitives.asymmetric import ec
|
|
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
|
|
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
|
|
|
CURVE = ec.SECP256R1()
|
|
|
|
class MessageHeader:
|
|
dh: object
|
|
pn: int
|
|
n: int
|
|
|
|
def GENERATE_DH():
|
|
return ec.generate_private_key(CURVE)
|
|
|
|
def DH(dh_pair, dh_pub):
|
|
return dh_pair.exchange(ec.ECDH(), dh_pub)
|
|
|
|
def KDF_RK(rk, dh_out):
|
|
output = HKDF(
|
|
algorithm=hashes.SHA256(),
|
|
length=64,
|
|
salt=rk,
|
|
info=b"",
|
|
).derive(dh_out)
|
|
return output[:32], output[32:]
|
|
|
|
|
|
def KDF_CK(ck):
|
|
return (
|
|
hmac.new(ck, b"\x01", "sha256").digest(),
|
|
hmac.new(ck, b"\x02", "sha256").digest(),
|
|
)
|
|
|
|
def ENCRYPT(mk, plaintext, associated_data):
|
|
nonce = os.urandom(12)
|
|
return nonce + AESGCM(mk).encrypt(nonce, plaintext, associated_data)
|
|
|
|
|
|
def DECRYPT(mk, ciphertext, associated_data):
|
|
return AESGCM(mk).decrypt(ciphertext[:12], ciphertext[12:], associated_data)
|
|
|
|
def HEADER(dh_pair, pn, n):
|
|
public_key = dh_pair.public_key()
|
|
return MessageHeader(public_key, int(pn), int(n))
|
|
|
|
def CONCAT(ad, header):
|
|
header_tup = pickle.dumps(
|
|
(header.dh.public_bytes(serialization.Encoding.X962,serialization.PublicFormat.UncompressedPoint), header.pn, header.n),
|
|
)
|
|
return len(ad).to_bytes(8) + ad + header_tup
|
|
|
|
class MessengerServer:
|
|
def __init__(self, server_signing_key, server_decryption_key):
|
|
self.server_signing_key = server_signing_key
|
|
self.server_decryption_key = server_decryption_key
|
|
|
|
def decryptReport(self, ct):
|
|
data = pickle.loads(ct)
|
|
key = ec.EllipticCurvePublicKey.from_encoded_point(
|
|
CURVE, data["bytes"]
|
|
)
|
|
shared = DH(self.server_decryption_key, key)
|
|
derived_key = HKDF(
|
|
algorithm=hashes.SHA256(),
|
|
length=32,
|
|
salt=None,
|
|
info=b""
|
|
).derive(shared)
|
|
aesgcm = AESGCM(derived_key)
|
|
plaintext = aesgcm.decrypt(data["nonce"], data["ciphertext"], data["bytes"])
|
|
return plaintext.decode("latin1")
|
|
|
|
def signCert(self, cert):
|
|
return self.server_signing_key.sign(cert, ec.ECDSA(hashes.SHA256()))
|
|
|
|
class MessengerClient:
|
|
|
|
def __init__(self, name, server_signing_pk, server_encryption_pk):
|
|
self.name = name
|
|
self.server_signing_pk = server_signing_pk
|
|
self.server_encryption_pk = server_encryption_pk
|
|
self.conns = {}
|
|
self.certs = {}
|
|
self.dh = GENERATE_DH()
|
|
self.certificate = None
|
|
|
|
def RatchetInitAlice(state, SK, bob_dh_public_key):
|
|
state.DHs = GENERATE_DH()
|
|
state.DHr = bob_dh_public_key
|
|
state.RK, state.CKs = KDF_RK(SK, DH(state.DHs, state.DHr))
|
|
state.CKr = None
|
|
state.Ns = 0
|
|
state.Nr = 0
|
|
state.PN = 0
|
|
state.MKSKIPPED = {}
|
|
|
|
def RatchetInitBob(state, SK, bob_dh_key_pair):
|
|
state.DHs = bob_dh_key_pair
|
|
state.DHr = None
|
|
state.RK = SK
|
|
state.CKs = None
|
|
state.CKr = None
|
|
state.Ns = 0
|
|
state.Nr = 0
|
|
state.PN = 0
|
|
state.MKSKIPPED = {}
|
|
|
|
def RatchetSendKey(state):
|
|
state.CKs, mk = KDF_CK(state.CKs)
|
|
Ns = state.Ns
|
|
state.Ns += 1
|
|
return Ns, mk
|
|
|
|
def RatchetEncrypt(state, plaintext, AD):
|
|
Ns, mk = RatchetSendKey(state)
|
|
header = HEADER(state.DHs, state.PN, Ns)
|
|
return header, ENCRYPT(mk, plaintext, CONCAT(AD, header))
|
|
|
|
def RatchetReceiveKey(state, header):
|
|
mk = TrySkippedMessageKeys(state, header)
|
|
if mk != None:
|
|
return mk
|
|
if header.dh != state.DHr:
|
|
SkipMessageKeys(state, header.pn)
|
|
DHRatchet(state, header)
|
|
SkipMessageKeys(state, header.n)
|
|
state.CKr, mk = KDF_CK(state.CKr)
|
|
state.Nr += 1
|
|
return mk
|
|
|
|
def RatchetDecrypt(state, header, ciphertext, AD):
|
|
mk = RatchetReceiveKey(state, header)
|
|
return DECRYPT(mk, ciphertext, CONCAT(AD, header))
|
|
|
|
def TrySkippedMessageKeys(state, header):
|
|
if (header.dh, header.n) in state.MKSKIPPED:
|
|
mk = state.MKSKIPPED[header.dh, header.n]
|
|
del state.MKSKIPPED[header.dh, header.n]
|
|
return mk
|
|
else:
|
|
return None
|
|
|
|
def SkipMessageKeys(state, until):
|
|
if state.Nr + MAX_SKIP < until:
|
|
raise Error()
|
|
if state.CKr != None:
|
|
while state.Nr < until:
|
|
state.CKr, mk = KDF_CK(state.CKr)
|
|
state.MKSKIPPED[state.DHr, state.Nr] = mk
|
|
state.Nr += 1
|
|
|
|
def DHRatchet(state, header):
|
|
state.PN = state.Ns
|
|
state.Ns = 0
|
|
state.Nr = 0
|
|
state.DHr = header.dh
|
|
state.RK, state.CKr = KDF_RK(state.RK, DH(state.DHs, state.DHr))
|
|
state.DHs = GENERATE_DH()
|
|
state.RK, state.CKs = KDF_RK(state.RK, DH(state.DHs, state.DHr))
|
|
|
|
def generateCertificate(self):
|
|
self.certificate = pickle.dumps((self.name, self.dh.public_key().public_bytes(serialization.Encoding.X962,
|
|
serialization.PublicFormat.UncompressedPoint)))
|
|
return self.certificate
|
|
|
|
def receiveCertificate(self, certificate, signature):
|
|
self.server_signing_pk.verify(signature, certificate, ec.ECDSA(hashes.SHA256()))
|
|
name, public_bytes = pickle.loads(certificate)
|
|
public_key = ec.EllipticCurvePublicKey.from_encoded_point(CURVE, public_bytes)
|
|
self.certs[name] = public_key
|
|
|
|
def sendMessage(self, name, message):
|
|
raise Exception("not implemented!")
|
|
return
|
|
|
|
def receiveMessage(self, name, header, ciphertext):
|
|
raise Exception("not implemented!")
|
|
return
|
|
|
|
# this is technically hybrid encryption instead of just el gamal but I blame the instructions instead of myself
|
|
# maybe i'll change this if I want to use a late day
|
|
def report(self, name, message):
|
|
report = (name, message)
|
|
plaintext = pickle.dumps(report)
|
|
temp_key = GENERATE_DH()
|
|
temp_bytes = temp_key.public_key().public_bytes(
|
|
serialization.Encoding.X962,
|
|
serialization.PublicFormat.UncompressedPoint,
|
|
)
|
|
shared_secret = DH(temp_key, self.server_encryption_pk)
|
|
hkdf = HKDF(
|
|
algorithm=hashes.SHA256(),
|
|
length=32,
|
|
salt=None,
|
|
info=b""
|
|
)
|
|
key = hkdf.derive(shared_secret)
|
|
nonce = os.urandom(12)
|
|
aesgcm = AESGCM(key)
|
|
ciphertext = aesgcm.encrypt(nonce, plaintext, temp_bytes) # not sure if passing in temp_bytes is correct, but it seems like it should be
|
|
data = {
|
|
"bytes": temp_bytes,
|
|
"nonce": nonce,
|
|
"ciphertext": ciphertext
|
|
}
|
|
return plaintext.decode("latin1"), pickle.dumps(data)
|
|
|