Dissecting the MyMonero Scam: Part Three

mymonero fraud May 31, 2021

Remember, 'MyMonero' claims that you can, "Get your entire transfer history instantly on any device with only your seed words."

They also state, "Never fret about losing your transaction history — there's no wallet file to back up."

When 'MyMonero' claims that they'll allow users to access any and all transactions immediately, we know that they must be associating transactions with a given public key.

However, based on what was explained in the prior section, the issues likely go significantly deeper than that.

Since the user is not running the blockchain at all and the server allegedly only has the public view key, there is no way that 'MyMonero' could provide instantaneous balances for users unless they also have the private view key. Remember, in the previous section we stated that the private view key is needed in order to scan the blockchain and ascertain whether a transaction has been encrypted to the recipient.

In the next section when we look at the code base of 'MyMonero', we'll see that the reality is even worse than we originally imagined.

MyMonero Code Review

For those that are wondering where to find the relevant source code for MyMonero repos, one need look no further than GitHub.

MyMonero
MyMonero has 21 repositories available. Follow their code on GitHub.

Specifically, we're going to start with the 'monero-rpc-server' repo, which contains all of the code that would be needed for the rpc server used by Monero (i.e., this is supposed to be indicative of the code that the 'MyMonero' hosted service runs on, server side).

The link to this portion of the code can be found here:

mymonero/mymonero-rpc-server
Contribute to mymonero/mymonero-rpc-server development by creating an account on GitHub.

Exploring the 'MyMonero-RPC-Server' Repo

We're going to start with src/main.js.

mymonero/mymonero-rpc-server
Contribute to mymonero/mymonero-rpc-server development by creating an account on GitHub.

Below is the full code, re-published for convenience (minus license considerations for brevity):

const path = require('path')
//
const document_store = new (require('./document_store.files'))({
	userDataAbsoluteFilepath: path.resolve("."),
	fs: require('fs')
})
//
const wallet_rpc_server = new (require('./wallet_rpc_server'))({
    document_store: document_store
})
wallet_rpc_server.start()
//
// TODO: daemon (block info) rpc server instance
//
//

Few Notes on the Code Above

The constant (variable) 'document_store' is defined with parameters that indicate where all of the files of users are stored at on the server.

The remaining code dictates the instantiation of the server (i.e., code ran to start the actual daemon for the server itself)

Exploring the 'MyMonero' document_store.files provisions

Here, we're going to look at the src/document_store.files.js file.

mymonero/mymonero-rpc-server
Contribute to mymonero/mymonero-rpc-server development by creating an account on GitHub.

Below we're going to take a look at the first part of the codebase. Contrary to the format we used last time, there will be annotations inserted by 'Librehash' in the code (there are no original annotations provided by the Monero team). These annotations are being inserted for clarity purposes (for those that are not super well-versed in reading Javascript.

