mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2024-12-25 07:29:23 -05:00
lib: add standard mnemonic support
This commit is contained in:
parent
bcbc9c33e3
commit
6560ee79ad
@ -0,0 +1,45 @@
|
|||||||
|
package im.molly.monero.mnemonics
|
||||||
|
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import im.molly.monero.parseHex
|
||||||
|
import org.junit.Test
|
||||||
|
|
||||||
|
class MoneroMnemonicTest {
|
||||||
|
|
||||||
|
data class TestCase(val entropy: String, val words: String, val language: String)
|
||||||
|
|
||||||
|
private val testVector = listOf(
|
||||||
|
TestCase(
|
||||||
|
entropy = "3b094ca7218f175e91fa2402b4ae239a2fe8262792a3e718533a1a357a1e4109",
|
||||||
|
words = "tavern judge beyond bifocals deepest mural onward dummy eagle diode gained vacation rally cause firm idled jerseys moat vigilant upload bobsled jobs cunning doing jobs",
|
||||||
|
language = "en",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun validateKnownMnemonics() {
|
||||||
|
testVector.forEach {
|
||||||
|
validateMnemonicGeneration(it)
|
||||||
|
validateEntropyRecovery(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateMnemonicGeneration(testCase: TestCase) {
|
||||||
|
val mnemonicCode = MoneroMnemonic.generateMnemonic(testCase.entropy.parseHex())
|
||||||
|
assertMnemonicCode(mnemonicCode, testCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateEntropyRecovery(testCase: TestCase) {
|
||||||
|
val mnemonicCode = MoneroMnemonic.recoverEntropy(testCase.words)
|
||||||
|
assertThat(mnemonicCode).isNotNull()
|
||||||
|
assertMnemonicCode(mnemonicCode!!, testCase)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assertMnemonicCode(mnemonicCode: MnemonicCode, testCase: TestCase) {
|
||||||
|
with(mnemonicCode) {
|
||||||
|
assertThat(entropy).isEqualTo(testCase.entropy.parseHex())
|
||||||
|
assertThat(String(words)).isEqualTo(testCase.words)
|
||||||
|
assertThat(locale.language).isEqualTo(testCase.language)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -72,15 +72,20 @@ target_link_libraries(
|
|||||||
)
|
)
|
||||||
|
|
||||||
set(MNEMONICS_SOURCES
|
set(MNEMONICS_SOURCES
|
||||||
|
mnemonics/jni_cache.cc
|
||||||
mnemonics/jni_loader.cc
|
mnemonics/jni_loader.cc
|
||||||
|
mnemonics/mnemonics.cc
|
||||||
)
|
)
|
||||||
|
|
||||||
add_library(monero_mnemonics SHARED ${COMMON_SOURCES} ${MNEMONICS_SOURCES})
|
add_library(monero_mnemonics SHARED ${COMMON_SOURCES} ${MNEMONICS_SOURCES})
|
||||||
|
|
||||||
target_link_libraries(
|
target_link_libraries(
|
||||||
monero_mnemonics
|
monero_mnemonics
|
||||||
|
PUBLIC
|
||||||
|
Monero::easylogging
|
||||||
PRIVATE
|
PRIVATE
|
||||||
Monero::electrum_words
|
Monero::electrum_words
|
||||||
|
OpenSSL::SSL
|
||||||
log
|
log
|
||||||
)
|
)
|
||||||
|
|
||||||
|
21
lib/android/src/main/cpp/mnemonics/jni_cache.cc
Normal file
21
lib/android/src/main/cpp/mnemonics/jni_cache.cc
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
#include "jni_cache.h"
|
||||||
|
|
||||||
|
namespace monero {
|
||||||
|
|
||||||
|
// im.molly.monero.mnemonics
|
||||||
|
ScopedJvmGlobalRef<jclass> MoneroMnemonicClass;
|
||||||
|
jmethodID MoneroMnemonic_buildMnemonicFromNative;
|
||||||
|
|
||||||
|
void initializeJniCache(JNIEnv* env) {
|
||||||
|
// im.molly.monero.mnemonics
|
||||||
|
auto moneroMnemonicClass = findClass(env, "im/molly/monero/mnemonics/MoneroMnemonic");
|
||||||
|
|
||||||
|
MoneroMnemonic_buildMnemonicFromNative = moneroMnemonicClass
|
||||||
|
.getStaticMethodId(env,
|
||||||
|
"buildMnemonicFromNative",
|
||||||
|
"([B[BLjava/lang/String;)Lim/molly/monero/mnemonics/MnemonicCode;");
|
||||||
|
|
||||||
|
MoneroMnemonicClass = moneroMnemonicClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace monero
|
17
lib/android/src/main/cpp/mnemonics/jni_cache.h
Normal file
17
lib/android/src/main/cpp/mnemonics/jni_cache.h
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
#ifndef MNEMONICS_JNI_CACHE_H__
|
||||||
|
#define MNEMONICS_JNI_CACHE_H__
|
||||||
|
|
||||||
|
#include "common/jvm.h"
|
||||||
|
|
||||||
|
namespace monero {
|
||||||
|
|
||||||
|
// Initialize various classes and method pointers cached for use in JNI.
|
||||||
|
void initializeJniCache(JNIEnv* env);
|
||||||
|
|
||||||
|
// im.molly.monero.mnemonics
|
||||||
|
extern ScopedJvmGlobalRef<jclass> MoneroMnemonicClass;
|
||||||
|
extern jmethodID MoneroMnemonic_buildMnemonicFromNative;
|
||||||
|
|
||||||
|
} // namespace monero
|
||||||
|
|
||||||
|
#endif // MNEMONICS_JNI_CACHE_H__
|
@ -1,3 +1,5 @@
|
|||||||
|
#include "jni_cache.h"
|
||||||
|
|
||||||
#include "common/jvm.h"
|
#include "common/jvm.h"
|
||||||
|
|
||||||
namespace monero {
|
namespace monero {
|
||||||
@ -10,6 +12,8 @@ JNI_OnLoad(JavaVM* vm, void* reserved) {
|
|||||||
return JNI_ERR;
|
return JNI_ERR;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initializeJniCache(env);
|
||||||
|
|
||||||
return JNI_VERSION_1_6;
|
return JNI_VERSION_1_6;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
78
lib/android/src/main/cpp/mnemonics/mnemonics.cc
Normal file
78
lib/android/src/main/cpp/mnemonics/mnemonics.cc
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
#include "common/eraser.h"
|
||||||
|
|
||||||
|
#include "jni_cache.h"
|
||||||
|
|
||||||
|
#include "electrum-words.h"
|
||||||
|
|
||||||
|
namespace monero {
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT jobject JNICALL
|
||||||
|
Java_im_molly_monero_mnemonics_MoneroMnemonicKt_nativeElectrumWordsGenerateMnemonic(
|
||||||
|
JNIEnv* env,
|
||||||
|
jclass clazz,
|
||||||
|
jbyteArray j_entropy,
|
||||||
|
jstring j_language) {
|
||||||
|
std::vector<char> entropy = jvmToNativeByteArray(env, JvmParamRef<jbyteArray>(j_entropy));
|
||||||
|
Eraser entropy_eraser(entropy);
|
||||||
|
|
||||||
|
std::string language = jvmToStdString(env, j_language);
|
||||||
|
epee::wipeable_string words;
|
||||||
|
|
||||||
|
bool success =
|
||||||
|
crypto::ElectrumWords::bytes_to_words(entropy.data(),
|
||||||
|
entropy.size(),
|
||||||
|
words,
|
||||||
|
language);
|
||||||
|
if (!success) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedJvmLocalRef<jobject> j_mnemonic_code(
|
||||||
|
env, MoneroMnemonicClass.callStaticObjectMethod(
|
||||||
|
env, MoneroMnemonic_buildMnemonicFromNative,
|
||||||
|
j_entropy,
|
||||||
|
nativeToJvmByteArray(env, words.data(), words.size()).obj(),
|
||||||
|
j_language
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return j_mnemonic_code.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C"
|
||||||
|
JNIEXPORT jobject JNICALL
|
||||||
|
Java_im_molly_monero_mnemonics_MoneroMnemonicKt_nativeElectrumWordsRecoverEntropy(
|
||||||
|
JNIEnv* env,
|
||||||
|
jclass clazz,
|
||||||
|
jbyteArray j_source
|
||||||
|
) {
|
||||||
|
std::vector<char> words = jvmToNativeByteArray(env, JvmParamRef<jbyteArray>(j_source));
|
||||||
|
Eraser words_eraser(words);
|
||||||
|
|
||||||
|
std::string language;
|
||||||
|
epee::wipeable_string entropy, w_words(words.data(), words.size());
|
||||||
|
|
||||||
|
bool success =
|
||||||
|
crypto::ElectrumWords::words_to_bytes(w_words,
|
||||||
|
entropy,
|
||||||
|
0 /* len */,
|
||||||
|
true /* duplicate */,
|
||||||
|
language);
|
||||||
|
if (!success) {
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
ScopedJvmLocalRef<jobject> j_mnemonic_code(
|
||||||
|
env, MoneroMnemonicClass.callStaticObjectMethod(
|
||||||
|
env, MoneroMnemonic_buildMnemonicFromNative,
|
||||||
|
nativeToJvmByteArray(env, entropy.data(), entropy.size()).obj(),
|
||||||
|
j_source,
|
||||||
|
nativeToJvmString(env, language).obj()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return j_mnemonic_code.Release();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace monero
|
@ -1,4 +1,7 @@
|
|||||||
set(ELECTRUM_WORDS_SOURCES
|
set(ELECTRUM_WORDS_SOURCES
|
||||||
|
contrib/epee/src/memwipe.c
|
||||||
|
contrib/epee/src/mlocker.cpp
|
||||||
|
contrib/epee/src/wipeable_string.cpp
|
||||||
src/mnemonics/electrum-words.cpp
|
src/mnemonics/electrum-words.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,6 +24,9 @@ add_library(
|
|||||||
electrum_words STATIC ${ELECTRUM_WORDS_SOURCES} ${ELECTRUM_WORDS_OVERRIDES}
|
electrum_words STATIC ${ELECTRUM_WORDS_SOURCES} ${ELECTRUM_WORDS_OVERRIDES}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Disable Easylogging++ logging
|
||||||
|
add_definitions(-DELPP_DISABLE_LOGS)
|
||||||
|
|
||||||
target_include_directories(
|
target_include_directories(
|
||||||
electrum_words
|
electrum_words
|
||||||
PUBLIC
|
PUBLIC
|
||||||
|
@ -55,7 +55,7 @@ bool RemoteNodeClient::invoke(const boost::string_ref uri,
|
|||||||
header << p.first << ": " << p.second << "\r\n";
|
header << p.first << ": " << p.second << "\r\n";
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
ScopedJvmLocalRef<jobject> j_response = {
|
ScopedJvmLocalRef<jobject> j_response(
|
||||||
env, m_wallet_native.callObjectMethod(
|
env, m_wallet_native.callObjectMethod(
|
||||||
env, WalletNative_callRemoteNode,
|
env, WalletNative_callRemoteNode,
|
||||||
nativeToJvmString(env, method.data()).obj(),
|
nativeToJvmString(env, method.data()).obj(),
|
||||||
@ -63,7 +63,7 @@ bool RemoteNodeClient::invoke(const boost::string_ref uri,
|
|||||||
nativeToJvmString(env, header.str()).obj(),
|
nativeToJvmString(env, header.str()).obj(),
|
||||||
nativeToJvmByteArray(env, body.data(), body.length()).obj()
|
nativeToJvmByteArray(env, body.data(), body.length()).obj()
|
||||||
)
|
)
|
||||||
};
|
);
|
||||||
m_response_info.clear();
|
m_response_info.clear();
|
||||||
if (j_response.is_null()) {
|
if (j_response.is_null()) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -34,7 +34,9 @@ void initializeJniCache(JNIEnv* env) {
|
|||||||
Logger_logFromNative = logger
|
Logger_logFromNative = logger
|
||||||
.getMethodId(env, "logFromNative", "(ILjava/lang/String;Ljava/lang/String;)V");
|
.getMethodId(env, "logFromNative", "(ILjava/lang/String;Ljava/lang/String;)V");
|
||||||
TxInfo_ctor = txInfoClass
|
TxInfo_ctor = txInfoClass
|
||||||
.getMethodId(env, "<init>", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILjava/lang/String;JIIJJJJZZ)V");
|
.getMethodId(env,
|
||||||
|
"<init>",
|
||||||
|
"(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILjava/lang/String;JIIJJJJZZ)V");
|
||||||
WalletNative_callRemoteNode = walletNative
|
WalletNative_callRemoteNode = walletNative
|
||||||
.getMethodId(env,
|
.getMethodId(env,
|
||||||
"callRemoteNode",
|
"callRemoteNode",
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
package im.molly.monero.mnemonics
|
||||||
|
|
||||||
|
import java.io.Closeable
|
||||||
|
import java.nio.CharBuffer
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.security.auth.Destroyable
|
||||||
|
|
||||||
|
class MnemonicCode private constructor(
|
||||||
|
private val _entropy: ByteArray,
|
||||||
|
private val _words: CharArray,
|
||||||
|
val locale: Locale,
|
||||||
|
) : Destroyable, Closeable, Iterable<CharArray> {
|
||||||
|
|
||||||
|
constructor(entropy: ByteArray, words: CharBuffer, locale: Locale) : this(
|
||||||
|
entropy.clone(),
|
||||||
|
words.array().copyOfRange(words.position(), words.remaining()),
|
||||||
|
locale,
|
||||||
|
)
|
||||||
|
|
||||||
|
val entropy: ByteArray
|
||||||
|
get() = checkNotDestroyed { _entropy.clone() }
|
||||||
|
|
||||||
|
val words: CharArray
|
||||||
|
get() = checkNotDestroyed { _words.clone() }
|
||||||
|
|
||||||
|
override fun iterator(): Iterator<CharArray> = object : Iterator<CharArray> {
|
||||||
|
private var cursor: Int = 0
|
||||||
|
|
||||||
|
override fun hasNext(): Boolean = checkNotDestroyed { cursor < _words.size }
|
||||||
|
|
||||||
|
override fun next(): CharArray {
|
||||||
|
if (!hasNext()) {
|
||||||
|
throw NoSuchElementException()
|
||||||
|
}
|
||||||
|
|
||||||
|
val endIndex = findNextWordEnd(cursor)
|
||||||
|
val currentWord = _words.copyOfRange(cursor, endIndex)
|
||||||
|
cursor = endIndex + 1
|
||||||
|
|
||||||
|
return currentWord
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findNextWordEnd(startIndex: Int): Int {
|
||||||
|
var endIndex = startIndex
|
||||||
|
while (endIndex < _words.size && _words[endIndex] != ' ') {
|
||||||
|
endIndex++
|
||||||
|
}
|
||||||
|
return endIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var destroyed = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
override fun destroy() {
|
||||||
|
if (!destroyed) {
|
||||||
|
_entropy.fill(0)
|
||||||
|
_words.fill(0.toChar())
|
||||||
|
}
|
||||||
|
destroyed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() = destroy()
|
||||||
|
|
||||||
|
protected fun finalize() = destroy()
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean =
|
||||||
|
this === other || (other is MnemonicCode && MessageDigest.isEqual(entropy, other.entropy))
|
||||||
|
|
||||||
|
override fun hashCode(): Int = entropy.contentHashCode()
|
||||||
|
|
||||||
|
private inline fun <T> checkNotDestroyed(block: () -> T): T {
|
||||||
|
check(!destroyed) { "MnemonicCode has already been destroyed" }
|
||||||
|
return block()
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,90 @@
|
|||||||
|
package im.molly.monero.mnemonics
|
||||||
|
|
||||||
|
import im.molly.monero.CalledByNative
|
||||||
|
import im.molly.monero.NativeLoader
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.CharBuffer
|
||||||
|
import java.nio.charset.StandardCharsets
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
|
||||||
|
object MoneroMnemonic {
|
||||||
|
init {
|
||||||
|
NativeLoader.loadMnemonicsLibrary()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Supported languages based on mnemonics/electrum-words.cpp
|
||||||
|
val supportedLanguages = mapOf(
|
||||||
|
"ang" to "English (old)",
|
||||||
|
"de" to "German",
|
||||||
|
"en" to "English",
|
||||||
|
"eo" to "Esperanto",
|
||||||
|
"es" to "Spanish",
|
||||||
|
"fr" to "French",
|
||||||
|
"it" to "Italian",
|
||||||
|
"ja" to "Japanese",
|
||||||
|
"jbo" to "Lojban",
|
||||||
|
"nl" to "Dutch",
|
||||||
|
"pt" to "Portuguese",
|
||||||
|
"ru" to "Russian",
|
||||||
|
"zh" to "Chinese (simplified)",
|
||||||
|
)
|
||||||
|
|
||||||
|
fun generateMnemonic(entropy: ByteArray, locale: Locale = Locale.US): MnemonicCode {
|
||||||
|
require(entropy.isNotEmpty()) { "Entropy must not be empty" }
|
||||||
|
require(entropy.size % 4 == 0) { "Entropy size must be a multiple of 4" }
|
||||||
|
|
||||||
|
val language = supportedLanguages[locale.language]
|
||||||
|
?: throw IllegalArgumentException("Invalid locale: $locale")
|
||||||
|
|
||||||
|
return requireNotNull(nativeElectrumWordsGenerateMnemonic(entropy, language))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun recoverEntropy(words: CharArray): MnemonicCode? =
|
||||||
|
recoverEntropy(CharBuffer.wrap(words))
|
||||||
|
|
||||||
|
fun recoverEntropy(words: CharSequence): MnemonicCode? =
|
||||||
|
recoverEntropy(CharBuffer.wrap(words))
|
||||||
|
|
||||||
|
private fun recoverEntropy(words: CharBuffer): MnemonicCode? {
|
||||||
|
require(words.isNotEmpty()) { "Input words must not be empty" }
|
||||||
|
|
||||||
|
val byteBuffer = StandardCharsets.UTF_8.encode(words)
|
||||||
|
val wordsBytes = ByteArray(byteBuffer.remaining())
|
||||||
|
byteBuffer.get(wordsBytes)
|
||||||
|
byteBuffer.array().fill(0)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
nativeElectrumWordsRecoverEntropy(wordsBytes)
|
||||||
|
} finally {
|
||||||
|
wordsBytes.fill(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@CalledByNative("mnemonics/mnemonics.cc")
|
||||||
|
@JvmStatic
|
||||||
|
private fun buildMnemonicFromNative(
|
||||||
|
entropy: ByteArray,
|
||||||
|
wordsBytes: ByteArray,
|
||||||
|
language: String,
|
||||||
|
): MnemonicCode {
|
||||||
|
val byteBuffer = ByteBuffer.wrap(wordsBytes)
|
||||||
|
val words = StandardCharsets.UTF_8.decode(byteBuffer)
|
||||||
|
val languageCode = supportedLanguages.entries.first { it.value == language }.key
|
||||||
|
|
||||||
|
return try {
|
||||||
|
MnemonicCode(entropy, words, Locale(languageCode))
|
||||||
|
} finally {
|
||||||
|
words.array().fill(0.toChar())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private external fun nativeElectrumWordsGenerateMnemonic(
|
||||||
|
entropy: ByteArray,
|
||||||
|
language: String,
|
||||||
|
): MnemonicCode?
|
||||||
|
|
||||||
|
private external fun nativeElectrumWordsRecoverEntropy(
|
||||||
|
source: ByteArray,
|
||||||
|
): MnemonicCode?
|
Loading…
Reference in New Issue
Block a user