2018-09-17 22:13:30 +02:00
|
|
|
import json
|
|
|
|
import binascii
|
2019-03-14 00:47:13 +01:00
|
|
|
from copy import deepcopy
|
|
|
|
from collections import OrderedDict
|
|
|
|
|
|
|
|
import google.protobuf.json_format as json_pb # pylint: disable=no-name-in-module
|
2018-09-17 22:13:30 +02:00
|
|
|
from google.protobuf import json_format # pylint: disable=no-name-in-module
|
|
|
|
from google.protobuf.message import DecodeError as DecodeError_pb # pylint: disable=no-name-in-module,import-error
|
2019-03-14 00:47:13 +01:00
|
|
|
from google.protobuf.message import Message # pylint: disable=no-name-in-module,import-error
|
2018-09-17 22:13:30 +02:00
|
|
|
|
2019-03-14 00:47:13 +01:00
|
|
|
from lbrynet.schema.types.v2 import claim_pb2 as claim_pb
|
|
|
|
from torba.client.constants import COIN
|
2018-09-17 22:13:30 +02:00
|
|
|
|
2019-03-14 00:47:13 +01:00
|
|
|
from lbrynet.schema.types.v1 import legacy_claim_pb2
|
2018-12-15 07:25:46 +01:00
|
|
|
from lbrynet.schema.signature import Signature
|
2018-09-17 22:31:44 +02:00
|
|
|
from lbrynet.schema.validator import get_validator
|
|
|
|
from lbrynet.schema.signer import get_signer
|
2019-03-14 00:47:13 +01:00
|
|
|
from lbrynet.schema.legacy_schema_v1.claim import Claim as LegacyClaim
|
2019-02-28 22:54:37 +01:00
|
|
|
from lbrynet.schema.legacy_schema_v1 import CLAIM_TYPE_NAMES
|
|
|
|
from lbrynet.schema.constants import CURVE_NAMES, SECP256k1
|
2018-09-17 22:31:44 +02:00
|
|
|
from lbrynet.schema.encoding import decode_fields, decode_b64_fields, encode_fields
|
|
|
|
from lbrynet.schema.error import DecodeError
|
|
|
|
from lbrynet.schema.fee import Fee
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
|
|
|
|
class ClaimDict(OrderedDict):
|
2018-12-15 07:25:46 +01:00
|
|
|
def __init__(self, claim_dict=None, detached_signature: Signature=None):
|
2019-03-06 03:37:44 +01:00
|
|
|
if isinstance(claim_dict, legacy_claim_pb2.Claim):
|
2018-09-17 22:13:30 +02:00
|
|
|
raise Exception("To initialize %s with a Claim protobuf use %s.load_protobuf" %
|
|
|
|
(self.__class__.__name__, self.__class__.__name__))
|
2018-12-15 07:25:46 +01:00
|
|
|
self.detached_signature = detached_signature
|
2018-09-17 22:13:30 +02:00
|
|
|
OrderedDict.__init__(self, claim_dict or [])
|
|
|
|
|
|
|
|
@property
|
|
|
|
def protobuf_dict(self):
|
|
|
|
"""Claim dictionary using base64 to represent bytes"""
|
|
|
|
|
|
|
|
return json.loads(json_format.MessageToJson(self.protobuf, True))
|
|
|
|
|
|
|
|
@property
|
|
|
|
def protobuf(self):
|
|
|
|
"""Claim message object"""
|
|
|
|
|
2019-03-14 00:47:13 +01:00
|
|
|
return LegacyClaim.load(self)
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def serialized(self):
|
|
|
|
"""Serialized Claim protobuf"""
|
2019-01-18 01:24:24 +01:00
|
|
|
if self.detached_signature:
|
2019-01-05 10:02:43 +01:00
|
|
|
return self.detached_signature.serialized
|
2018-09-17 22:13:30 +02:00
|
|
|
return self.protobuf.SerializeToString()
|
|
|
|
|
|
|
|
@property
|
|
|
|
def serialized_no_signature(self):
|
|
|
|
"""Serialized Claim protobuf without publisherSignature field"""
|
|
|
|
claim = self.protobuf
|
|
|
|
claim.ClearField("publisherSignature")
|
|
|
|
return ClaimDict.load_protobuf(claim).serialized
|
|
|
|
|
|
|
|
@property
|
|
|
|
def has_signature(self):
|
2019-01-18 01:24:24 +01:00
|
|
|
return self.protobuf.HasField("publisherSignature") or (
|
|
|
|
self.detached_signature and self.detached_signature.raw_signature
|
|
|
|
)
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def is_certificate(self):
|
|
|
|
claim = self.protobuf
|
|
|
|
return CLAIM_TYPE_NAMES[claim.claimType] == "certificate"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def is_stream(self):
|
|
|
|
claim = self.protobuf
|
|
|
|
return CLAIM_TYPE_NAMES[claim.claimType] == "stream"
|
|
|
|
|
|
|
|
@property
|
|
|
|
def source_hash(self):
|
|
|
|
claim = self.protobuf
|
|
|
|
if not CLAIM_TYPE_NAMES[claim.claimType] == "stream":
|
|
|
|
return None
|
|
|
|
return binascii.hexlify(claim.stream.source.source)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def has_fee(self):
|
|
|
|
claim = self.protobuf
|
|
|
|
if not CLAIM_TYPE_NAMES[claim.claimType] == "stream":
|
|
|
|
return None
|
|
|
|
if claim.stream.metadata.HasField("fee"):
|
|
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
|
|
@property
|
|
|
|
def source_fee(self):
|
|
|
|
claim = self.protobuf
|
|
|
|
if not CLAIM_TYPE_NAMES[claim.claimType] == "stream":
|
|
|
|
return None
|
|
|
|
if claim.stream.metadata.HasField("fee"):
|
|
|
|
return Fee.load_protobuf(claim.stream.metadata.fee)
|
|
|
|
return None
|
|
|
|
|
|
|
|
@property
|
2019-01-18 03:37:37 +01:00
|
|
|
def certificate_id(self) -> str:
|
2019-01-18 01:24:24 +01:00
|
|
|
if self.protobuf.HasField("publisherSignature"):
|
2019-01-18 03:37:37 +01:00
|
|
|
return binascii.hexlify(self.protobuf.publisherSignature.certificateId).decode()
|
2019-01-05 10:02:43 +01:00
|
|
|
if self.detached_signature and self.detached_signature.certificate_id:
|
2019-01-18 03:37:37 +01:00
|
|
|
return binascii.hexlify(self.detached_signature.certificate_id).decode()
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
@property
|
|
|
|
def signature(self):
|
|
|
|
if not self.has_signature:
|
|
|
|
return None
|
|
|
|
return binascii.hexlify(self.protobuf.publisherSignature.signature)
|
|
|
|
|
|
|
|
@property
|
|
|
|
def protobuf_len(self):
|
|
|
|
"""Length of serialized string"""
|
|
|
|
|
|
|
|
return self.protobuf.ByteSize()
|
|
|
|
|
|
|
|
@property
|
|
|
|
def json_len(self):
|
|
|
|
"""Length of json encoded string"""
|
|
|
|
|
|
|
|
return len(json.dumps(self.claim_dict))
|
|
|
|
|
|
|
|
@property
|
|
|
|
def claim_dict(self):
|
|
|
|
"""Claim dictionary with bytes represented as hex and base58"""
|
|
|
|
|
2019-01-18 02:20:47 +01:00
|
|
|
return dict(encode_fields(self, self.detached_signature))
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
@classmethod
|
2018-12-15 07:25:46 +01:00
|
|
|
def load_protobuf_dict(cls, protobuf_dict, detached_signature=None):
|
2018-09-17 22:13:30 +02:00
|
|
|
"""
|
|
|
|
Load a ClaimDict from a dictionary with base64 encoded bytes
|
|
|
|
(as returned by the protobuf json formatter)
|
|
|
|
"""
|
|
|
|
|
2018-12-15 07:25:46 +01:00
|
|
|
return cls(decode_b64_fields(protobuf_dict), detached_signature=detached_signature)
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
@classmethod
|
2018-12-15 07:25:46 +01:00
|
|
|
def load_protobuf(cls, protobuf_claim, detached_signature=None):
|
2018-09-17 22:13:30 +02:00
|
|
|
"""Load ClaimDict from a protobuf Claim message"""
|
2018-12-15 07:25:46 +01:00
|
|
|
return cls.load_protobuf_dict(json.loads(json_format.MessageToJson(protobuf_claim, True)), detached_signature)
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def load_dict(cls, claim_dict):
|
|
|
|
"""Load ClaimDict from a dictionary with hex and base58 encoded bytes"""
|
|
|
|
try:
|
2019-01-18 02:20:47 +01:00
|
|
|
claim_dict, detached_signature = decode_fields(claim_dict)
|
|
|
|
return cls.load_protobuf(cls(claim_dict).protobuf, detached_signature)
|
2018-09-17 22:13:30 +02:00
|
|
|
except json_format.ParseError as err:
|
|
|
|
raise DecodeError(str(err))
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def deserialize(cls, serialized):
|
|
|
|
"""Load a ClaimDict from a serialized protobuf string"""
|
2019-01-05 10:02:43 +01:00
|
|
|
detached_signature = Signature.flagged_parse(serialized)
|
2018-09-17 22:13:30 +02:00
|
|
|
|
2019-03-06 03:37:44 +01:00
|
|
|
temp_claim = legacy_claim_pb2.Claim()
|
2018-09-17 22:13:30 +02:00
|
|
|
try:
|
2019-01-05 10:02:43 +01:00
|
|
|
temp_claim.ParseFromString(detached_signature.payload)
|
2018-09-17 22:13:30 +02:00
|
|
|
except DecodeError_pb:
|
|
|
|
raise DecodeError(DecodeError_pb)
|
2018-12-15 07:25:46 +01:00
|
|
|
return cls.load_protobuf(temp_claim, detached_signature=detached_signature)
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
@classmethod
|
2018-12-31 05:08:02 +01:00
|
|
|
def generate_certificate(cls, private_key, curve=SECP256k1):
|
2018-09-17 22:13:30 +02:00
|
|
|
signer = get_signer(curve).load_pem(private_key)
|
|
|
|
return cls.load_protobuf(signer.certificate)
|
|
|
|
|
2019-01-04 20:06:18 +01:00
|
|
|
def sign(self, private_key, claim_address, cert_claim_id, curve=SECP256k1, name=None, force_detached=False):
|
2018-09-17 22:13:30 +02:00
|
|
|
signer = get_signer(curve).load_pem(private_key)
|
2019-01-04 20:06:18 +01:00
|
|
|
signed, signature = signer.sign_stream_claim(self, claim_address, cert_claim_id, name, force_detached)
|
2018-12-31 01:23:03 +01:00
|
|
|
return ClaimDict.load_protobuf(signed, signature)
|
2018-09-17 22:13:30 +02:00
|
|
|
|
2018-12-15 07:25:46 +01:00
|
|
|
def validate_signature(self, claim_address, certificate, name=None):
|
2018-09-17 22:13:30 +02:00
|
|
|
if isinstance(certificate, ClaimDict):
|
|
|
|
certificate = certificate.protobuf
|
|
|
|
curve = CURVE_NAMES[certificate.certificate.keyType]
|
|
|
|
validator = get_validator(curve).load_from_certificate(certificate, self.certificate_id)
|
2018-12-31 01:23:03 +01:00
|
|
|
return validator.validate_claim_signature(self, claim_address, name)
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
def validate_private_key(self, private_key, certificate_id):
|
|
|
|
certificate = self.protobuf
|
|
|
|
if CLAIM_TYPE_NAMES[certificate.claimType] != "certificate":
|
|
|
|
return
|
|
|
|
curve = CURVE_NAMES[certificate.certificate.keyType]
|
|
|
|
validator = get_validator(curve).load_from_certificate(certificate, certificate_id)
|
|
|
|
signing_key = validator.signing_key_from_pem(private_key)
|
|
|
|
return validator.validate_private_key(signing_key)
|
|
|
|
|
|
|
|
def get_validator(self, certificate_id):
|
|
|
|
"""
|
2018-09-17 22:31:44 +02:00
|
|
|
Get a lbrynet.schema.validator.Validator object for a certificate claim
|
2018-09-17 22:13:30 +02:00
|
|
|
|
|
|
|
:param certificate_id: claim id of this certificate claim
|
2018-09-17 22:31:44 +02:00
|
|
|
:return: None or lbrynet.schema.validator.Validator object
|
2018-09-17 22:13:30 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
claim = self.protobuf
|
|
|
|
if CLAIM_TYPE_NAMES[claim.claimType] != "certificate":
|
|
|
|
return
|
|
|
|
curve = CURVE_NAMES[claim.certificate.keyType]
|
|
|
|
return get_validator(curve).load_from_certificate(claim, certificate_id)
|
2019-03-14 00:47:13 +01:00
|
|
|
|
|
|
|
|
|
|
|
class Schema(Message):
|
|
|
|
@classmethod
|
|
|
|
def load(cls, message):
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def _load(cls, data, message):
|
|
|
|
if isinstance(data, dict):
|
|
|
|
data = json.dumps(data)
|
|
|
|
return json_pb.Parse(data, message)
|
|
|
|
|
|
|
|
|
|
|
|
class Claim(Schema):
|
|
|
|
CLAIM_TYPE_STREAM = 0 #fixme: 0 is unset, should be fixed on proto file to be 1 and 2!
|
|
|
|
CLAIM_TYPE_CERT = 1
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def load(cls, message: dict):
|
|
|
|
_claim = deepcopy(message)
|
|
|
|
_message_pb = claim_pb.Claim()
|
|
|
|
|
|
|
|
if "certificate" in _claim: # old protobuf, migrate
|
|
|
|
_cert = _claim.pop("certificate")
|
|
|
|
assert isinstance(_cert, dict)
|
|
|
|
_message_pb.type = Claim.CLAIM_TYPE_CERT
|
|
|
|
_message_pb.channel.MergeFrom(claim_pb.Channel(public_key=_cert.pop("publicKey")))
|
|
|
|
_claim = {} # so we dont need to know what other fields we ignored
|
|
|
|
elif "channel" in _claim:
|
|
|
|
_channel = _claim.pop("channel")
|
|
|
|
_message_pb.type = Claim.CLAIM_TYPE_CERT
|
|
|
|
_message_pb.channel = claim_pb.Channel(**_channel)
|
|
|
|
elif "stream" in _claim:
|
|
|
|
_message_pb.type = Claim.CLAIM_TYPE_STREAM
|
|
|
|
_stream = _claim.pop("stream")
|
|
|
|
if "source" in _stream:
|
|
|
|
_source = _stream.pop("source")
|
|
|
|
_message_pb.stream.hash = _source.get("source", b'') # fixme: fail if empty?
|
|
|
|
_message_pb.stream.media_type = _source.pop("contentType")
|
|
|
|
if "metadata" in _stream:
|
|
|
|
_metadata = _stream.pop("metadata")
|
|
|
|
_message_pb.stream.license = _metadata.get("license")
|
|
|
|
_message_pb.stream.description = _metadata.get("description")
|
|
|
|
_message_pb.stream.language = _metadata.get("language")
|
|
|
|
_message_pb.stream.title = _metadata.get("title")
|
|
|
|
_message_pb.stream.author = _metadata.get("author")
|
|
|
|
_message_pb.stream.license_url = _metadata.get("licenseUrl")
|
|
|
|
_message_pb.stream.thumbnail_url = _metadata.get("thumbnail")
|
|
|
|
if _metadata.get("nsfw"):
|
|
|
|
_message_pb.stream.tags.append("nsfw")
|
|
|
|
if "fee" in _metadata:
|
|
|
|
_message_pb.stream.fee.address = _metadata["fee"]["address"]
|
|
|
|
_message_pb.stream.fee.currency = {
|
|
|
|
"LBC": 0,
|
|
|
|
"USD": 1
|
|
|
|
}[_metadata["fee"]["currency"]]
|
|
|
|
multiplier = COIN if _metadata["fee"]["currency"] == "LBC" else 100
|
|
|
|
total = int(_metadata["fee"]["amount"]*multiplier)
|
|
|
|
_message_pb.stream.fee.amount = total if total >= 0 else 0
|
|
|
|
_claim = {}
|
|
|
|
else:
|
|
|
|
raise AttributeError
|
|
|
|
|
|
|
|
return cls._load(_claim, _message_pb)
|