mirror of
https://github.com/mollyim/monero-wallet-sdk.git
synced 2024-12-24 23:19: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
|
||||
mnemonics/jni_cache.cc
|
||||
mnemonics/jni_loader.cc
|
||||
mnemonics/mnemonics.cc
|
||||
)
|
||||
|
||||
add_library(monero_mnemonics SHARED ${COMMON_SOURCES} ${MNEMONICS_SOURCES})
|
||||
|
||||
target_link_libraries(
|
||||
monero_mnemonics
|
||||
PUBLIC
|
||||
Monero::easylogging
|
||||
PRIVATE
|
||||
Monero::electrum_words
|
||||
OpenSSL::SSL
|
||||
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"
|
||||
|
||||
namespace monero {
|
||||
@ -10,6 +12,8 @@ JNI_OnLoad(JavaVM* vm, void* reserved) {
|
||||
return JNI_ERR;
|
||||
}
|
||||
|
||||
initializeJniCache(env);
|
||||
|
||||
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
|
||||
contrib/epee/src/memwipe.c
|
||||
contrib/epee/src/mlocker.cpp
|
||||
contrib/epee/src/wipeable_string.cpp
|
||||
src/mnemonics/electrum-words.cpp
|
||||
)
|
||||
|
||||
@ -21,6 +24,9 @@ add_library(
|
||||
electrum_words STATIC ${ELECTRUM_WORDS_SOURCES} ${ELECTRUM_WORDS_OVERRIDES}
|
||||
)
|
||||
|
||||
# Disable Easylogging++ logging
|
||||
add_definitions(-DELPP_DISABLE_LOGS)
|
||||
|
||||
target_include_directories(
|
||||
electrum_words
|
||||
PUBLIC
|
||||
|
@ -55,7 +55,7 @@ bool RemoteNodeClient::invoke(const boost::string_ref uri,
|
||||
header << p.first << ": " << p.second << "\r\n";
|
||||
}
|
||||
try {
|
||||
ScopedJvmLocalRef<jobject> j_response = {
|
||||
ScopedJvmLocalRef<jobject> j_response(
|
||||
env, m_wallet_native.callObjectMethod(
|
||||
env, WalletNative_callRemoteNode,
|
||||
nativeToJvmString(env, method.data()).obj(),
|
||||
@ -63,7 +63,7 @@ bool RemoteNodeClient::invoke(const boost::string_ref uri,
|
||||
nativeToJvmString(env, header.str()).obj(),
|
||||
nativeToJvmByteArray(env, body.data(), body.length()).obj()
|
||||
)
|
||||
};
|
||||
);
|
||||
m_response_info.clear();
|
||||
if (j_response.is_null()) {
|
||||
return false;
|
||||
|
@ -34,7 +34,9 @@ void initializeJniCache(JNIEnv* env) {
|
||||
Logger_logFromNative = logger
|
||||
.getMethodId(env, "logFromNative", "(ILjava/lang/String;Ljava/lang/String;)V");
|
||||
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
|
||||
.getMethodId(env,
|
||||
"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