Unverified Commit 8568eee7 authored by Maciej Hirsz's avatar Maciej Hirsz Committed by GitHub
Browse files

feat: Substrate stuff in Rust (#279)

* chore: Squashed old branch
* feat: expose blake2s hash function
* fix: Duplicate function names in iOS
* feat: Expose `substrateAddress` in native.js
* feat: sr25519 signing
* feat: Complete SURI derivation
* feat: Expose blake2s proxy function in native.js
* fix: Added react methods to EthkeyBridge.m
parent 3cb84de5
Pipeline #49433 failed with stage
in 13 seconds
......@@ -55,7 +55,11 @@ public class EthkeyBridge extends ReactContextBaseJavaModule {
@ReactMethod
public void brainWalletSign(String seed, String message, Promise promise) {
promise.resolve(ethkeyBrainwalletSign(seed, message));
try {
promise.resolve(ethkeyBrainwalletSign(seed, message));
} catch (Exception e) {
promise.reject("invalid phrase", "invalid phrase");
}
}
@ReactMethod
......@@ -73,6 +77,11 @@ public class EthkeyBridge extends ReactContextBaseJavaModule {
promise.resolve(ethkeyKeccak(data));
}
@ReactMethod
public void blake2s(String data, Promise promise) {
promise.resolve(ethkeyBlake(data));
}
@ReactMethod
public void ethSign(String data, Promise promise) {
promise.resolve(ethkeyEthSign(data));
......@@ -111,15 +120,46 @@ public class EthkeyBridge extends ReactContextBaseJavaModule {
}
}
@ReactMethod
public void qrCodeHex(String data, Promise promise) {
try {
promise.resolve(ethkeyQrCodeHex(data));
} catch (Exception e) {
promise.reject("failed to create QR code", "failed to create QR code");
}
}
@ReactMethod
public void substrateAddress(String seed, int prefix, Promise promise) {
try {
promise.resolve(substrateBrainwalletAddress(seed, prefix));
} catch (Exception e) {
promise.reject("invalid phrase", "invalid phrase");
}
}
@ReactMethod
public void substrateSign(String seed, String message, Promise promise) {
try {
promise.resolve(substrateBrainwalletSign(seed, message));
} catch (Exception e) {
promise.reject("invalid phrase", "invalid phrase");
}
}
private static native String ethkeyBrainwalletAddress(String seed);
private static native String ethkeyBrainwalletBIP39Address(String seed);
private static native String ethkeyBrainwalletSign(String seed, String message);
private static native String ethkeyRlpItem(String data, int position);
private static native String ethkeyKeccak(String data);
private static native String ethkeyBlake(String data);
private static native String ethkeyEthSign(String data);
private static native String ethkeyBlockiesIcon(String seed);
private static native String ethkeyRandomPhrase();
private static native String ethkeyEncryptData(String data, String password);
private static native String ethkeyDecryptData(String data, String password);
private static native String ethkeyQrCode(String data);
private static native String ethkeyQrCodeHex(String data);
private static native String substrateBrainwalletAddress(String seed, int prefix);
private static native String substrateBrainwalletSign(String seed, String message);
}
......@@ -36,5 +36,8 @@ RCT_EXTERN_METHOD(randomPhrase:(RCTPromiseResolveBlock)resolve reject:(RCTPromis
RCT_EXTERN_METHOD(encryptData:(NSString*)data password:(NSString*)password resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(decryptData:(NSString*)data password:(NSString*)password resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(qrCode:(NSString*)data resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(substrateAddress:(NSString*)seed version:(NSUInteger*)version resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(substrateSign:(NSString*)seed message:(NSString*)message resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(blake2s:(NSString*)data resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
@end
......@@ -28,7 +28,6 @@ class EthkeyBridge: NSObject {
@objc func brainWalletSign(_ seed: String, message: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
var error: UInt32 = 0
print(seed, " + ", message)
var seed_ptr = seed.asPtr()
var message_ptr = message.asPtr()
let signature_rust_str = ethkey_brainwallet_sign(&error, &seed_ptr, &message_ptr)
......@@ -65,6 +64,17 @@ class EthkeyBridge: NSObject {
resolve(hash)
}
@objc func blake2s(_ data: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
var error: UInt32 = 0
var data_ptr = data.asPtr()
let hash_rust_str = blake(&error, &data_ptr)
let hash_rust_str_ptr = rust_string_ptr(hash_rust_str)
let hash = String.fromStringPtr(ptr: hash_rust_str_ptr!.pointee)
rust_string_ptr_destroy(hash_rust_str_ptr)
rust_string_destroy(hash_rust_str)
resolve(hash)
}
@objc func ethSign(_ data: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
var error: UInt32 = 0
var data_ptr = data.asPtr()
......@@ -143,4 +153,42 @@ class EthkeyBridge: NSObject {
reject("Failed to generate blockies", nil, nil)
}
}
@objc func qrCodeHex(_ data: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
var error: UInt32 = 0
var data_ptr = data.asPtr()
let icon_rust_str = qrcode_hex(&error, &data_ptr)
let icon_rust_str_ptr = rust_string_ptr(icon_rust_str)
let icon = String.fromStringPtr(ptr: icon_rust_str_ptr!.pointee)
rust_string_ptr_destroy(icon_rust_str_ptr)
rust_string_destroy(icon_rust_str)
if error == 0 {
resolve(icon)
} else {
reject("Failed to generate blockies", nil, nil)
}
}
@objc func substrateAddress(_ seed: String, version: UInt32, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
var error: UInt32 = 0
var seed_ptr = seed.asPtr()
let address_rust_str = substrate_brainwallet_address(&error, &seed_ptr, version)
let address_rust_str_ptr = rust_string_ptr(address_rust_str)
let address = String.fromStringPtr(ptr: address_rust_str_ptr!.pointee)
rust_string_ptr_destroy(address_rust_str_ptr)
rust_string_destroy(address_rust_str)
resolve(address)
}
@objc func substrateSign(_ seed: String, message: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
var error: UInt32 = 0
var seed_ptr = seed.asPtr()
var message_ptr = message.asPtr()
let signature_rust_str = substrate_brainwallet_sign(&error, &seed_ptr, &message_ptr)
let signature_rust_str_ptr = rust_string_ptr(signature_rust_str)
let signature = String.fromStringPtr(ptr: signature_rust_str_ptr!.pointee)
rust_string_ptr_destroy(signature_rust_str_ptr)
rust_string_destroy(signature_rust_str)
resolve(signature)
}
}
This diff is collapsed.
......@@ -5,17 +5,22 @@ authors = ["debris <marek.kotewicz@gmail.com>"]
edition = "2018"
[dependencies]
base58 = "0.1.0"
base64 = "0.10.1"
blake2-rfc = "0.2.18"
blockies = "0.3"
ethsign = { version = "0.6.1", default-features = false, features = ["pure-rust"] }
jni = { version = "0.10.2", default-features = false, optional = true }
lazy_static = "1.3.0"
libc = "0.2"
codec = { package = "parity-scale-codec", version = "1.0.0", default-features = false, features = ["derive"] }
regex = "1.2.1"
rlp = { version = "0.3.0", features = ["ethereum"] }
rustc-hex = "2.0.1"
schnorrkel = "0.1.0"
schnorrkel = "0.8.5"
serde = "1.0"
serde_json = "1.0"
sha2 = "0.8"
substrate-bip39 = "0.3.1"
tiny-bip39 = { version = "0.6.1", default-features = false }
tiny-hderive = "0.1"
tiny-keccak = "1.4"
......
......@@ -49,30 +49,30 @@ struct rust_string* ethkey_brainwallet_bip39_address(unsigned* error, const stru
// returns message signed with keypair
struct rust_string* ethkey_brainwallet_sign(unsigned* error, const struct rust_string_ptr* seed, const struct rust_string_ptr* message);
// rlp ffi
// returns rlp item at given position
struct rust_string* rlp_item(unsigned* error, const struct rust_string_ptr* rlp, const unsigned position);
// sha3 ffi
struct rust_string* keccak256(unsigned* error, const struct rust_string_ptr* data);
struct rust_string* eth_sign(unsigned* error, const struct rust_string_ptr* data);
struct rust_string* blake(unsigned* error, const struct rust_string_ptr* data);
// blockies ffi
struct rust_string* eth_sign(unsigned* error, const struct rust_string_ptr* data);
struct rust_string* blockies_icon(unsigned* error, const struct rust_string_ptr* blockies_seed);
// random phrase ffi
struct rust_string* random_phrase(unsigned* error);
// data encryption ffi
struct rust_string* encrypt_data(unsigned* error, const struct rust_string_ptr* data, const struct rust_string_ptr* password);
struct rust_string* decrypt_data(unsigned* error, const struct rust_string_ptr* encrypted_data, const struct rust_string_ptr* password);
// qr code generator
// qr code generator for utf-8 strings
struct rust_string* qrcode(unsigned* error, const struct rust_string_ptr* data);
// qr code generator for hex-encoded binary
struct rust_string* qrcode_hex(unsigned* error, const struct rust_string_ptr* data);
// ss58 address (including prefix) for sr25519 key generated out of BIP39 phrase
struct rust_string* substrate_brainwallet_address(unsigned* error, const struct rust_string_ptr* seed, const unsigned prefix);
struct rust_string* substrate_brainwallet_sign(unsigned* error, const struct rust_string_ptr* seed, const struct rust_string_ptr* data);
......@@ -15,6 +15,7 @@
// along with Parity. If not, see <http://www.gnu.org/licenses/>.
mod eth;
mod sr25519;
mod util;
use util::StringPtr;
......@@ -25,7 +26,7 @@ use rlp::decode_list;
use rustc_hex::{ToHex, FromHex};
use tiny_keccak::Keccak;
use tiny_keccak::keccak256 as keccak;
use blake2_rfc::blake2s::blake2s;
use std::num::NonZeroU32;
// 10240 is always non-zero, ergo this is safe
......@@ -68,6 +69,33 @@ pub unsafe extern fn ethkey_keypair_sign(keypair: *mut KeyPair, message: *mut St
Box::into_raw(Box::new(signature))
}
fn qrcode_bytes(data: &[u8]) -> Option<String> {
use qrcodegen::{QrCode, QrCodeEcc};
use pixelate::{Image, Color, BLACK};
let qr = QrCode::encode_binary(data, QrCodeEcc::Medium).ok()?;
let palette = &[Color::Rgba(255,255,255,0), BLACK];
let mut pixels = Vec::with_capacity((qr.size() * qr.size()) as usize);
for y in 0..qr.size() {
for x in 0..qr.size() {
pixels.push(qr.get_module(x, y) as u8);
}
}
let mut result = Vec::new();
Image {
palette,
pixels: &pixels,
width: qr.size() as usize,
scale: 16,
}.render(&mut result).ok()?;
Some(base64png(&result))
}
export! {
@Java_io_parity_signer_EthkeyBridge_ethkeyBrainwalletAddress
fn ethkey_brainwallet_address(seed: &str) -> String {
......@@ -114,6 +142,13 @@ export! {
Some(keccak(&data).to_hex())
}
@Java_io_parity_signer_EthkeyBridge_ethkeyBlake
fn blake(data: &str) -> Option<String> {
let data: Vec<u8> = data.from_hex().ok()?;
Some(blake2s(32, &[], &data).as_bytes().to_hex())
}
@Java_io_parity_signer_EthkeyBridge_ethkeyBlockiesIcon
fn blockies_icon(seed: String) -> Option<String> {
use blockies::Ethereum;
......@@ -169,36 +204,35 @@ export! {
@Java_io_parity_signer_EthkeyBridge_ethkeyQrCode
fn qrcode(data: &str) -> Option<String> {
use qrcodegen::{QrCode, QrCodeEcc};
use pixelate::{Image, Color, BLACK};
qrcode_bytes(data.as_bytes())
}
let qr = QrCode::encode_binary(data.as_bytes(), QrCodeEcc::Medium).ok()?;
@Java_io_parity_signer_EthkeyBridge_ethkeyQrCodeHex
fn qrcode_hex(data: &str) -> Option<String> {
qrcode_bytes(&data.from_hex::<Vec<u8>>().ok()?)
}
let palette = &[Color::Rgba(255,255,255,0), BLACK];
let mut pixels = Vec::with_capacity((qr.size() * qr.size()) as usize);
@Java_io_parity_signer_EthkeyBridge_substrateBrainwalletAddress
fn substrate_brainwallet_address(suri: &str, prefix: u8) -> Option<String> {
let keypair = sr25519::KeyPair::from_suri(suri)?;
for y in 0..qr.size() {
for x in 0..qr.size() {
pixels.push(qr.get_module(x, y) as u8);
}
}
Some(keypair.ss58_address(prefix))
}
let mut result = Vec::new();
@Java_io_parity_signer_EthkeyBridge_substrateBrainwalletSign
fn substrate_brainwallet_sign(suri: &str, message: &str) -> Option<String> {
let keypair = sr25519::KeyPair::from_suri(suri)?;
Image {
palette,
pixels: &pixels,
width: qr.size() as usize,
scale: 16,
}.render(&mut result).ok()?;
let message: Vec<u8> = message.from_hex().ok()?;
let signature = keypair.sign(&message);
Some(base64png(&result))
Some(signature.to_hex())
}
}
#[cfg(test)]
mod tests {
use super::rlp_item;
use super::*;
#[test]
fn test_rlp_item() {
......@@ -210,4 +244,24 @@ mod tests {
assert_eq!(rlp_item(rlp, 4), Some("0a".into()));
assert_eq!(rlp_item(rlp, 5), Some("".into()));
}
#[test]
fn test_substrate_brainwallet_address() {
let suri = "grant jaguar wish bench exact find voice habit tank pony state salmon";
// Secret seed: 0xb139e4050f80172b44957ef9d1755ef5c96c296d63b8a2b50025bf477bd95224
// Public key (hex): 0x944eeb240615f4a94f673f240a256584ba178e22dd7b67503a753968e2f95761
let expected = "5FRAPSnpgmnXAnmPVv68fT6o7ntTvaZmkTED8jDttnXs9k4n";
let generated = substrate_brainwallet_address(suri, 42).unwrap();
assert_eq!(expected, generated);
}
#[test]
fn test_substrate_brainwallet_address_suri() {
let suri = "grant jaguar wish bench exact find voice habit tank pony state salmon//hard/soft/0";
let expected = "5D4kaJXj5HVoBw2tFFsDj56BjZdPhXKxgGxZuKk4K3bKqHZ6";
let generated = substrate_brainwallet_address(suri, 42).unwrap();
assert_eq!(expected, generated);
}
}
use codec::{Encode, Decode};
use lazy_static::lazy_static;
use regex::Regex;
use schnorrkel::{ExpansionMode, SecretKey};
use schnorrkel::derive::{ChainCode, Derivation};
use substrate_bip39::mini_secret_from_entropy;
use bip39::{Mnemonic, Language};
use base58::ToBase58;
pub struct KeyPair(schnorrkel::Keypair);
const SIGNING_CTX: &[u8] = b"substrate";
const JUNCTION_ID_LEN: usize = 32;
const CHAIN_CODE_LENGTH: usize = 32;
impl KeyPair {
pub fn from_bip39_phrase(phrase: &str, password: Option<&str>) -> Option<KeyPair> {
let mnemonic = Mnemonic::from_phrase(phrase, Language::English).ok()?;
let mini_secret_key = mini_secret_from_entropy(mnemonic.entropy(), password.unwrap_or("")).ok()?;
Some(KeyPair(mini_secret_key.expand_to_keypair(ExpansionMode::Ed25519)))
}
// Should match implementation at https://github.com/paritytech/substrate/blob/master/core/primitives/src/crypto.rs#L653-L682
pub fn from_suri(suri: &str) -> Option<KeyPair> {
lazy_static! {
static ref RE_SURI: Regex = {
Regex::new(r"^(?P<phrase>\w+( \w+)*)?(?P<path>(//?[^/]+)*)(///(?P<password>.*))?$")
.expect("constructed from known-good static value; qed")
};
static ref RE_JUNCTION: Regex = Regex::new(r"/(/?[^/]+)").expect("constructed from known-good static value; qed");
}
let cap = RE_SURI.captures(suri)?;
let path = RE_JUNCTION.captures_iter(&cap["path"]).map(|j| DeriveJunction::from(&j[1]));
let pair = Self::from_bip39_phrase(
cap.name("phrase").map(|p| p.as_str())?,
cap.name("password").map(|p| p.as_str())
)?;
Some(pair.derive(path))
}
fn derive(&self, path: impl Iterator<Item = DeriveJunction>) -> Self {
let init = self.0.secret.clone();
let result = path.fold(init, |acc, j| match j {
DeriveJunction::Soft(cc) => acc.derived_key_simple(ChainCode(cc), &[]).0,
DeriveJunction::Hard(cc) => derive_hard_junction(&acc, cc),
});
KeyPair(result.to_keypair())
}
pub fn ss58_address(&self, prefix: u8) -> String {
let mut v = vec![prefix];
v.extend_from_slice(&self.0.public.to_bytes());
let r = ss58hash(&v);
v.extend_from_slice(&r.as_bytes()[0..2]);
v.to_base58()
}
pub fn sign(&self, message: &[u8]) -> [u8; 64] {
let context = schnorrkel::signing_context(SIGNING_CTX);
self.0.sign(context.bytes(message)).to_bytes()
}
}
fn derive_hard_junction(secret: &SecretKey, cc: [u8; CHAIN_CODE_LENGTH]) -> SecretKey {
secret.hard_derive_mini_secret_key(Some(ChainCode(cc)), b"").0.expand(ExpansionMode::Ed25519)
}
/// A since derivation junction description. It is the single parameter used when creating
/// a new secret key from an existing secret key and, in the case of `SoftRaw` and `SoftIndex`
/// a new public key from an existing public key.
#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Encode, Decode)]
enum DeriveJunction {
/// Soft (vanilla) derivation. Public keys have a correspondent derivation.
Soft([u8; JUNCTION_ID_LEN]),
/// Hard ("hardened") derivation. Public keys do not have a correspondent derivation.
Hard([u8; JUNCTION_ID_LEN]),
}
impl DeriveJunction {
/// Consume self to return a hard derive junction with the same chain code.
fn harden(self) -> Self { DeriveJunction::Hard(self.unwrap_inner()) }
/// Create a new soft (vanilla) DeriveJunction from a given, encodable, value.
///
/// If you need a hard junction, use `hard()`.
fn soft<T: Encode>(index: T) -> Self {
let mut cc: [u8; JUNCTION_ID_LEN] = Default::default();
index.using_encoded(|data| if data.len() > JUNCTION_ID_LEN {
let hash_result = blake2_rfc::blake2b::blake2b(JUNCTION_ID_LEN, &[], data);
let hash = hash_result.as_bytes();
cc.copy_from_slice(hash);
} else {
cc[0..data.len()].copy_from_slice(data);
});
DeriveJunction::Soft(cc)
}
/// Consume self to return the chain code.
fn unwrap_inner(self) -> [u8; JUNCTION_ID_LEN] {
match self {
DeriveJunction::Hard(c) | DeriveJunction::Soft(c) => c,
}
}
}
impl<T: AsRef<str>> From<T> for DeriveJunction {
fn from(j: T) -> DeriveJunction {
let j = j.as_ref();
let (code, hard) = if j.starts_with("/") {
(&j[1..], true)
} else {
(j, false)
};
let res = if let Ok(n) = str::parse::<u64>(code) {
// number
DeriveJunction::soft(n)
} else {
// something else
DeriveJunction::soft(code)
};
if hard {
res.harden()
} else {
res
}
}
}
fn ss58hash(data: &[u8]) -> blake2_rfc::blake2b::Blake2bResult {
const PREFIX: &[u8] = b"SS58PRE";
let mut context = blake2_rfc::blake2b::Blake2b::new(64);
context.update(PREFIX);
context.update(data);
context.finalize()
}
......@@ -82,6 +82,16 @@ impl Argument<'static> for u32 {
}
}
#[cfg(not(feature = "jni"))]
impl Argument<'static> for u8 {
type Ext = u32;
type Env = Cell<u32>;
fn convert(_: &Self::Env, val: Self::Ext) -> Self {
val as u8
}
}
#[cfg(not(feature = "jni"))]
impl<'a> Argument<'static> for &'a str {
type Ext = *const StringPtr;
......@@ -143,6 +153,16 @@ impl<'jni> Argument<'jni> for u32 {
}
}
#[cfg(feature = "jni")]
impl<'jni> Argument<'jni> for u8 {
type Ext = jint;
type Env = JNIEnv<'jni>;
fn convert(_: &Self::Env, val: Self::Ext) -> Self {
val as u8
}
}
#[cfg(feature = "jni")]
impl<'a, 'jni> Argument<'jni> for &'a str {
type Ext = JString<'jni>;
......
......@@ -108,6 +108,31 @@ export function decryptData (data, password) {
return EthkeyBridge.decryptData(data, password);
}
// Creates a QR code for the UTF-8 representation of a string
export function qrCode (data) {
return EthkeyBridge.qrCode(data)
return EthkeyBridge.qrCode(data);
}
// Creates a QR code for binary data from a hex-encoded string
export function qrCodeHex (data) {
return EthkeyBridge.qrCodeHex(data);
}
export function blake2s (data) {
return EthkeyBridge.blake2s(data);
}
// Get an SS58 encoded address for a sr25519 account from a BIP39 phrase and a prefix.
// Prefix is a number used in the SS58 encoding:
//
// Polkadot proper = 0
// Kusama = 2
// Default (testnets) = 42
export function substrateAddress (seed, prefix) {
return EthkeyBridge.substrateAddress(seed, prefix);
}
// Sign data using sr25519 crypto for a BIP39 phrase. Message is hex-encoded byte array.
export function substrateSign (seed, message) {
return EthkeyBridge.substrateSign(seed, message);
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment