Skip to content
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
## 0.0.2

- Changed `bip32_serialze` str_len to a pointer which returns the final length of the
base58-encoded out string.
- Added some precautionary `sodium_memzero()` calls.
- Made `bip32_b58_encode()` and `bip32_b58_decode()` a public part of the API.
57 changes: 41 additions & 16 deletions bip32.c
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ int bip32_from_seed(bip32_key *key, const unsigned char *seed, size_t seed_len)
memcpy(key->chain_code, output + BIP32_PRIVKEY_SIZE, BIP32_CHAINCODE_SIZE);

exit:
sodium_memzero(output, crypto_auth_hmacsha512_BYTES);
secp256k1_context_destroy(ctx);
return retcode;
}
Expand Down Expand Up @@ -164,6 +165,9 @@ int bip32_index_derive(bip32_key *target, const bip32_key *source, uint32_t inde

bip32_hmac_sha512(output, source->chain_code, BIP32_CHAINCODE_SIZE, hmac_msg, hmac_msg_len);

// hmac_msg potentially contains privkey bytes.
sodium_memzero(hmac_msg, hmac_msg_len);

memcpy(target->chain_code, output + BIP32_PRIVKEY_SIZE, BIP32_CHAINCODE_SIZE);

if (source->is_private) {
Expand Down Expand Up @@ -222,19 +226,6 @@ int bip32_index_derive(bip32_key *target, const bip32_key *source, uint32_t inde
return retcode;
}


// Returns true if invalid path characters are detected in a path string.
static bool has_invalid_path_characters(const char* str) {
const char* valid = "m/0123456789hH'pP";
while (*str) {
if (!strchr(valid, *str)) {
return true;
}
str++;
}
return false;
}

int bip32_derive_from_str(bip32_key* target, const char* source, const char* path) {
if (!target || !source || !path || strncmp(path, "m", 1) != 0) {
return 0;
Expand All @@ -250,6 +241,7 @@ int bip32_derive_from_str(bip32_key* target, const char* source, const char* pat
strncmp(source, "xpub", 4) == 0 ||
strncmp(source, "tpub", 4) == 0) {
if (!bip32_deserialize(&basekey, source, strlen(source))) {
sodium_memzero(&basekey, sizeof(bip32_key));
return 0;
}
}
Expand All @@ -261,6 +253,7 @@ int bip32_derive_from_str(bip32_key* target, const char* source, const char* pat
return 0;
}
if (!bip32_from_seed(&basekey, seedbytes, bin_len)) {
sodium_memzero(&basekey, sizeof(bip32_key));
return 0;
}
} else {
Expand All @@ -269,6 +262,7 @@ int bip32_derive_from_str(bip32_key* target, const char* source, const char* pat

if (bip32_derive(&basekey, path)) {
memcpy(target, &basekey, sizeof(bip32_key));
sodium_memzero(&basekey, sizeof(bip32_key));
return 1;
}
return 0;
Expand All @@ -284,6 +278,18 @@ int bip32_derive_from_seed(bip32_key* target, const unsigned char* seed, size_t
return 0;
}

// Returns true if invalid path characters are detected in a path string.
static bool has_invalid_path_characters(const char* str) {
const char* valid = "m/0123456789hH'pP";
while (*str) {
if (!strchr(valid, *str)) {
return true;
}
str++;
}
return false;
}

// Do an in-place derivation on `key`.
int bip32_derive(bip32_key* key, const char* path) {
if (!path || strncmp(path, "m", 1) != 0 || has_invalid_path_characters(path)) {
Expand Down Expand Up @@ -314,6 +320,7 @@ int bip32_derive(bip32_key* key, const char* path) {
if (bip32_index_derive(key, &tmp, path_index) != 1) {
return 0;
}
sodium_memzero(&tmp, sizeof(bip32_key));
p = strchr(end, '/');
}

Expand All @@ -323,7 +330,7 @@ int bip32_derive(bip32_key* key, const char* path) {
#define SER_SIZE 78
#define SER_PLUS_CHECKSUM_SIZE (SER_SIZE + 4)

int bip32_serialize(const bip32_key *key, char *str, size_t str_len) {
int bip32_serialize(const bip32_key *key, char *str, size_t* str_len) {
unsigned char data[SER_PLUS_CHECKSUM_SIZE];
uint32_t version;

Expand Down Expand Up @@ -362,7 +369,10 @@ int bip32_serialize(const bip32_key *key, char *str, size_t str_len) {
bip32_sha256_double(hash, data, 78);
memcpy(data + SER_SIZE, hash, 4);

return b58enc(str, &str_len, data, SER_PLUS_CHECKSUM_SIZE);
bool b58_ok = bip32_b58_encode(str, str_len, data, SER_PLUS_CHECKSUM_SIZE);
sodium_memzero(data, SER_PLUS_CHECKSUM_SIZE);

return b58_ok ? 1 : 0;
}

#define BIP32_BASE58_BYTES_LEN 82
Expand All @@ -371,7 +381,8 @@ int bip32_deserialize(bip32_key *key, const char *str, const size_t str_len) {
unsigned char data[BIP32_BASE58_BYTES_LEN];
size_t data_len = BIP32_BASE58_BYTES_LEN;

if (!b58tobin(data, &data_len, str, str_len) || data_len != BIP32_BASE58_BYTES_LEN) {
if (!bip32_b58_decode(data, &data_len, str, str_len) || data_len != BIP32_BASE58_BYTES_LEN) {
sodium_memzero(data, BIP32_BASE58_BYTES_LEN);
return 0;
}

Expand Down Expand Up @@ -426,16 +437,22 @@ int bip32_deserialize(bip32_key *key, const char *str, const size_t str_len) {

if (key->is_private) {
if (data[45] != 0) {
sodium_memzero(data, BIP32_BASE58_BYTES_LEN);
secp256k1_context_destroy(ctx);
return 0;
}

memcpy(key->key.privkey, data + 46, BIP32_PRIVKEY_SIZE);
sodium_memzero(data, BIP32_BASE58_BYTES_LEN);

if (!secp256k1_ec_seckey_verify(ctx, key->key.privkey)) {
secp256k1_context_destroy(ctx);
return 0;
}
} else {
memcpy(key->key.pubkey, data + 45, BIP32_PUBKEY_SIZE);
sodium_memzero(data, BIP32_BASE58_BYTES_LEN);

secp256k1_pubkey pubkey;
if (!secp256k1_ec_pubkey_parse(ctx, &pubkey, key->key.pubkey, BIP32_PUBKEY_SIZE)) {
secp256k1_context_destroy(ctx);
Expand Down Expand Up @@ -484,3 +501,11 @@ void bip32_hmac_sha512(
crypto_auth_hmacsha512_update(&state, msg, msg_len);
crypto_auth_hmacsha512_final(&state, hmac_out);
}

bool bip32_b58_encode(char* str_out, size_t* out_size, const unsigned char* data, size_t data_size) {
return b58enc(str_out, out_size, data, data_size);
}

bool bip32_b58_decode(unsigned char* bin_out, size_t* out_size, const char* str_in, size_t str_size) {
return b58tobin(bin_out, out_size, str_in, str_size);
}
23 changes: 21 additions & 2 deletions bip32.h
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/**
* This API is generally designed to mimic the libsecp256k1 library, in terms of
* argument order and return conventions.
*/
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
Expand Down Expand Up @@ -60,11 +64,14 @@ int bip32_derive_from_str(bip32_key *target, const char* source, const char* pat
*/
int bip32_derive(bip32_key *target, const char* path);

/** Serialize a BIP32 key to its base58 string representation.
/** Serialize a BIP32 key to its base58 string representation. Writes the resulting
* string to `str`, and the length of the resulting string to `str_len`.
*
* `str_len` must initially be set to the maximum length of the `str` buffer.
*
* Returns 1 if successful.
*/
int bip32_serialize(const bip32_key *key, char *str, size_t str_len);
int bip32_serialize(const bip32_key *key, char *str, size_t* str_len);

/** Deserialize a BIP32 key from its base58 string representation.
*
Expand Down Expand Up @@ -106,6 +113,18 @@ void bip32_hmac_sha512(
size_t msg_len
);

/** Encode some bytes to a base58 string.
*
* Returns true if successful.
*/
bool bip32_b58_encode(char* str_out, size_t* out_size, const unsigned char* data, size_t data_size);

/** Decode some bytes from a base58 string.
*
* Returns true if successful.
*/
bool bip32_b58_decode(unsigned char* bin_out, size_t* out_size, const char* str_in, size_t str_size);

#ifdef __cplusplus
}
#endif
3 changes: 2 additions & 1 deletion examples/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ int main(int argc, char *argv[]) {
return 1;
}

if (!bip32_serialize(&key, serialized, sizeof(serialized))) {
size_t out_size;
if (!bip32_serialize(&key, serialized, &out_size)) {
fprintf(stderr, "Serialization failed\n");
return 1;
}
Expand Down
77 changes: 65 additions & 12 deletions examples/py/bindings.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,17 @@
from functools import lru_cache
from pathlib import Path
from ctypes import (
c_uint8, c_uint32, c_size_t, c_char_p, c_ubyte, c_void_p,
Structure, Union, POINTER, create_string_buffer
)
c_uint8,
c_uint32,
c_size_t,
c_char_p,
c_ubyte,
c_void_p,
Structure,
Union,
POINTER,
create_string_buffer,
byref)


@lru_cache
Expand All @@ -20,7 +28,9 @@ def get_bip32_module():
bip32_lib.bip32_init.argtypes = [POINTER(BIP32Key)]
bip32_lib.bip32_init.restype = None

bip32_lib.bip32_derive_from_seed.argtypes = [POINTER(BIP32Key), POINTER(c_ubyte), c_size_t, c_char_p]
bip32_lib.bip32_derive_from_seed.argtypes = [
POINTER(BIP32Key), POINTER(c_ubyte), c_size_t, c_char_p
]
bip32_lib.bip32_derive_from_seed.restype = ctypes.c_int

bip32_lib.bip32_derive_from_str.argtypes = [POINTER(BIP32Key), c_char_p, c_char_p]
Expand All @@ -29,7 +39,9 @@ def get_bip32_module():
bip32_lib.bip32_derive.argtypes = [POINTER(BIP32Key), c_char_p]
bip32_lib.bip32_derive.restype = ctypes.c_int

bip32_lib.bip32_serialize.argtypes = [POINTER(BIP32Key), c_char_p, c_size_t]
bip32_lib.bip32_serialize.argtypes = [
POINTER(BIP32Key), c_char_p, POINTER(c_size_t)
]
bip32_lib.bip32_serialize.restype = ctypes.c_bool

bip32_lib.bip32_deserialize.argtypes = [POINTER(BIP32Key), c_char_p, c_size_t]
Expand All @@ -38,14 +50,22 @@ def get_bip32_module():
bip32_lib.bip32_get_public.argtypes = [POINTER(BIP32Key), POINTER(BIP32Key)]
bip32_lib.bip32_get_public.restype = ctypes.c_int

bip32_lib.bip32_b58_encode.argtypes = [
c_char_p, POINTER(c_size_t), POINTER(c_ubyte), c_size_t
]
bip32_lib.bip32_b58_encode.restype = ctypes.c_bool

bip32_lib.bip32_b58_decode.argtypes = [
POINTER(c_ubyte), POINTER(c_size_t), c_char_p, c_size_t
]
bip32_lib.bip32_b58_decode.restype = ctypes.c_bool

return bip32_lib


class KeyUnion(Union):
_fields_ = [
('privkey', c_uint8 * 32),
('pubkey', c_uint8 * 33)
]
_fields_ = [('privkey', c_uint8 * 32), ('pubkey', c_uint8 * 33)]


class BIP32Key(Structure):
_fields_ = [
Expand All @@ -65,6 +85,7 @@ def print(self):


class BIP32:

def __init__(self):
self.key = BIP32Key()
self.bip32_lib = get_bip32_module()
Expand All @@ -80,7 +101,9 @@ def derive(self, path: str) -> 'BIP32':

def serialize(self):
buf = create_string_buffer(200) # Standard BIP32 serialization length
if not self.bip32_lib.bip32_serialize(self.key, buf, len(buf)):
out_len = c_size_t(len(buf))

if not self.bip32_lib.bip32_serialize(self.key, buf, byref(out_len)):
raise ValueError("Serialization failed")
return buf.value.decode()

Expand All @@ -106,7 +129,8 @@ def derive(source: str, path: str = 'm') -> BIP32:

"""
b = BIP32()
if not get_bip32_module().bip32_derive_from_str(b.key, source.encode(), path.encode()):
if not get_bip32_module().bip32_derive_from_str(
b.key, source.encode(), path.encode()):
raise ValueError("failed")
return b

Expand All @@ -115,6 +139,35 @@ def derive_from_seed(seed: bytes, path: str = 'm') -> BIP32:
b = BIP32()
c_seed = ctypes.c_char_p(seed)
seed_ptr = ctypes.cast(c_seed, POINTER(c_ubyte))
if not get_bip32_module().bip32_derive_from_seed(b.key, seed_ptr, len(seed), path.encode()):
if not get_bip32_module().bip32_derive_from_seed(
b.key, seed_ptr, len(seed), path.encode()):
raise ValueError("failed")
return b


def b58_encode(inp: bytes) -> str:
data_len = len(inp)
data_arr = (c_ubyte * data_len)(*inp)

out_size = c_size_t(data_len * 2)
str_out = ctypes.create_string_buffer(out_size.value)

if not get_bip32_module().bip32_b58_encode(
str_out, byref(out_size), data_arr, data_len):
raise ValueError("base58 encoding failed")

return str_out.value[:out_size.value].decode('utf-8')


def b58_decode(in_str: str) -> bytes:
str_bytes = c_char_p(in_str.encode('utf-8'))
str_len = len(in_str)

out_size = c_size_t(str_len * 2)
bin_out = (c_ubyte * out_size.value)()

if not get_bip32_module().bip32_b58_decode(
bin_out, byref(out_size), str_bytes, str_len):
raise ValueError("base58 decoding failed")

return bytes(bin_out[-out_size.value:])
29 changes: 28 additions & 1 deletion examples/py/test_fuzz_cross_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import pytest
from hypothesis import given, strategies as st, target, settings

from bindings import derive, derive_from_seed
from bindings import derive, derive_from_seed, b58_decode, b58_encode

log = logging.getLogger(__name__)
logging.basicConfig()
Expand Down Expand Up @@ -181,5 +181,32 @@ def test_xpub_impls(bip32_path):
assert ours == pys



@given(b58_data=st.binary(min_size=0, max_size=1000))
@settings(max_examples=1000)
def test_base58(b58_data: bytes):
if b58_data and len(b58_data) >= 2:
# TODO: figure out why the base58 impl is failing on example b':'
assert b58_decode(b58_encode(b58_data)) == b58_data


def test_base58_known_vectors():
cases = [
(bytes.fromhex(""), ""),
(bytes.fromhex("00"), "1"),
(bytes.fromhex("0000"), "11"),
(bytes.fromhex("68656c6c6f20776f726c64"), "StV1DL6CwTryKyV"),
(bytes.fromhex("0068656c6c6f20776f726c64"), "1StV1DL6CwTryKyV"),
(bytes.fromhex("000068656c6c6f20776f726c64"), "11StV1DL6CwTryKyV"),
]

for raw, encoded in cases:
if raw: # Skip empty input for encoding test
assert b58_encode(raw) == encoded

if encoded: # Skip empty input for decoding test
assert b58_decode(encoded) == raw


if __name__ == "__main__":
pytest.main([__file__, "-v", "--capture=no", "--hypothesis-show-statistics", "-x"] + sys.argv[1:])
Loading
Loading