From 6560ee79adb6f666fe1e2dab63a0ff67a0b5788a Mon Sep 17 00:00:00 2001 From: Oscar Mira Date: Tue, 2 Jan 2024 20:33:54 +0100 Subject: [PATCH] lib: add standard mnemonic support --- .../monero/mnemonics/MoneroMnemonicTest.kt | 45 ++++++++++ lib/android/src/main/cpp/CMakeLists.txt | 5 ++ .../src/main/cpp/mnemonics/jni_cache.cc | 21 +++++ .../src/main/cpp/mnemonics/jni_cache.h | 17 ++++ .../src/main/cpp/mnemonics/jni_loader.cc | 4 + .../src/main/cpp/mnemonics/mnemonics.cc | 78 ++++++++++++++++ .../cpp/monero/electrum_words/CMakeLists.txt | 6 ++ .../src/main/cpp/wallet/http_client.cc | 4 +- lib/android/src/main/cpp/wallet/jni_cache.cc | 4 +- .../im/molly/monero/mnemonics/MnemonicCode.kt | 77 ++++++++++++++++ .../molly/monero/mnemonics/MoneroMnemonic.kt | 90 +++++++++++++++++++ 11 files changed, 348 insertions(+), 3 deletions(-) create mode 100644 lib/android/src/androidTest/kotlin/im/molly/monero/mnemonics/MoneroMnemonicTest.kt create mode 100644 lib/android/src/main/cpp/mnemonics/jni_cache.cc create mode 100644 lib/android/src/main/cpp/mnemonics/jni_cache.h create mode 100644 lib/android/src/main/cpp/mnemonics/mnemonics.cc create mode 100644 lib/android/src/main/kotlin/im/molly/monero/mnemonics/MnemonicCode.kt create mode 100644 lib/android/src/main/kotlin/im/molly/monero/mnemonics/MoneroMnemonic.kt diff --git a/lib/android/src/androidTest/kotlin/im/molly/monero/mnemonics/MoneroMnemonicTest.kt b/lib/android/src/androidTest/kotlin/im/molly/monero/mnemonics/MoneroMnemonicTest.kt new file mode 100644 index 0000000..2bc86c3 --- /dev/null +++ b/lib/android/src/androidTest/kotlin/im/molly/monero/mnemonics/MoneroMnemonicTest.kt @@ -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) + } + } +} diff --git a/lib/android/src/main/cpp/CMakeLists.txt b/lib/android/src/main/cpp/CMakeLists.txt index 121cb5e..e3ce4e4 100644 --- a/lib/android/src/main/cpp/CMakeLists.txt +++ b/lib/android/src/main/cpp/CMakeLists.txt @@ -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 ) diff --git a/lib/android/src/main/cpp/mnemonics/jni_cache.cc b/lib/android/src/main/cpp/mnemonics/jni_cache.cc new file mode 100644 index 0000000..2e2e153 --- /dev/null +++ b/lib/android/src/main/cpp/mnemonics/jni_cache.cc @@ -0,0 +1,21 @@ +#include "jni_cache.h" + +namespace monero { + +// im.molly.monero.mnemonics +ScopedJvmGlobalRef 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 diff --git a/lib/android/src/main/cpp/mnemonics/jni_cache.h b/lib/android/src/main/cpp/mnemonics/jni_cache.h new file mode 100644 index 0000000..f7a7d77 --- /dev/null +++ b/lib/android/src/main/cpp/mnemonics/jni_cache.h @@ -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 MoneroMnemonicClass; +extern jmethodID MoneroMnemonic_buildMnemonicFromNative; + +} // namespace monero + +#endif // MNEMONICS_JNI_CACHE_H__ diff --git a/lib/android/src/main/cpp/mnemonics/jni_loader.cc b/lib/android/src/main/cpp/mnemonics/jni_loader.cc index 30761e8..ba702d7 100644 --- a/lib/android/src/main/cpp/mnemonics/jni_loader.cc +++ b/lib/android/src/main/cpp/mnemonics/jni_loader.cc @@ -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; } diff --git a/lib/android/src/main/cpp/mnemonics/mnemonics.cc b/lib/android/src/main/cpp/mnemonics/mnemonics.cc new file mode 100644 index 0000000..ca39cc5 --- /dev/null +++ b/lib/android/src/main/cpp/mnemonics/mnemonics.cc @@ -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 entropy = jvmToNativeByteArray(env, JvmParamRef(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 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 words = jvmToNativeByteArray(env, JvmParamRef(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 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 diff --git a/lib/android/src/main/cpp/monero/electrum_words/CMakeLists.txt b/lib/android/src/main/cpp/monero/electrum_words/CMakeLists.txt index f0dfd4c..54ee104 100644 --- a/lib/android/src/main/cpp/monero/electrum_words/CMakeLists.txt +++ b/lib/android/src/main/cpp/monero/electrum_words/CMakeLists.txt @@ -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 diff --git a/lib/android/src/main/cpp/wallet/http_client.cc b/lib/android/src/main/cpp/wallet/http_client.cc index 9b567f8..15cd2c9 100644 --- a/lib/android/src/main/cpp/wallet/http_client.cc +++ b/lib/android/src/main/cpp/wallet/http_client.cc @@ -55,7 +55,7 @@ bool RemoteNodeClient::invoke(const boost::string_ref uri, header << p.first << ": " << p.second << "\r\n"; } try { - ScopedJvmLocalRef j_response = { + ScopedJvmLocalRef 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; diff --git a/lib/android/src/main/cpp/wallet/jni_cache.cc b/lib/android/src/main/cpp/wallet/jni_cache.cc index 5c9a465..061fb45 100644 --- a/lib/android/src/main/cpp/wallet/jni_cache.cc +++ b/lib/android/src/main/cpp/wallet/jni_cache.cc @@ -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, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILjava/lang/String;JIIJJJJZZ)V"); + .getMethodId(env, + "", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;IILjava/lang/String;JIIJJJJZZ)V"); WalletNative_callRemoteNode = walletNative .getMethodId(env, "callRemoteNode", diff --git a/lib/android/src/main/kotlin/im/molly/monero/mnemonics/MnemonicCode.kt b/lib/android/src/main/kotlin/im/molly/monero/mnemonics/MnemonicCode.kt new file mode 100644 index 0000000..0a9e9c6 --- /dev/null +++ b/lib/android/src/main/kotlin/im/molly/monero/mnemonics/MnemonicCode.kt @@ -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 { + + 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 = object : Iterator { + 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 checkNotDestroyed(block: () -> T): T { + check(!destroyed) { "MnemonicCode has already been destroyed" } + return block() + } +} diff --git a/lib/android/src/main/kotlin/im/molly/monero/mnemonics/MoneroMnemonic.kt b/lib/android/src/main/kotlin/im/molly/monero/mnemonics/MoneroMnemonic.kt new file mode 100644 index 0000000..ad3ecde --- /dev/null +++ b/lib/android/src/main/kotlin/im/molly/monero/mnemonics/MoneroMnemonic.kt @@ -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?