const path = require('path')
//
// the term 'class DocumentStore' signals that the class is being   
// defined by all of the parameters that fall underneath the first '{' 
// bracket. 
class DocumentStore
{
	constructor(options)
	{
// below are definitions (const = constants)
		const self = this
		self.options = options
		{
// another definition
			const options_userDataAbsoluteFilepath = options.userDataAbsoluteFilepath
// the following code dictates that if the parameters for 
// 'options_userDataAbsoluteFilepath' are not provided or are
// undefined, then an error must be thrown that informs the
// operator that this variable must receive parameters
			if (!options_userDataAbsoluteFilepath || typeof options_userDataAbsoluteFilepath === 'undefined') {
				throw "options.userDataAbsoluteFilepath required"
			}
			//
			self.userDataAbsoluteFilepath = options_userDataAbsoluteFilepath
			//
			self.fs = options.fs
			if (!self.fs || typeof self.fs === 'undefined') {
				throw "options.fs required"
			}
		}

There's nothing too eventful in the code thus far until we get until we get the next portion:

}
// strip trailing slashes so we can just append path components with string ops internally (join is hairy on android due to it being a url instead of a path)
var pathTo_dataSubdir = self.userDataAbsoluteFilepath // dirs are annoying in web, so using a file ext for detection instead
while (pathTo_dataSubdir.endsWith('/')) {
    pathTo_dataSubdir = pathTo_dataSubdir.substring(0, pathTo_dataSubdir.length - 1)
}
self.pathTo_dataSubdir = pathTo_dataSubdir
// console.log("self.pathTo_dataSubdir" , self.pathTo_dataSubdir)

The code above dictate how to locate and select files on the server (likely as a result of user RPC requests).

'MyMonero' RPC Files for the Server Base

This file can be found at src/rpc_server_base.js.

mymonero/mymonero-rpc-server
Contribute to mymonero/mymonero-rpc-server development by creating an account on GitHub.

This is where the majority of the problematic code is located.

Let's check out the first portion below:

const http = require('http')
const url = require('url')
//
class Server
{
    constructor(options)
    {
        this.port = options.port || 18083
        this.server_name = options.server_name || "an RPC server"
    }
    //
    // Interface - Imperatives
    start()
    {
        const self = this
        if (self.hasStarted == true) {
            throw "[Server/start] Code fault; start() may only be called once"
            return
        }

We know that the server requires an http connection above (vs. https). There doesn't appear to be any provisions for TLS or SSL that would protect the connection to whatever the endpoint will be.

It should be assumed that one must construct their own code and merge it into the relevant Javascript files if they are to enjoy this feature.

If we skip over to the src/symmetric_string_cryptor.js file, we can see that passwords are passed to the server in plain text before eventually being transformed (server side) into the password & keys used to encrypt the user's file that's stored on the server.

The module used here is 'jscryptor' that's used; the source code for that npm module can be found here.

Openbase

For some reason, 'AES' in 'CBC' mode is used (which is entirely insecure).

See below for  the relevant code:

var crypto = require('crypto');
//
var currentVersionCryptorFormatVersion = 3;
var cryptor_settings = 
{
	algorithm: 'aes-256-cbc',
	options: 1, // this gets inserted into the format. should probably be renamed to something more concretely descriptive
	salt_length: 8,
	iv_length: 16,
	pbkdf2: 
	{
		iterations: 10000,
		key_length: 32
	},
	hmac: 
	{
		includes_header: true,
		algorithm: 'sha256',
		length: 32
	}
}

Brief Notes:

  • The 'iv length' is 16. Since 168 = 128 and the IV must have the same length as the block (128 bits), its likely that the numbers we see in the above code are in bytes (1 byte = 8 bits).*
  • Until we see how the code is implemented in the greater structure of the repo, there are no comments that can be made on how its instantiated. Looking at what's presented in this folder, it appears that the code is largely boiler plate standard for what one could expect from AES-CBC (defaults essentially).

Reviewing the 'Wallet_RPC_Methods.js' File

This is the one file where most of the impactful revelations about MyMonero will be made.

Specifically, we're going to see that:

  • A user's wallet is generated by the MyMonero server.
  • Their private view and spend keys are stored on server.
  • Their public view and spend keys are stored on server as well.
  • Each user's transaction history is stored on server as well (and the client that MyMonero has is designed to scan the blockchain for any additional.
  • Each of the user's keys are directly associated with their client (by IP address).
  • The encryption used for files that are stored on the server is entirely insecure and could lead to not only a given user's files being decrypted / discovered (i.e., their wallet password & credentials), attackers may be able to discover other secrets from the server as well.
  • The connection from client to server is entirely insecure (i.e., no TLS encryption or any other ad-hoc mechanism applied to grant the user some level of security). Thus, all information sent to the server is done in plain text.
  • There is no mechanism for users to wipe their data from the servers (or even request it if they wanted to; its mentioned in the code that there should be some way to 'zero out' the data - but since this is still labeled as 'TO DO', we'll assume that this never got done to this day).

With those points above said, let's begin digging into the relevant excerpts of code, starting with the code that dictates the creation of a new wallet on the 'MyMonero' server (remember we're in the RPC-server wallet repo).

async function _read_wallet_json_for_file_named(store, filename, password)
{
    let raw_str = await _contents_of_file_named(store, filename)
    if (!raw_str) {
        return null // wallet does not exist on disk yet
    }
    let plain_str = await cryptor.New_DecryptedString__Promise(raw_str, password)
    return JSON.parse(plain_str) // thrown exception will 'reject'
}
async function _write_wallet_json_for_file_named(store, filename, password, plain_doc)
{
    let str = await cryptor.New_EncryptedBase64String__Promise(JSON.stringify(plain_doc), password)
    await store.write(filename, str)
}

The code above essentially looks to see if the user in question has a wallet that's on the server yet. If the answer is no (i.e., return null // wallet does not exist on disk yet), then a new wallet is created.

The details of the wallet are stuffed in a JSON file (and given a file  name), then encrypted with a password. Of course, this password is provided by the user - but sent over plain text to the server, which then uses said password to instantiate the encryption from the previous file we examined.

The next few lines of code reveal all of the information that the wallet receives:

async function _store_wallet_with(
    store,
    filename, password,
    address, 
    view_key, spend_key, 
    pub_spend_key, pub_view_key,
    mnemonic, mnemonic_language
) {
    const plain_doc = 
    {
        address: address, 
        view_key: view_key, 
        spend_key: spend_key, 
        pub_spend_key: pub_spend_key, 
        pub_view_key: pub_view_key,
        mnemonic: mnemonic, 
        mnemonic_language: mnemonic_language
    }
    await _write_wallet_json_for_file_named(store, filename, password, plain_doc)
}

Few Notes About the Code Above

The first part of the code defines the different parameters for what's going to be included in the wallet. Specifically the terms that have yet to be defined in the file at this point are the, 'address', 'view_key', 'spend_key', 'pub_spend_key', 'pub_view_key', 'mnemonic', and 'mnemonic_language'.

The next part of the code where it says const plain_doc =, provides those terms that we mentioned above. In specific, 'address' refers to address. 'View_key' refers to the private view key. 'Spend_key' refers to the private spend key. 'Pub_spend_key' references the public spend key. 'Pub_view_key' references the public view key. 'Mnemonic' references the mnemonic phrase and 'mnemonic_language' references the language of the words produced by the mnemonic.

These conclusions are not assumptions but rather finite definitions in the code.

Specifically, between lines 248-251, we can see the code governing the definition for the "view key" (making it known that its a secret key) - see below:

function opened_wallet_struct__view_key__sec()
{
    return opened_wallet_struct.doc.view_key
}

In the code where it states, 'opend_wallet_struct__view__sec()', the (sec) is an abbreviation for "secret" [clearly].

The same construction exists for the private spend key as well:

function opened_wallet_struct__spend_key__sec()
{
    return opened_wallet_struct.doc.spend_key
}

Moving further, the construction for the mnemonic is even more obvious (see below):

function opened_wallet_struct__seed()
{
    return opened_wallet_struct.doc.mnemonic
}

More evidence that this is how the server client functions can be found under the 'test' directory in the same GitHub repository. Specifically located at tests/rpc_client__wallet__basic.spec.js.

mymonero/mymonero-rpc-server
Contribute to mymonero/mymonero-rpc-server development by creating an account on GitHub.

The constant for 'rpc_server_url' (defined in line 34), once again makes it clear that this is the server (not the client storing these files locally).

Explicity the code defines the 'rpc_server_url' string as shown below:

const rpc_server_url = 'http://localhost:18082/json'

"Localhost" refers to the host's computer, confirming that the executor of this script is to be considered the server (whom users are connecting with remotely).  Additionally it would make no sense for someone to be making RPC requests to access files that they hold locally...for any reason.

Below is more interesting code from the same test file that provides a bit more insight into exactly how the data is extracted from users by 'MyMonero':

const filename0 = "mytestwallet"
const password0 = "mytestpassword"
const wallet_language = "English"
//
const created_wallet_filename0 = filename0+"-"+(new Date()).getTime()
//
describe("RPC client tests - Wallet RPC - basic wallet functions", function()
{
	//
	// I. creating, closing, opening
	it("can create_wallet", function(done)
	{
		this.timeout(20 * 1000);
		//
		let rpc_req_id = "t1"
		let method = "create_wallet"
		let params = {
			filename: created_wallet_filename0,
			password: password0,
			language: wallet_language
		}
		_send_RPC_message(rpc_req_id, method, params, function(err, res_data) {
			if (err) {
				return done(err)
			}
			assert.equal(res_data.id, rpc_req_id)
			done()
		})
	});
	it("can close created wallet", function(done)
	{
		this.timeout(20 * 1000);
		//
		setTimeout(function() {
			let rpc_req_id = "t2"
			let method = "close_wallet"
			let params = {}
			_send_RPC_message(rpc_req_id, method, params, function(err, res_data) {
				if (err) {
					return done(err)
				}
				assert.equal(res_data.id, rpc_req_id)
				done()
			})
		}, 1000) // give the server a sec to stabilize
	})
	it("can open created, closed wallet", function(done)
	{
		this.timeout(20 * 1000);
		//
		let rpc_req_id = "t3"
		let method = "open_wallet"
		let params = {
			filename: created_wallet_filename0,
			password: password0
		}
		_send_RPC_message(rpc_req_id, method, params, function(err, res_data) {
			if (err) {
				return done(err)
			}
			assert.equal(res_data.id, rpc_req_id)
			done()
		})
	})
	it("can close opened created wallet", function(done)
	{
		this.timeout(20 * 1000);
		//
		setTimeout(function() {
				let rpc_req_id = "t4"
			let method = "close_wallet"
			let params = {}
			_send_RPC_message(rpc_req_id, method, params, function(err, res_data) {
				if (err) {
					return done(err)
				}
				assert.equal(res_data.id, rpc_req_id)
				done()
			})
		}, 1000) // give the server a sec to stabilize
	})
	//
	// II. restoring
	var deterministic_wallet_filename = "restored_wallet"+"-"+(new Date()).getTime() // so we don't get filename conflicts
	it("can restore_deterministic_wallet", function(done)
	{
		this.timeout(20 * 1000);
		//
		const addr1 = "43zxvpcj5Xv9SEkNXbMCG7LPQStHMpFCQCmkmR4u5nzjWwq5Xkv5VmGgYEsHXg4ja2FGRD5wMWbBVMijDTqmmVqm93wHGkg"
		const vk1 = "7bea1907940afdd480eff7c4bcadb478a0fbb626df9e3ed74ae801e18f53e104"
		const sk1 = "4e6d43cd03812b803c6f3206689f5fcc910005fc7e91d50d79b0776dbefcd803"
		const seedwords1 = "foxes selfish humid nexus juvenile dodge pepper ember biscuit elapse jazz vibrate biscuit"
		// const wallet_language = "English"
		//
		let rpc_req_id = "t5"
		let method = "restore_deterministic_wallet"
		let params = {
			filename: deterministic_wallet_filename, 
			password: password0,
			seed: seedwords1,
			restore_height: 0,
			seed_offset: "",
			autosave_current: true
		}
		_send_RPC_message(rpc_req_id, method, params, function(err, res_data) {
			if (err) {
				return done(err)
			}
			assert.equal(res_data.id, rpc_req_id)
			const result = res_data.result
			assert.equal(addr1, result.address)
			assert.equal("Wallet has been restored successfully.", result.info)
			assert.equal(seedwords1, result.seed)
			// assert.equal(result.was_deprecated, false) // TODO
			done()
		})
	})
	it("can close restored wallet", function(done)
	{
		this.timeout(20 * 1000);
		//
		setTimeout(function() {
			let rpc_req_id = "t6"
			let method = "close_wallet"
			let params = {}
			_send_RPC_message(rpc_req_id, method, params, function(err, res_data) {
				if (err) {
					return done(err)
				}
				assert.equal(res_data.id, rpc_req_id)
				done()
			})
		}, 4000); // give the WS server / client some time to sync up before closing the WS connection down
	})
	it("can reopen restored wallet for txs", function(done)
	{
		this.timeout(20 * 1000);
		//
		let rpc_req_id = "t7"
		let method = "open_wallet"
		let params = {
			filename: deterministic_wallet_filename,
			password: password0
		}
		_send_RPC_message(rpc_req_id, method, params, function(err, res_data) {
			if (err) {
				return done(err)
			}
			assert.equal(res_data.id, rpc_req_id)
			done()
		})
	})
});

Tags

cryptomedication

Happy to serve and help wherever I'm needed in the blockchain space. #Education #EthicalContent #BringingLibretotheForefront

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.