Skip to content

Commit c6deb6b

Browse files
authored
Merge branch 'main' into feat/get-public-key-ed25519-tests
2 parents 72b2aa0 + c3a7a44 commit c6deb6b

File tree

7 files changed

+87
-115
lines changed

7 files changed

+87
-115
lines changed

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.7.0]
11+
12+
### Added
13+
14+
- feat: getPublicKeyEd25519 ([#26](https://github.com/MetaMask/native-utils/pull/26))
15+
1016
## [0.6.0]
1117

1218
### Added
@@ -44,7 +50,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4450
- feat: add native public key generation ([#5](https://github.com/MetaMask/native-utils/pull/5))
4551
- feat: add Example app with tests cases ([#6](https://github.com/MetaMask/native-utils/pull/6))
4652

47-
[Unreleased]: https://github.com/MetaMask/native-utils/compare/v0.6.0...HEAD
53+
[Unreleased]: https://github.com/MetaMask/native-utils/compare/v0.7.0...HEAD
54+
[0.7.0]: https://github.com/MetaMask/native-utils/compare/v0.6.0...v0.7.0
4855
[0.6.0]: https://github.com/MetaMask/native-utils/compare/v0.5.0...v0.6.0
4956
[0.5.0]: https://github.com/MetaMask/native-utils/compare/v0.4.0...v0.5.0
5057
[0.4.0]: https://github.com/MetaMask/native-utils/compare/v0.3.0...v0.4.0

cpp/HybridNativeUtils.cpp

Lines changed: 52 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,77 +1,95 @@
11
#include "HybridNativeUtils.hpp"
2-
#include "secp256k1_wrapper.h"
2+
#include "secp256k1/include/secp256k1.h"
33
#include "hex_utils.hpp"
44
#include "botan_conditional.h"
55
#include <stdexcept>
6+
#include <mutex>
67

78
namespace margelo::nitro::metamask_nativeutils {
89

9-
// Static global context for maximum performance
10-
static secp256k1_context* g_ctx = nullptr;
10+
// Static global context for maximum performance.
11+
// Made const and initialized with a call-once guard for thread safety.
12+
static std::once_flag g_ctx_once;
13+
static const secp256k1_context* g_ctx = nullptr;
1114

12-
// Initialize context once (thread-safe)
1315
static void initializeContext() {
16+
std::call_once(g_ctx_once, []() {
17+
g_ctx = secp256k1_context_create(SECP256K1_CONTEXT_NONE);
18+
});
19+
1420
if (!g_ctx) {
15-
g_ctx = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
21+
throw std::runtime_error("Failed to initialize secp256k1 context");
1622
}
1723
}
1824

25+
// Helper that wraps secp256k1_ec_pubkey_serialize and enforces the expected output length.
26+
// libsecp256k1 treats the length parameter as an in/out value: on input it is the buffer
27+
// capacity, on output it is the actual number of bytes written. We defensively verify that
28+
// the actual length matches the format we requested (33 or 65 bytes) so that future changes
29+
// in libsecp256k1 cannot cause us to read uninitialized or truncated public key data.
30+
static void serializeSecp256k1PubkeyChecked(
31+
const secp256k1_pubkey* pubkey,
32+
uint8_t* output,
33+
size_t expectedLen,
34+
unsigned int flags) {
35+
size_t outputLen = expectedLen;
36+
if (!secp256k1_ec_pubkey_serialize(g_ctx, output, &outputLen, pubkey, flags)) {
37+
throw std::runtime_error("Failed to serialize public key");
38+
}
39+
if (outputLen != expectedLen) {
40+
throw std::runtime_error("Unexpected public key length from secp256k1");
41+
}
42+
}
1943

2044
// Common function to generate public key from raw private key bytes
2145
static std::shared_ptr<ArrayBuffer> generatePublicKeyFromBytes(const uint8_t* privateKeyBytes, bool isCompressed) {
2246
initializeContext();
2347

2448
// Use secp256k1's built-in validation (checks if key is not 0 and < curve order)
2549
if (!secp256k1_ec_seckey_verify(g_ctx, privateKeyBytes)) {
26-
throw std::runtime_error("private key invalid 3");
50+
throw std::runtime_error("Private key is invalid");
2751
}
2852

2953
// Create public key from private key
3054
secp256k1_pubkey pubkey;
3155
if (!secp256k1_ec_pubkey_create(g_ctx, &pubkey, privateKeyBytes)) {
32-
throw std::runtime_error("private key invalid 3");
56+
throw std::runtime_error("Failed to create public key from private key");
3357
}
3458

3559
// Serialize the public key
3660
size_t keySize = isCompressed ? 33 : 65;
3761
auto buffer = ArrayBuffer::allocate(keySize);
3862
auto data = static_cast<uint8_t*>(buffer->data());
39-
40-
size_t outputLen = keySize;
63+
4164
unsigned int flags = isCompressed ? SECP256K1_EC_COMPRESSED : SECP256K1_EC_UNCOMPRESSED;
42-
43-
if (!secp256k1_ec_pubkey_serialize(g_ctx, data, &outputLen, &pubkey, flags)) {
44-
throw std::runtime_error("private key invalid 3");
45-
}
65+
66+
serializeSecp256k1PubkeyChecked(&pubkey, data, keySize, flags);
4667

4768
return buffer;
4869
}
4970

5071
std::shared_ptr<ArrayBuffer> HybridNativeUtils::toPublicKey(const std::string& privateKey, bool isCompressed) {
51-
std::string hex = privateKey;
52-
5372
// Must be exactly 64 characters (32 bytes)
54-
if (hex.length() != 64) {
55-
throw std::runtime_error("Uint8Array expected");
73+
if (privateKey.length() != 64) {
74+
throw std::runtime_error("Private key must be 64 hex characters (32 bytes)");
5675
}
5776

58-
// Convert hex to bytes with validation
59-
uint8_t seckey[32];
60-
hexToBytes(hex, seckey, 32);
77+
uint8_t privateKeyBytes[32];
78+
hexToBytes(privateKey, privateKeyBytes, 32);
6179

62-
return generatePublicKeyFromBytes(seckey, isCompressed);
80+
return generatePublicKeyFromBytes(privateKeyBytes, isCompressed);
6381
}
6482

6583
std::shared_ptr<ArrayBuffer> HybridNativeUtils::toPublicKeyFromBytes(const std::shared_ptr<ArrayBuffer>& privateKey, bool isCompressed) {
6684
// Validate input size (must be exactly 32 bytes for secp256k1)
6785
if (privateKey->size() != 32) {
68-
throw std::runtime_error("Uint8Array expected");
86+
throw std::runtime_error("Private key must be 32 bytes");
6987
}
7088

7189
// Get the private key bytes directly
72-
const uint8_t* seckey = static_cast<const uint8_t*>(privateKey->data());
90+
const uint8_t* privateKeyBytes = static_cast<const uint8_t*>(privateKey->data());
7391

74-
return generatePublicKeyFromBytes(seckey, isCompressed);
92+
return generatePublicKeyFromBytes(privateKeyBytes, isCompressed);
7593
}
7694

7795
// Common function to generate ed25519 public key from private key bytes (seed)
@@ -93,6 +111,9 @@ std::shared_ptr<ArrayBuffer> HybridNativeUtils::getPublicKeyEd25519(const std::s
93111
}
94112

95113
std::shared_ptr<ArrayBuffer> HybridNativeUtils::getPublicKeyEd25519FromBytes(const std::shared_ptr<ArrayBuffer>& privateKey) {
114+
if (privateKey->size() != 32) {
115+
throw std::runtime_error("Private key must be 32 bytes");
116+
}
96117
const uint8_t* seed = static_cast<const uint8_t*>(privateKey->data());
97118

98119
return generateEd25519PublicKeyFromBytes(seed);
@@ -106,7 +127,6 @@ static std::shared_ptr<ArrayBuffer> keccak256Hash(const uint8_t* dataBytes, size
106127

107128
hasher->update(dataBytes, dataLen);
108129

109-
// Convert hex string to bytes
110130
auto result = ArrayBuffer::allocate(32);
111131
hasher->final(static_cast<uint8_t*>(result->data()));
112132

@@ -121,10 +141,7 @@ std::shared_ptr<ArrayBuffer> HybridNativeUtils::keccak256(const std::string& dat
121141
auto dataBuffer = ArrayBuffer::allocate(dataLen);
122142
uint8_t* dataBytes = static_cast<uint8_t*>(dataBuffer->data());
123143

124-
// Convert hex string to bytes
125-
for (size_t i = 0; i < dataLen; i++) {
126-
dataBytes[i] = (hexCharToByte(data[i * 2]) << 4) | hexCharToByte(data[i * 2 + 1]);
127-
}
144+
hexToBytes(data, dataBytes, dataLen);
128145

129146
return keccak256FromBytes(dataBuffer);
130147
}
@@ -156,10 +173,11 @@ std::shared_ptr<ArrayBuffer> HybridNativeUtils::pubToAddress(const std::shared_p
156173

157174
// Serialize to uncompressed format (65 bytes)
158175
uint8_t uncompressedKey[65];
159-
size_t outputLen = 65;
160-
if (!secp256k1_ec_pubkey_serialize(g_ctx, uncompressedKey, &outputLen, &parsedPubkey, SECP256K1_EC_UNCOMPRESSED)) {
161-
throw std::runtime_error("Failed to serialize public key");
162-
}
176+
serializeSecp256k1PubkeyChecked(
177+
&parsedPubkey,
178+
uncompressedKey,
179+
65,
180+
SECP256K1_EC_UNCOMPRESSED);
163181

164182
// Skip the 0x04 prefix byte for keccak hashing
165183
memcpy(uncompressedPubKeyBytes, uncompressedKey + 1, 64);
@@ -171,7 +189,7 @@ std::shared_ptr<ArrayBuffer> HybridNativeUtils::pubToAddress(const std::shared_p
171189
}
172190
}
173191

174-
auto hashResult = keccak256Hash(pubKeyBytes, pubKeySize);
192+
auto hashResult = keccak256Hash(pubKeyBytes, 64);
175193

176194
// Return the last 20 bytes (Ethereum address)
177195
auto result = ArrayBuffer::allocate(20);
@@ -181,7 +199,6 @@ std::shared_ptr<ArrayBuffer> HybridNativeUtils::pubToAddress(const std::shared_p
181199
}
182200

183201
std::shared_ptr<ArrayBuffer> HybridNativeUtils::hmacSha512(const std::shared_ptr<ArrayBuffer>& key, const std::shared_ptr<ArrayBuffer>& data) {
184-
// Get key and data pointers
185202
const uint8_t* keyBytes = static_cast<const uint8_t*>(key->data());
186203
const uint8_t* dataBytes = static_cast<const uint8_t*>(data->data());
187204

cpp/hex_utils.cpp

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ bool isValidHexChar(char c) {
99

1010
void validateHexString(const std::string& hex) {
1111
if (hex.length() % 2 != 0) {
12-
throw std::runtime_error("hex invalid");
12+
throw std::runtime_error("Invalid hex string: odd length");
1313
}
1414

1515
for (char c : hex) {
1616
if (!isValidHexChar(c)) {
17-
throw std::runtime_error("hex invalid");
17+
throw std::runtime_error("Invalid hex string: contains non-hex characters");
1818
}
1919
}
2020
}
@@ -23,14 +23,14 @@ uint8_t hexCharToByte(char c) {
2323
if (c >= '0' && c <= '9') return c - '0';
2424
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
2525
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
26-
throw std::runtime_error("hex invalid");
26+
throw std::runtime_error("Invalid hex character");
2727
}
2828

2929
void hexToBytes(const std::string& hex, uint8_t* bytes, size_t expectedLen) {
3030
validateHexString(hex);
3131

3232
if (hex.length() != expectedLen * 2) {
33-
throw std::runtime_error("Uint8Array expected");
33+
throw std::runtime_error("Invalid hex string length");
3434
}
3535

3636
for (size_t i = 0; i < expectedLen; i++) {

cpp/secp256k1_wrapper.h

Lines changed: 0 additions & 63 deletions
This file was deleted.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@metamask/native-utils",
3-
"version": "0.6.0",
3+
"version": "0.7.0",
44
"description": "React Native Utils for MetaMask. This project is under development and that individuals should use it at their own risk.",
55
"homepage": "https://github.com/MetaMask/native-utils",
66
"bugs": {

src/index.tsx

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { NitroModules } from 'react-native-nitro-modules';
22
import type { NativeUtils } from './NativeUtils.nitro';
33
import {
4-
bigintToBytes,
4+
bigintPrivateKeyToBytes,
55
uint8ArrayToArrayBuffer,
66
arrayBufferToUint8Array,
77
numberArrayToUint8Array,
@@ -14,12 +14,12 @@ export function multiply(a: number, b: number): number {
1414
return NativeUtilsHybridObject.multiply(a, b);
1515
}
1616

17-
/** Uint8Array alias for compatibility */
18-
export type Bytes = Uint8Array;
19-
/** Hex-encoded string or Uint8Array. */
20-
export type Hex = Bytes | string;
21-
/** Private key can be hex string, Uint8Array, or bigint. */
22-
export type PrivKey = Hex | bigint;
17+
/** Uint8Array of private key bytes. */
18+
export type BytesPrivateKey = Uint8Array;
19+
/** Hex-encoded string of private key. */
20+
export type HexPrivateKey = string;
21+
/** Private key can be hex string, bytes, or bigint. */
22+
export type PrivateKey = HexPrivateKey | BytesPrivateKey | bigint;
2323

2424
/**
2525
* Generate a public key from a private key using the secp256k1 elliptic curve.
@@ -30,7 +30,7 @@ export type PrivKey = Hex | bigint;
3030
* @returns Uint8Array containing the public key bytes
3131
*/
3232
export function getPublicKey(
33-
privateKey: PrivKey,
33+
privateKey: PrivateKey,
3434
isCompressed: boolean = true,
3535
): Uint8Array {
3636
let result: ArrayBuffer;
@@ -40,7 +40,7 @@ export function getPublicKey(
4040
result = NativeUtilsHybridObject.toPublicKey(privateKey, isCompressed);
4141
} else if (typeof privateKey === 'bigint') {
4242
// Convert bigint to bytes (basic validation here, detailed validation in C++)
43-
const privateKeyBytes = bigintToBytes(privateKey);
43+
const privateKeyBytes = bigintPrivateKeyToBytes(privateKey);
4444
const privateKeyBuffer = uint8ArrayToArrayBuffer(privateKeyBytes);
4545
result = NativeUtilsHybridObject.toPublicKeyFromBytes(
4646
privateKeyBuffer,
@@ -144,7 +144,7 @@ export function hmacSha512(key: Uint8Array, data: Uint8Array): Uint8Array {
144144
* @returns Uint8Array containing 32-byte Ed25519 public key
145145
*/
146146
export function getPublicKeyEd25519(
147-
privateKey: Uint8Array | string,
147+
privateKey: BytesPrivateKey | HexPrivateKey,
148148
_compressed?: boolean,
149149
): Uint8Array {
150150
let result: ArrayBuffer;

src/utils.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export const N =
55
/**
66
* Convert bigint to 32-byte Uint8Array (big-endian)
77
*/
8-
export function bigintToBytes(num: bigint): Uint8Array {
8+
export function bigintPrivateKeyToBytes(num: bigint): Uint8Array {
99
if (num < 0n) {
1010
throw new Error('Private key must be positive');
1111
}
@@ -46,5 +46,16 @@ export function arrayBufferToUint8Array(buffer: ArrayBuffer): Uint8Array {
4646
* Convert number array to Uint8Array
4747
*/
4848
export function numberArrayToUint8Array(arr: number[]): Uint8Array {
49+
// Dev-only validation to catch silent conversions
50+
if (process.env.NODE_ENV !== 'production') {
51+
for (let i = 0; i < arr.length; i++) {
52+
const val = arr[i];
53+
if (val === undefined || !Number.isInteger(val) || val < 0 || val > 255) {
54+
throw new Error(
55+
`Invalid byte value at index ${i}: expected integer 0-255, got ${val}`,
56+
);
57+
}
58+
}
59+
}
4960
return new Uint8Array(arr);
5061
}

0 commit comments

Comments
 (0)