ZIP 339 support.

Signed-off-by: Daira Hopwood <daira@jacaranda.org>
This commit is contained in:
Daira Hopwood 2021-08-07 01:02:41 +01:00
parent fd462fd8c4
commit 52ed61157f
5 changed files with 291 additions and 0 deletions

View File

@ -0,0 +1,59 @@
#ifndef ZCASH_RUST_INCLUDE_RUST_ZIP339_H
#define ZCASH_RUST_INCLUDE_RUST_ZIP339_H
#include "rust/types.h"
#include <stddef.h>
#include <stdint.h>
#ifndef __cplusplus
#include <assert.h>
#endif
#ifdef __cplusplus
extern "C" {
#endif
// These must match src/rust/src/zip339_ffi.rs.
// (They happen to also match the integer values correspond in the bip0039-rs crate with the
// "all-languages" feature enabled, but that's not required.)
enum Language
#ifdef __cplusplus
: uint32_t
#endif
{
English = 0,
SimplifiedChinese = 1,
TraditionalChinese = 2,
Czech = 3,
French = 4,
Italian = 5,
Japanese = 6,
Korean = 7,
Portuguese = 8,
Spanish = 9,
SIZE_HACK = 0xFFFFFFFF // needed when compiling as C
};
static_assert(sizeof(Language) == 4);
/// Creates a phrase with the given entropy, in the given `Language`. The phrase is represented as
/// a pointer to a null-terminated C string that must not be written to, and must be freed by
/// `zip339_free_phrase`.
const char * zip339_entropy_to_phrase(Language language, const uint8_t *entropy, size_t entropy_len);
/// Frees a phrase returned by `zip339_entropy_to_phrase`.
void zip339_free_phrase(char *phrase);
/// Returns `true` if the given string is a valid mnemonic phrase in the given `Language`.
bool zip339_validate_phrase(Language language, const char *phrase);
/// Copies the seed for the given phrase into the 64-byte buffer `buf`.
/// Returns true if successful, false on error. In case of error, `buf` is zeroed.
bool zip339_phrase_to_seed(Language language, const char *phrase, uint8_t (*buf)[64]);
#ifdef __cplusplus
}
#endif
#endif // ZCASH_RUST_INCLUDE_RUST_ZIP339_H

View File

@ -72,6 +72,7 @@ mod address_ffi;
mod history_ffi;
mod orchard_ffi;
mod transaction_ffi;
mod zip339_ffi;
mod test_harness_ffi;

View File

