Skip to content

Commit

Permalink
clients/py: fixed-up unit tests + packaging + readme
Browse files Browse the repository at this point in the history
  • Loading branch information
CedarMist committed Oct 28, 2023
1 parent 9e4877f commit 060382d
Show file tree
Hide file tree
Showing 18 changed files with 345 additions and 222 deletions.
6 changes: 5 additions & 1 deletion clients/py/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
__pycache__
__pycache__
dist
build
.mypy_cache
*.egg-info
16 changes: 16 additions & 0 deletions clients/py/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
PYTHON?=python3
MODULE=sapphirepy

all: mypy test wheel

mypy:
$(PYTHON) -mmypy $(MODULE) --check-untyped-defs

test:
$(PYTHON) -munittest discover

clean:
rm -rf sapphirepy/__pycache__ build dist sapphire.py.egg-info .mypy_cache

wheel:
$(PYTHON) setup.py bdist_wheel
16 changes: 16 additions & 0 deletions clients/py/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# sapphire.py

```python
from web3 import Web3
from sapphirepy import sapphire

# Setup your Web3 provider with a signing account
w3 = Web3(Web3.HTTPProvider('http://localhost:8545'))
w3.middleware_onion.add(construct_sign_and_send_raw_middleware(account))

# Finally, wrap the provider to add Sapphire end-to-end encryption
w3 = sapphire.wrap(w3)
```

The Sapphire middleware for Web3.py ensures all transactions, gas estimates and view calls are end-to-end encrypted between your application and the smart contract.

108 changes: 0 additions & 108 deletions clients/py/kat.py

This file was deleted.

11 changes: 6 additions & 5 deletions clients/py/sapphirepy/deoxysii.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

import struct
import array
import hmac

BlockSize = 16

Expand Down Expand Up @@ -377,10 +378,10 @@ def __init__(self, key:bytes|bytearray):
self.derivedKs = STKDeriveK(key)

@property
def name(self):
def implementation(self):
return "vartime"

