lib: add standard mnemonic support

This commit is contained in:
Oscar Mira 2024-01-02 20:33:54 +01:00
parent bcbc9c33e3
commit 6560ee79ad
11 changed files with 348 additions and 3 deletions

View File

@ -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)
}
}
}

View File

@ -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
) )

View 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

View 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__

View File

@ -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;
} }

View 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

View File

@ -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

View File

@ -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;

View File

@ -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",

View File

@ -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()
}
}

View File

@ -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?