@ -10,6 +10,7 @@ mod key_components;
mod mmr;
mod notes;
mod signatures;
mod zip339;
#[test]
fn sapling_generators() {

View File

@ -0,0 +1,120 @@
use libc::c_char;
use std::{
convert::{TryFrom, TryInto},
ffi::CStr,
ptr,
};
use crate::zip339_ffi::{
zip339_entropy_to_phrase, zip339_free_phrase, zip339_phrase_to_seed, zip339_validate_phrase,
Language,
};
use zcash_primitives::zip339;
#[test]
fn test_try_from_language() {
assert_eq!(Language(0).try_into(), Ok(zip339::Language::English));
assert!(zip339::Language::try_from(Language(1234)).is_err());
}
#[test]
#[should_panic]
fn test_null_entropy_to_phrase_panics() {
zip339_entropy_to_phrase(Language(0), ptr::null(), 0);
}
#[test]
fn test_free_null_phrase_is_noop() {
zip339_free_phrase(ptr::null_mut());
}
#[test]
#[should_panic]
fn test_validate_null_phrase_panics() {
zip339_validate_phrase(Language(0), ptr::null());
}
#[test]
#[should_panic]
fn test_null_phrase_to_seed_panics() {
zip339_phrase_to_seed(Language(0), ptr::null(), ptr::NonNull::dangling().as_ptr());
}
#[test]
#[should_panic]
fn test_phrase_to_seed_with_null_buffer_panics() {
zip339_phrase_to_seed(
Language(0),
ptr::NonNull::dangling().as_ptr(),
ptr::null_mut(),
);
}
#[test]
fn test_known_answers() {
let mut entropy = [0u8; 32];
// English and another language with non-Latin script.
let expected_phrase_en = "abandon abandon abandon abandon abandon abandon abandon abandon \
abandon abandon abandon abandon abandon abandon abandon abandon \
abandon abandon abandon abandon abandon abandon abandon art";
let expected_phrase_ko = "가격 가격 가격 가격 가격 가격 가격 가격 \
\
";
let expected_seed_en = [
0x40, 0x8B, 0x28, 0x5C, 0x12, 0x38, 0x36, 0x00, 0x4F, 0x4B, 0x88, 0x42, 0xC8, 0x93, 0x24,
0xC1, 0xF0, 0x13, 0x82, 0x45, 0x0C, 0x0D, 0x43, 0x9A, 0xF3, 0x45, 0xBA, 0x7F, 0xC4, 0x9A,
0xCF, 0x70, 0x54, 0x89, 0xC6, 0xFC, 0x77, 0xDB, 0xD4, 0xE3, 0xDC, 0x1D, 0xD8, 0xCC, 0x6B,
0xC9, 0xF0, 0x43, 0xDB, 0x8A, 0xDA, 0x1E, 0x24, 0x3C, 0x4A, 0x0E, 0xAF, 0xB2, 0x90, 0xD3,
0x99, 0x48, 0x08, 0x40,
];
let expected_seed_ko = [
0xD6, 0x93, 0x5F, 0x34, 0x3A, 0xC6, 0x66, 0x97, 0x58, 0x06, 0xDF, 0xA7, 0xBD, 0x0E, 0x45,
0xF8, 0x3A, 0x6F, 0x0A, 0x1A, 0x9F, 0x48, 0x54, 0xF4, 0x40, 0xEC, 0x09, 0x0B, 0xE3, 0xEF,
0x16, 0xEA, 0x09, 0x42, 0x03, 0xEC, 0x07, 0xA1, 0x7D, 0x8B, 0x43, 0x9B, 0xBD, 0x2F, 0x89,
0x3D, 0xB8, 0xB8, 0x3D, 0x3E, 0x43, 0xD4, 0x59, 0xA3, 0x6C, 0x29, 0xDF, 0xAB, 0xC2, 0xF9,
0xF5, 0xB2, 0x47, 0x72,
];
for &(expected_phrase, expected_seed, language) in [
(expected_phrase_en, expected_seed_en, Language(0)),
(expected_phrase_ko, expected_seed_ko, Language(7)),
]
.iter()
{
// Testing these all together simplifies memory management in the test code.
// test zip339_entropy_to_phrase
let phrase = zip339_entropy_to_phrase(language, entropy[..].as_mut_ptr(), 32);
assert_eq!(
unsafe { CStr::from_ptr(phrase) }.to_str().unwrap(),
expected_phrase
);
// test zip339_validate_phrase
assert!(zip339_validate_phrase(language, phrase));
assert!(!zip339_validate_phrase(Language(1), phrase));
assert!(!zip339_validate_phrase(Language(1234), phrase));
// test zip339_phrase_to_seed
let mut seed = [0u8; 64];
assert!(zip339_phrase_to_seed(
language,
phrase,
seed[..].as_mut_ptr()
));
assert_eq!(seed, expected_seed);
// test that zip339_phrase_to_seed zeros buffer on failure
let mut seed = [0xFFu8; 64];
assert!(!zip339_phrase_to_seed(
Language(1),
phrase,
seed[..].as_mut_ptr()
));
assert_eq!(seed, [0u8; 64]);
zip339_free_phrase(phrase as *mut c_char);
}
}

110
src/rust/src/zip339_ffi.rs Normal file
View File

@ -0,0 +1,110 @@
use libc::{c_char, size_t};
use std::{
convert::{TryFrom, TryInto},
ffi::{CStr, CString},
ptr, slice,
};
use zcash_primitives::zip339;
// It's safer to use a wrapper type here than an enum. We can't stop a C caller from passing
// an unrecognized value as a `language` parameter; if it were an enum on the Rust side,
// then that would immediately be UB, whereas this way we can return null or false.
#[repr(C)]
#[derive(Copy, Clone)]
pub struct Language(pub u32);
impl TryFrom<Language> for zip339::Language {
type Error = ();
fn try_from(language: Language) -> Result<Self, ()> {
// These must match `src/rust/include/zip339.h`.
match language {
Language(0) => Ok(zip339::Language::English),
Language(1) => Ok(zip339::Language::SimplifiedChinese),
Language(2) => Ok(zip339::Language::TraditionalChinese),
Language(3) => Ok(zip339::Language::Czech),
Language(4) => Ok(zip339::Language::French),
Language(5) => Ok(zip339::Language::Italian),
Language(6) => Ok(zip339::Language::Japanese),
Language(7) => Ok(zip339::Language::Korean),
Language(8) => Ok(zip339::Language::Portuguese),
Language(9) => Ok(zip339::Language::Spanish),
Language(_) => Err(()),
}
}
}
/// Creates a phrase with the given entropy, in the given `Language`. The phrase is represented as
/// a pointer to a null-terminated C string that must not be written to, and must be freed by
/// `zip339_free_phrase`.
#[no_mangle]
pub extern "C" fn zip339_entropy_to_phrase(
language: Language,
entropy: *const u8,
entropy_len: size_t,
) -> *const c_char {
assert!(!entropy.is_null());
if let Ok(language) = language.try_into() {
let entropy = unsafe { slice::from_raw_parts(entropy, entropy_len) }.to_vec();
if let Ok(mnemonic) = zip339::Mnemonic::from_entropy_in(language, entropy) {
if let Ok(phrase) = CString::new(mnemonic.phrase()) {
return phrase.into_raw();
}
}
}
ptr::null()
}
/// Frees a phrase returned by `zip339_entropy_to_phrase`.
#[no_mangle]
pub extern "C" fn zip339_free_phrase(phrase: *mut c_char) {
if !phrase.is_null() {
unsafe {
CString::from_raw(phrase);
}
}
}
/// Returns `true` if the given string is a valid mnemonic phrase in the given `Language`.
#[no_mangle]
pub extern "C" fn zip339_validate_phrase(language: Language, phrase: *const c_char) -> bool {
assert!(!phrase.is_null());
if let Ok(language) = language.try_into() {
if let Ok(phrase) = unsafe { CStr::from_ptr(phrase) }.to_str() {
return zip339::Mnemonic::validate_in(language, phrase).is_ok();
}
}
false
}
/// Copies the seed for the given phrase into the 64-byte buffer `buf`.
/// Returns true if successful, false on error. In case of error, `buf` is zeroed.
#[no_mangle]
pub extern "C" fn zip339_phrase_to_seed(
language: Language,
phrase: *const c_char,
buf: *mut u8,
) -> bool {
assert!(!phrase.is_null());
assert!(!buf.is_null());
if let Ok(language) = language.try_into() {
if let Ok(phrase) = unsafe { CStr::from_ptr(phrase) }.to_str() {
if let Ok(mnemonic) = zip339::Mnemonic::from_phrase_in(language, phrase) {
// Use the empty passphrase.
let seed = mnemonic.to_seed("");
unsafe {
ptr::copy(seed.as_ptr(), buf, 64);
}
return true;
}
}
}
unsafe {
ptr::write_bytes(buf, 0, 64);
}
false
}