Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] PrivateKey backed by cryptography #13

Merged
merged 5 commits into from
May 22, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions bitforge/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from network import Network
from privkey import PrivateKey
from pubkey import PublicKey
from address import Address
from script import Script
from hdprivkey import HDPrivateKey
from network import Network
import network
import ecdsa
# from address import Address
# from script import Script
# from hdprivkey import HDPrivateKey
from unit import Unit
from uri import URI
# from uri import URI

import network
import privkey
import pubkey
import unit
# import uri
157 changes: 86 additions & 71 deletions bitforge/address.py
Original file line number Diff line number Diff line change
@@ -1,120 +1,135 @@
import binascii, collections
from enum import Enum
import collections

import network, utils
from network import Network
from encoding import *
from errors import *
# from script import Script
import enum

from bitforge import encoding, error, network

BaseAddress = collections.namedtuple('Address', ['phash', 'network', 'type'])

class Address(BaseAddress):
class Type(enum.Enum):
PublicKey = 'pubkey_hash'
Script = 'script_hash'


class Error(error.BitforgeError):
pass


class Type(Enum):
PublicKey = 'pubkeyhash'
Script = 'scripthash'
class InvalidEncoding(Error):
pass

class Error(BitforgeError):
pass

class UnknownNetwork(Error, Network.UnknownNetwork):
"No network for Address with an attribute '{key}' of value {value}"
class InvalidHashLength(Error, error.StringError):
"The address hash {string} should be 20 bytes long, not {length}"

class InvalidVersion(Error, NumberError):
"Failed to detect Address type and network from version number {number}"

class InvalidBase58h(Error, InvalidBase58h):
"The Address string {string} is not valid base58/check"
class InvalidType(Error, error.ObjectError):
"{object} is not a valid address type"

class InvalidHex(Error, InvalidHex):
"The Address string {string} is not valid hexadecimal"

class InvalidHashLength(Error, StringError):
"The address hash {string} should be 20 bytes long, not {length}"
BaseAddress = collections.namedtuple('Address', [
'hash',
'type',
'network',
])

class InvalidBinaryLength(Error, StringError):
"The binary address {string} should be 21 bytes long, not {length}"

class InvalidType(Error, ObjectError):
"Address type {object} is not an instance of Address.Type"
class Address(BaseAddress):
"""Bitcoin address."""

def __new__(cls, hash, type=Type.PublicKey, network=network.default):
"""TODO"""

if not isinstance(type, Type):
raise InvalidType(type)

def __new__(cls, phash, network = network.default, type = Type.PublicKey):
if not isinstance(type, Address.Type):
raise Address.InvalidType(type)
if len(hash) != 20:
raise InvalidHashLength(hash)

if len(phash) != 20:
raise Address.InvalidHashLength(phash)
return super(Address, cls).__new__(cls, hash, type, network)

return super(Address, cls).__new__(cls, phash, network, type)
@classmethod
def from_string(cls, string):
"""TODO"""

@staticmethod
def from_string(string):
try:
bytes = decode_base58h(string)
except InvalidBase58h:
raise Address.InvalidBase58h(string)
data = encoding.a2b_base58check(string)

except encoding.InvalidEncoding as e:
raise InvalidEncoding(e.message)

return cls.from_bytes(data)

return Address.from_bytes(bytes)
@classmethod
def from_bytes(cls, data):
"""TODO"""

@staticmethod
def from_bytes(bytes):
if len(bytes) != 21:
raise Address.InvalidBinaryLength(bytes)
if len(data) != 21:
raise InvalidEncoding('Invalid address length')

network, type = Address.classify_bytes(bytes)
type_, network_ = cls.classify_bytes(data)

return Address(bytes[1:], network, type)
return cls(data[1:], type_, network_)

@classmethod
def from_hex(cls, string):
"""TODO"""

@staticmethod
def from_hex(string):
try:
bytes = decode_hex(string)
except InvalidHex:
raise Address.InvalidHex(string)
data = encoding.a2b_hex(string)

except encoding.InvalidEncoding as e:
raise InvalidEncoding(e.message)

return cls.from_bytes(data)

@classmethod
def classify_bytes(cls, data):
"""TODO"""

return Address.from_bytes(bytes)
data = bytearray(data)
version = data[0]

@staticmethod
def classify_bytes(bytes):
version = decode_int(bytes[0])
network_ = network.Network.get_by_field('pubkey_hash_prefix', version, raises=False)
if network_ is not None:
return (Type.PublicKey, network_)

network = Network.get_by_field('pubkeyhash', version, raises = False)
if network is not None:
return (network, Address.Type.PublicKey)
network_ = network.Network.get_by_field('script_hash_prefix', version, raises=False)
if network_ is not None:
return (Type.Script, network_)

network = Network.get_by_field('scripthash', version, raises = False)
if network is not None:
return (network, Address.Type.Script)
raise InvalidEncoding('Invalid version number')

raise Address.InvalidVersion(version)
@classmethod
def from_public_key(cls, pubkey):
"""TODO"""

@staticmethod
def from_public_key(pubkey):
phash = ripemd160(sha256(pubkey.to_bytes()))
return Address(phash, pubkey.network, Address.Type.PublicKey)
return pubkey.address()