def E(self, nonce:bytes|bytearray, dst:bytearray, ad:bytes|bytearray, msg:bytes|bytearray):
def E(self, nonce:bytes|bytearray, dst:bytearray, ad:bytes|bytearray|None, msg:bytes|bytearray):
assert len(nonce) == (BlockSize-1)
tweak = bytearray(TweakSize)
tmp = bytearray(BlockSize)
Expand Down Expand Up @@ -460,7 +461,7 @@ def E(self, nonce:bytes|bytearray, dst:bytearray, ad:bytes|bytearray, msg:bytes|

dst[len(dst)-TagSize:] = tag

def D(self, nonce:bytes|bytearray, dst:bytearray, ad:bytes|bytearray, ciphertext:bytes|bytearray):
def D(self, nonce:bytes|bytearray, dst:bytearray, ad:bytes|bytearray|None, ciphertext:bytes|bytearray):
assert len(nonce) == TagSize-1
assert len(dst) == len(ciphertext) - TagSize

Expand Down Expand Up @@ -542,8 +543,8 @@ def D(self, nonce:bytes|bytearray, dst:bytearray, ad:bytes|bytearray, ciphertext
decNonce[0] = PrefixTag << PrefixShift
bcEncrypt(tagP, self.derivedKs, decNonce, tagP)

# Tag verification, XXX: variable time
return tag == tagP
# Tag verification
return hmac.compare_digest(tag, tagP)

def deriveSubTweakKeys(derivedKs:list[bytearray], t:bytearray):
assert len(derivedKs) == STKCount
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
from binascii import unhexlify
import hmac

from os import urandom
import cbor2
from nacl.bindings.crypto_scalarmult import crypto_scalarmult
from nacl.utils import random
from nacl.public import PrivateKey, PublicKey
import cbor2

from .deoxysii import DeoxysII, NonceSize, TagSize
from .error import SapphireError

###############################################################################
# CBOR encoded envelope formats
Expand Down Expand Up @@ -36,7 +37,7 @@ class RequestBody(TypedDict):
pk: bytes
data: bytes
nonce: bytes
epoch: Optional[int]
epoch: int

class RequestEnvelope(TypedDict):
body: dict
Expand All @@ -45,13 +46,13 @@ class RequestEnvelope(TypedDict):
###############################################################################
# Errors relating to encryption, decryption & envelope format

class SapphireBaseError(Exception):
class EnvelopeError(SapphireError):
pass

class SapphireFailError(SapphireBaseError):
class DecryptError(SapphireError):
pass

class SapphireUnknownError(SapphireBaseError):
class CallFailure(SapphireError):
pass

###############################################################################
Expand All @@ -61,63 +62,67 @@ def _deriveSharedSecret(pk:PublicKey, sk:PrivateKey):
msg = crypto_scalarmult(sk.encode(), pk._public_key)
return hmac.new(key, msg, digestmod='sha512_256').digest()

class TransactionEncrypter:
epoch:int|None
class TransactionCipher:
epoch:int
cipher:DeoxysII
myPublicKey:bytes
def __init__(self, peerPublicKey:PublicKey|str, peerEpoch:int|None=None):
ephemeralPublicKey:bytes

def __init__(self, peerPublicKey:PublicKey|str, peerEpoch:int):
if isinstance(peerPublicKey, str):
if len(peerPublicKey) != 66 or peerPublicKey[:2] != "0x":
raise RuntimeError('peerPublicKey.invalid', peerPublicKey)
raise ValueError('peerPublicKey.invalid', peerPublicKey)
peerPublicKey = PublicKey(unhexlify(peerPublicKey[2:]))
sk = PrivateKey.generate()
self.myPublicKey = sk.public_key.encode()
self.ephemeralPublicKey = sk.public_key.encode()
self.cipher = DeoxysII(_deriveSharedSecret(peerPublicKey, sk))
self.epoch = peerEpoch

def _encryptCallData(self, calldata: bytes):
nonce = urandom(NonceSize)
nonce = random(NonceSize)
plaintext = cbor2.dumps({'body': calldata}, canonical=True)
ciphertext = bytearray(len(plaintext) + TagSize)
self.cipher.E(nonce=nonce, dst=ciphertext, ad=b"", msg=plaintext)
self.cipher.E(nonce=nonce, dst=ciphertext, ad=None, msg=plaintext)
return ciphertext, nonce

def encrypt(self, plaintext: bytes):
ciphertext, nonce = self._encryptCallData(plaintext)
envelope:RequestEnvelope = {
'body': {
'pk': self.myPublicKey,
'pk': self.ephemeralPublicKey,
'data': ciphertext,
'nonce': nonce
'nonce': nonce,
'epoch': self.epoch
},
'format': FORMAT_Encrypted_X25519DeoxysII
}
if self.epoch:
envelope['body']['epoch'] = self.epoch
return cbor2.dumps(envelope, canonical=True)

def _decodeInner(self, plaintext:bytes) -> bytes:
innerResult = cast(ResultInner, cbor2.loads(plaintext))
if innerResult.get('ok', None) is not None:
return innerResult['ok']
raise SapphireFailError(innerResult['fail'])
raise CallFailure(innerResult['fail'])

def _decryptInner(self, envelope: AeadEnvelope):
plaintext = bytearray(len(envelope['data']) - TagSize)
decryptOk = self.cipher.D(
nonce=envelope['nonce'],
dst=plaintext,
ad=b"",
ad=None,
ciphertext=envelope['data'])
if not decryptOk:
raise RuntimeError('Failed to decrypt')
raise DecryptError()
return self._decodeInner(plaintext)

def decrypt(self, response: bytes):
callResult = cast(ResultOuter, cbor2.loads(response))
if not isinstance(callResult, dict):
raise EnvelopeError('callResult', type(callResult))
if callResult.get('failure', None) is not None:
raise SapphireFailError(callResult['failure'])
raise CallFailure(callResult['failure'])
ok = callResult.get('ok', None)
if not isinstance(ok, dict):
raise EnvelopeError('callResult.ok', type(ok))
if ok is not None:
return self._decryptInner(ok)
raise RuntimeError("No 'ok in call result!")
raise EnvelopeError("No 'ok in call result!")
3 changes: 3 additions & 0 deletions clients/py/sapphirepy/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

class SapphireError(Exception):
pass
Loading

0 comments on commit 060382d

Please sign in to comment.