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 4 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
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
7 changes: 5 additions & 2 deletions bitforge/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class InvalidField(Error, error.StringError):
'aliases', # All the network aliases

# Cryptography parameters
# WARNING: only curves over prime fields are currently supported
'curve', # Elliptic curve used for the crypto
'hash_function', # Signature hashing function

Expand Down Expand Up @@ -90,6 +91,8 @@ def __new__(cls, **kwargs):
if getattr(other, field) == getattr(network, field):
raise InvalidNetwork(field, getattr(network, field))

cls._networks.append(network)

# Enforce uniqueness of the network aliases
for name in [network.name] + network.aliases:
if name in cls._networks_by_name:
Expand All @@ -108,7 +111,7 @@ def __str__(self):
testnet = Network(
name = 'testnet',
aliases = [],
curve = ec.SECP256K1,
curve = ec.SECP256K1(),
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't see the need of having the curve as a Network parameter. I'm most probably missing a use case here, what do you have in mind?

hash_function = hashes.SHA256,
pubkeyhash = 111,
wif_prefix = 239,
Expand All @@ -129,7 +132,7 @@ def __str__(self):
default = livenet = Network(
name = 'livenet',
aliases = ['mainnet', 'default'],
curve = ec.SECP256K1,
curve = ec.SECP256K1(),
hash_function = hashes.SHA256,
pubkeyhash = 0x00,
wif_prefix = 0x80,
Expand Down
33 changes: 15 additions & 18 deletions bitforge/opcode.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import sys, inspect
from numbers import Number
from errors import *
import sys
import inspect

from bitforge import error


# Below is a list of all *named* opcodes. Their values, integers in the
Expand Down Expand Up @@ -155,28 +156,26 @@

class Opcode(object):

class Error(BitforgeError):
class Error(error.BitforgeError):
pass

class UnknownOpcodeName(Error, StringError):
class UnknownOpcodeName(Error, error.StringError):
"No known operation named {string}"

class UnknownOpcodeNumber(Error, NumberError):
class UnknownOpcodeNumber(Error, error.NumberError):
"No known operation numbered {number}"

class InvalidConstPushLength(Error, StringError):
class InvalidConstPushLength(Error, error.StringError):
"No constant push opcode can push {length} bytes (only [1-75])"

class InvalidPushLength(Error, NumberError):
class InvalidPushLength(Error, error.NumberError):
"No Opcode can push {number} bytes"

class TypeError(Error, ObjectError):
class TypeError(Error, error.ObjectError):
"Opcodes are initialized from numbers and names, got object {object}"


opcode_number_to_name = {} # Filled after class definition


def __init__(self, number):
if not (0 <= number <= 255):
raise Opcode.UnknownOpcodeNumber(number)
Expand Down Expand Up @@ -218,7 +217,6 @@ def __eq__(self, other):
def __hash__(self):
return hash(self.number)


@staticmethod
def for_number(n):
if 0 <= n <= 16:
Expand All @@ -243,13 +241,13 @@ def const_push_for(length):
@staticmethod
def var_push_for(length):
if length < 1:
raise InvalidPushLength(length)
raise Opcode.InvalidPushLength(length)

for opcode in [OP_PUSHDATA1, OP_PUSHDATA2, OP_PUSHDATA4]:
if length <= Opcode.data_length_max(opcode):
return opcode

raise InvalidPushLength(length)
raise Opcode.InvalidPushLength(length)

@staticmethod
def push_for(length):
Expand Down Expand Up @@ -282,7 +280,6 @@ def data_length_nbytes(opcode):
}[opcode]



# Walk the OP_* variables, mapping them to their names and creating Opcode objs:
_module = sys.modules[__name__]

Expand All @@ -294,7 +291,7 @@ def data_length_nbytes(opcode):
# Replace integer values with actual Opcode instances:
setattr(_module, name, Opcode(number))

Opcode.opcode_number_to_name[OP_0] = 'OP_0' # shares number with OP_FALSE
Opcode.opcode_number_to_name[OP_1] = 'OP_1' # shares number with OP_TRUE
Opcode.opcode_number_to_name[OP_0] = 'OP_0' # shares number with OP_FALSE
Opcode.opcode_number_to_name[OP_1] = 'OP_1' # shares number with OP_TRUE

del _module # the expected use for this module is to import *
del _module # the expected use for this module is to import *
Loading