def to_bytes(self):
"""TODO"""

version = getattr(self.network, self.type.value)
return chr(version) + self.phash
return chr(version) + self.hash

def to_string(self):
return encode_base58h(self.to_bytes())
"""TODO"""

return encoding.b2a_base58check(self.to_bytes())

def to_hex(self):
return encode_hex(self.to_bytes())
"""TODO"""

return encoding.b2a_hex(self.to_bytes())

# TODO: all keys should be from the same network
# @staticmethod
# @classmethod
# def from_public_keys(pubkeys, threshold):
# return Address.from_script(
# Script.buildMultisigOut(pubkeys, threshold),
# pubkeys[0].network
# )

# @staticmethod
# @classmethod
# def from_script(script, network = networks.default):
# if not isinstance(script, Script):
# raise ValueError('Expected instance of Script, not %s' % script)
Expand Down
77 changes: 38 additions & 39 deletions bitforge/encoding.py
Original file line number Diff line number Diff line change
@@ -1,68 +1,67 @@
import utils, binascii, hashlib
from errors import StringError
import binascii
import hashlib

from bitforge import error
from bitforge.utils import encoding

class EncodingError(StringError):
"The string {string} is not properly encoded"

class Error(error.BitforgeError):
pass

class InvalidBase58h(EncodingError):
"The string {string} is not valid base58/check"

def b2a_hex(data):
"""Convert a byte buffer to an hexadecimal string."""

class InvalidHex(EncodingError):
"The string {string} is not valid hexadecimal"
return binascii.b2a_hex(data)


def encode_base58h(bytes):
return utils.encoding.b2a_hashed_base58(bytes)
def a2b_hex(string):
"""Convert an hexadecimal string to a byte buffer."""

if len(string) % 2 == 1:
string = '0' + string

def decode_base58h(string):
try:
return utils.encoding.a2b_hashed_base58(string)
return binascii.a2b_hex(string.encode('ascii'))
except TypeError:
raise Error('Invalid hexadecimal string')

except utils.encoding.EncodingError:
raise InvalidBase58h(string)

def b2i_bigendian(data):
"""Convert a big endian byte buffer to an unsigned big integer."""

def encode_int(integer, big_endian = True):
bytes = bytearray()
# Encoding and decoding from hexa appears to be way faster than manually
# decoding the buffer in python.
return int(b2a_hex(data), 16)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may raise an error, capture it and raise a bitforge.Error

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What error?


while integer > 0:
bytes.append(integer & 0xff)
integer >>= 8

if big_endian:
bytes.reverse()
def i2b_bigendian(number, num_bytes = 0):
"""Convert an unsigned big integer to a zero-padded big endian byte buffer.
"""

return str(bytes)
# Encoding and decoding from hexa appears to be way faster than manually
# decoding the buffer in python.
return a2b_hex('%0*x' % (2 * num_bytes, number))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validate number is the right type and raise a bitforge.Error if doesn't.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we should check whether number is a non-negative integer? This is actually a wider discussion: should we check function argument types as a general strategy, like cryptography does, but contrary with the duck typing philosophy?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, this is a wider discussion. I believe we should embrace a fail early philosophy in bitforge's core classes. IMO this is specially valuable on security sensitive libraries.

@slezica I would like to hear you input on this regard.



def decode_int(bytes, big_endian = True):
if not big_endian:
bytes = reversed(bytes)
# TODO: implement these functions

integer = 0
def b2a_base58check(data):
"""Convert a byte buffer to a base58check string."""

for char in bytes:
integer <<= 8
integer += ord(char)
return encoding.b2a_hashed_base58(data)

return integer

def a2b_base58check(string):
"""Convert a base58check string to a byte buffer."""

def encode_hex(bytes):
return binascii.hexlify(bytes)


def decode_hex(string):
try:
return binascii.unhexlify(string)
except:
# unhexlify() throws 2 different exceptions (length, and alphabet)
raise InvalidHex(string)
return encoding.a2b_hashed_base58(string)
except encoding.EncodingError:
raise Error('Invalid base58check string')


# TODO: these are not encodings, they shouldn't be here

def sha256(bytes):
return hashlib.sha256(bytes).digest()
Expand Down
11 changes: 8 additions & 3 deletions bitforge/error.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@

class BitforgeError(Exception):

def __init__(self, *args, **kwargs):
self.cause = kwargs.pop('cause', None)
self.prepare(*args, **kwargs)
self.message = self.__doc__.format(**self.__dict__)

def prepare(self):
pass
def prepare(self, message=None):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

message=None vs message = None ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pep8:

Don't use spaces around the = sign when used to indicate a keyword argument or a default parameter value.

if message is not None:
self.__doc__ = message

def __str__(self):
return self.message


class ObjectError(BitforgeError):

def prepare(self, object):
self.object = object


class StringError(BitforgeError):

def prepare(self, string):
self.string = repr(string)
self.length = len(string)


class NumberError(BitforgeError):

def prepare(self, number):
self.number = number


class KeyValueError(BitforgeError):

def prepare(self, key, value):
self.key = key
self.value = value
Loading