Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Medusa Wallet to Showcase #1344

Open
wants to merge 3 commits into
base: staging
Choose a base branch
from

Conversation

Fell-x27
Copy link
Contributor

Checklist

Showcase addition

  • Title: Medusa Wallet
  • Description: A lightweight Cardano wallet focused on maximum privacy and user protection, enabling easy and secure access to funds even in untrusted or compromised environments.
  • Website: https://adawallet.io
  • Source: none
  • Tags:
    • wallet

rphair
rphair previously approved these changes Oct 18, 2024
Copy link
Collaborator

@rphair rphair left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was monitoring this as part of the CIP process and observed that it represents good implementation of standards and meets community expectations.

src/data/showcases.js Outdated Show resolved Hide resolved
@rphair rphair changed the title Medusa wallet added; Add Medusa Wallet to Showcase Oct 18, 2024
@rphair rphair added the showcase Indicates a PR/issue on showcase label Oct 18, 2024
Co-authored-by: Robert Phair <[email protected]>
{
title: "Medusa Wallet",
description:
"A lightweight Cardano wallet focused on privacy and user protection, enabling easy and secure access to funds even in untrusted or compromised environments.",

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medusa Wallet has, since version one, always stored the user's private key on their servers. It used to send their private key in plain text, and now they've added an obfuscation step with OTP to give a false sense of security. The description provided for the wallet is blatant lie and shows really bad faith from the authors. They've always pitched themselves as a secure trustless solution while secretly aggregating everybody's recovery phrases and therefore I strongly believe they, or any other product the same developer also creates, should never be trusted and never listed on any official portal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medusa Wallet has, since version one, always stored the user's private key on their servers.

Yes, but they are encrypted with the user's payment password, which is stored nowhere else. This allows users to access their wallet from any device without having to re-enter the mnemonic each time. At the same time, there’s an option to delete the wallet from the app.

Moreover, technically, this is safer than storing the keys in an SQLite file on the user's computer, as Daedalus does, for example. The reason is that most users' computers are inherently compromised.

It used to send their private key in plain text

That was a limitation due to using the cardano-wallet backend, but now Medusa runs on its own backend, and all cryptographic operations are handled strictly on the client side, in a separate isolated thread with no access for browser extensions or injected code. Plain text hasn’t been used for a long time now, since Medusa 2.0 was rewritten from scratch, avoiding any architectural limitations tied to cardano-wallet.

Even the user interface, which might look similar to the previous version, was rewritten from scratch. Medusa’s UI engine is its own, with close to zero dependencies, and those that are used have been thoroughly reviewed and locked.

Now they've added an obfuscation step with OTP to give a false sense of security

This is completely inaccurate, Sebastien. And honestly, it was quite harsh. Let’s break it down point by point.

First, OTP was always a part of Medusa, not just now. And it’s used solely for logging in and confirming wallet deletion, ensuring that the deletion request is initiated by the actual owner. But...there are plans to add OTP as an optional second factor for transactions.

Second, there is no obfuscation happening. Private keys are encrypted/decrypted with the payment password, strictly on the client side, using the cardano_memory_combine algorithm.

Third, communication with the server is done via a custom encrypted channel with a shared symmetric key, employing a Diffie-Hellman key exchange with a 4096-byte p parameter, ensuring an extremely high factoring cost. This method also allows dynamic key generation for each session and even within a session, offering protection against traffic analysis and passive MITM attacks. As for active attacks, well, that’s more in the “if NSA is after you” category.

Fourth, the encryption of transmitted data uses 64-bit AES-CTR, which makes reverse decryption via pattern analysis practically impossible.

Fifth, keys are downloaded to the client only when needed for signing, and they’re immediately moved to a parallel thread, bypassing the tab's runtime. Decryption also happens there, preventing interception of either the encrypted or decrypted data. Code injection, wrapping, and method swapping attacks are also mitigated.

Sixth, all of the above is easily verifiable. If you open Medusa in the browser’s developer tools, you’ll see that the client files are not obfuscated, not repacked with webpack, nor bundled together. They are organized into separate folders and files, with well-named variables and functions. It’s like an open book, with assurance that this is indeed the code being executed.

Seventh, Medusa even separates web traffic and user data on the server side. The web server hosting the site and the server running db-sync are different machines, with the latter being inaccessible publicly. Multiple cardano-node instances run on several other machines for redundancy through an SSH tunnel, making Medusa not only secure but also highly reliable and fail-safe.

The description provided for the wallet is a blatant lie and shows really bad faith from the authors.

I don’t want to get into an argument, but outright lying is what you are doing. This is an indication of bad faith on your part as a reviewer. You've made numerous unfounded accusations, which makes it clear you didn’t even try to understand the matter. Yet, you passed judgment and publicly slandered us.

They've always pitched themselves as a secure trustless solution while secretly aggregating everybody's recovery phrases.

Again, this is slander and aggression. Medusa has never secretly collected recovery phrases. The mnemonic never leaves the client. Private keys only leave the client in an encrypted form, over an encrypted channel. Medusa doesn’t even store user logins—just their hashes.

If you don’t want to use a mnemonic, Medusa supports Ledger and will soon support Keystone as well.

I strongly believe they, or any other product the same developer also creates, should never be trusted and never listed on any official portal.

It seems like this is something personal. Sebastien, you are a respected developer, but what you wrote here is utterly unprofessional. This does not give you the right to make accusations. It does not give you the right to present assumptions as facts. If you have concerns or questions, you could’ve asked them constructively, without such an insulting tone. It’s undeserved.

Let me remind you that Medusa emerged in the Cardano ecosystem around the same time as Yoroi, for the same reason—the lack of a lightweight wallet. Back then, the Cardano ecosystem had neither adequate libraries nor proper tools. cardano-wallet was the only thing publicly available, and I improvised with what I had. In essence, the first version of Medusa was something like a remote Daedalus. The current version is a completely different product—no compromises, no issues. It also fully supports Byron, being the only lightweight wallet capable of this, including Daedalus wallets that it can synchronize instantly, without using the user's private key for address validation. Which, in theory, should be impossible, but I made it happen and have already helped many people.

I’m a member of the Guild, a Cardano Ambassador, and my reputation is spotless, as is the project's. I value this reputation. But you’ve made accusations, essentially accusing us of breaking the law.

I’m open to answering any questions about Medusa. My claims are easily verifiable. I hide nothing, and I deceive no one. I expect an apology.

Copy link

@SebastienGllmt SebastienGllmt Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plain text hasn’t been used for a long time now

the fact that plain text was ever user at all without massive warnings to the user is really bad in itself. If this was just at one-time very brief thing from a new dev, maybe I could give it a pass. However, this has been going on for many years and MANY people have told you not to do this. Every time you get caught, you come back later with a new security feature that makes it slightly harder to detect the fact that you're still sending information that can be used to drain the user's wallet to your server. If for some strange reason this really is not on purpose (I can't even fathom how that would be possible given how many times you've been rejected for this reason), you have to understand that this isn't personal and it's normal for technical members of the community to vet the security of projects before they're added to lists such as this

Second, there is no obfuscation happening. Private keys are encrypted/decrypted with the payment password, strictly on the client side, using the cardano_memory_combine algorithm.

I don't think this is true. When you create a new Medusa wallet, it does the following:

  1. Create a wallet and save the root key to storage (this is the same as other wallets)
let entropy = bip39.mnemonicToEntropy(mnemonic);
let rootKey = cardano_lib.Bip32PrivateKey.from_bip39_entropy(hex2buf(entropy), hex2buf(secondFactor),)
await self.keyRing.addKey(rootKey.as_bytes(), null, [0], self.keyRing.keyTypes.private, schema, password, true));
  1. When you save your wallet, the key is sent to the server (no non-custodial wallet does this)

The save_wallet network call includes

let payload = new medusaNetwork.Payload();
// this is the user's BIP32PrivateKey encrypted with their spending password. The same root key generated from step 1 mentioned above
let keychain = await medusaFunds.crypto.exportKeyChain(this.keyId);
payload.data.set("keychain", keychain);
let walletSavingResult = await medusaNetwork.performAction("funds", "save_wallet", payload, true, false);

Even if you encrypt it with the user's spending password locally before sending it to your server, you can just create a rainbow table of passwords to decrypt this information. There is no good reason why the user's root key should be sent as part of the payload

Copy link
Contributor Author

@Fell-x27 Fell-x27 Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the fact that plain text was ever used at all without massive warnings to the user is really bad in itself

This is precisely why Medusa was rewritten from scratch as soon as the opportunity arose, and when I gathered enough feedback and suggestions from the community. Let me remind you that we are discussing the new version specifically.

Create a wallet and save the root key to storage (this is the same as other wallets)

let entropy = bip39.mnemonicToEntropy(mnemonic);
let rootKey = cardano_lib.Bip32PrivateKey.from_bip39_entropy(hex2buf(entropy), hex2buf(secondFactor),)
await self.keyRing.addKey(rootKey.as_bytes(), null, [0], self.keyRing.keyTypes.private, schema, password, true));

What's wrong with that code? I convert the mnemonic to entropy, derive the private key, and then add it to the keyring, which the wallet will use later. What's the issue here? Since you started digging, let’s go deeper and see what’s happening in the self.keyRing.addKey function:

  addKey: async function (key, parentId, path, type, schema = null, password = "", returnParentId = false) {
        if (!(key instanceof Uint8Array)) {
            if (key.to_bytes) {
                key = key.to_bytes();
            }

            if (key.as_bytes) {
                key = key.as_bytes();
            }

            if (typeof key === 'string' || key instanceof String) {
                key = str2ab(key);
            }

        }

        let pureId, id;


        if ((type === self.keyRing.keyTypes.private)) {
            if ((key.length === 96)) {
                const xprv96Key = cardano_lib.Bip32PrivateKey.from_bytes(key);
                const xprv128Key = xprv96Key.to_128_xprv();
                const xprv128hex = buf2hex(xprv128Key);
                const pubPart = xprv128hex.substring(128);
                id = await self.sha256(pubPart + path.join("/") + type);
            } else {
                id = await self.sha256((buf2hex(key)).substring(128) + path.join("/") + type);
            }
        } else {
            id = await self.sha256((buf2hex(key)) + path.join("/") + type);
        }


        let encrypted = ((password !== null) && (password !== ""));
        key = (encrypted) ? (cardano_tools.cardanoMemoryCombine(cardano_tools.Buffer(key), password)) : (key);

        let pureParentId = parentId || (await self.sha256(id + "parent"));
        parentId = parentId || await self.sha256(pureParentId + "|" + schema);
        id = await self.sha256(id + "|" + schema);
        if (!this.storage.hasOwnProperty(id)) {
            this.storage[id] = {
                "key": key,
                "parentId": parentId,
                "children": [],
                "path": path,
                "encrypted": encrypted,
                "type": type
            };
            if (this.storage.hasOwnProperty(parentId)) {
                this.storage[parentId].children.push(id);
            }
        }
        if (!returnParentId) {
            return id;
        } else {
            return {keyId: id, parentId: parentId, pureParentId: pureParentId};
        }
    },

There are only 2 lines where password is used:

let encrypted = ((password !== null) && (password !== ""));
key = (encrypted) ? (cardano_tools.cardanoMemoryCombine(cardano_tools.Buffer(key), password)) : (key);

The first one checks whether encryption/decryption is needed, and the second one performs it if required. After that, the password is not stored or used in any other way. The wallet does exactly what it should, just as I described. What’s the contradiction here? Afterward, the key is added to the keyring within a "virtual ledger". You also didn’t mention that this code is taken from crypto_worker.js, not from tab code. This is a thread responsible for crypto storage and crypto processing, dealing with key management, derivation, signing, and even transaction submission. It only returns synthetic key identifiers and transaction hashes, nothing more. So yes, the private key is stored there to enable wallet functionality, because... that’s how wallets work.

I don't think this is true.

But the code shows that my words are true. You yourself provided proof that Medusa uses the concept of a "virtual ledger," storing keys in the keyring only in an encrypted form, without storing the password. After this operation, the key cannot be extracted without the password—even at the level of the virtual ledger code.

The save_wallet network call includes

It includes the stuff to save the wallet. Because Medusa does it. Yes. Encrypted. Well... I still don't see any reason to accuse me of anything. I still don’t see a reason to say that I lied (since I have proven otherwise), and I still don’t see any justification for your extremely insulting claim: they've added an obfuscation step with OTP to give a false sense of security. Especially since above are the proofs of the contrary.

you can just create a rainbow table of passwords to decrypt this information

Okay, let’s talk about cryptography, hashes, and rainbow tables. Because I see that you don’t understand what you’re talking about, or you’re deliberately ignoring the facts, as was the case earlier. No problem, let’s go.

  1. Hashes. Seba, you correctly pointed out that I never use passwords in plaintext; instead, I hash them. I use the SHA-256 hash. During the early development stages, when the project was strictly local, I used whirlpool. It’s a great thing but overkill when you're using an XOR-like algorithm (cardano-memory-combine) for encryption, which cannot effectively use a password longer than the message length. So, I switched to SHA-256.

But if I don’t store the password, why do I hash it instead of using it "as is"? Because it’s a best practice in the crypto industry, and it’s even suggested in the cardano-wallet documentation, although cardano-wallet itself doesn’t hash passwords. This lets you get a encryption key of a guaranteed length of 32 bytes, which is a lot. Medusa also evaluates the strength of a user’s password and gives suggestions if it’s weak, but through hashing, we can at least avoid attempts at reversing the XOR encryption by guessing password length. This is quite good since it prevents an attacker, in the event of key theft, from narrowing down the range of possible passwords for a future brute-force attack. Remember this point; it's important.

  1. Let me play along a bit and assume that password hashes appear in my code as part of the wallet object and that I indeed used to transmit them in the past. A long time ago, during early development stages, these hashes were used for recovery in case the user lost their authenticator. But now, there's no need for them, so they aren't stored. And, by the way, if you look into the code, you might still find where I didn’t remove the generation of these hashes!
 changePassword: async function (owner, accountId, oldPassword, newPassword) {
        const xPubRecord = await self.keyRing.getKey(accountId);

        let reEncryptionResult = await self.keyRing.reEncryptKey(xPubRecord.parentId, oldPassword, newPassword, xPubRecord);

        if (reEncryptionResult.status === "success") {
            reEncryptionResult.message = await self.getSaltyHash(await self.whirlpool(newPassword + '' + owner), saltLength);
        }
        return reEncryptionResult;
    },

And you’ll see that these hashes are not the ones used for encrypting the password. These were salted whirlpool hashes. Whirlpool itself is great, and with salt, it’s an unbreakable thing. It was safe as it was, and now it’s not used at all. I should probably remove that line entirely, it’s pointless now.

  1. Rainbow tables! Seba, I assumed you knew what these are and why they are used. Maybe you misunderstood the logic of my code. Let’s break it down.

Rainbow tables are used to find hash collisions. Their purpose is to find a message that matches a given hash, like, say, a forum password, right? If hash is known, but password is not. Now let’s see why this argument doesn’t work.

Let’s assume that building rainbow tables for SHA-256 is a good idea, and I just happen to have an extra ~7 petabytes (!) in my pocket for that. Let’s say I do. Why wouldn’t it make sense:

  • Finding a collision wouldn’t be enough because encryption is done with the password, and we need the exact bitwise original.
  • Medusa doesn’t use the original password, Seba. It uses the hash itself. What exactly are you planning to extract from these rainbow tables? How would they help you? They wouldn’t. The hash is the password. And it is unknown. You need to crack that specifically! How could you solve it with rainbow tables?
  • So let me play along again—for a wallet break-in, I’d need to run a brute-force attack, not rainbow tables :) Brute force is, in essence, a white noise search...

Now let’s talk about brute force! As you’ve noticed, I use the cardano-memory-combine algorithm for encryption. There were three reasons for that:

  • It provides backward compatibility of the new Medusa with keys stored in cardano-wallet, enabling seamless migration of the user base. Users just saw their wallets updated, and their passwords continued to work. Awesome!
  • It’s an XOR-like algorithm "on steroids"—I love that family of algorithms.
  • This algorithm is incredibly expensive and slow. Just unbelievably expensive and slow. Encoding/decoding a key with a 256-bit password takes 1-2 seconds on my machine, and it's a pretty powerful one. And brute-forcing original passwords is pointless—we don’t know the original length, language, etc.; in the end, it’s still a 256-bit hash. So you’d have to brute-force the hashes themselves. At a speed of 1-2 per second. Good luck living long enough to see even 10% progress.

There is no good reason why the user's keychain would be sent as part of the payload

Actually, there is. Once you restore your wallet in Medusa on a trusted device, you gain the ability to access it from anywhere in the world, from any device, in any situation—even a compromised one. That’s its killer feature. Medusa doesn’t download the private key from the server unless it’s needed for signing something. You don’t need to enter the mnemonic every time you want to check your balance—entering a mnemonic on any device is a major attack vector for input interception, which I think you know.

For this reason, Medusa doesn’t use the classic "login-password" scheme for access, instead opting for one-time codes. These codes are single-use and change constantly—so even if they’re stolen during input, it doesn’t matter, because Medusa invalidates them instantly. Plus, we also support hardware wallets! Medusa keeps their keyring as well, but without storing the private key, of course. This way, you can check your balance wherever you are, without having to connect your Ledger in public and draw attention. It’s convenient. It’s secure. It eliminates not only direct attack vectors but also indirect ones.

And yes, I do extract the keyring from the "virtual ledger" during the saving procedure, but it is encrypted in such a way that only the server can decrypt it. This protects them from interception both at the level of client-side code (like extensions) and at the network level—even if HTTPS is compromised.

You might ask, "What if someone steals my account?" Just create a new one and restore your wallet there—it will instantly be disconnected from the old account. Because only the one who owns the keys owns the wallet.


And since we’re talking about what someone might do, this isn’t really an argument but more of a mental experiment. You probably have a kitchen knife at home. You could take it and kill someone on the street. Does that make you a criminal, someone who should be preemptively accused in front of everyone? I don’t think so.

For that reason, I still expect an apology. It's not a problem to admit that you overreacted.

Copy link

@SebastienGllmt SebastienGllmt Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let’s assume that building rainbow tables for SHA-256 is a good idea, and I just happen to have an extra ~7 petabytes (!) in my pocket for that

Nobody does this. Rainbow tables always starts the most commonly used passwords first and computes their hash so you can do a reverse lookup later. This is exactly what you would need to crack user's wallets as they onboard

It includes the stuff to save the wallet

Sending the user's root private key over the network is definitely not required. You're not fooling anybody with these walls of text of unrelated stuff when you're sending information to decrypt the user's private keys over the network.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when you're sending information to decrypt the user's private keys over the network.

What? Which information? Did you even try to read what I wrote?

This is exactly what you need to crack user's wallets as they onboard.

I see, you didn't...

You're not fooling anybody

Sebastien, stop talking to me like that.

You have insulted and slandered me, attributing malicious intent to me, yet you haven’t found anything that would support your accusations, while at the same time you confirmed my words. Still, you’re trying to find a black cat in a dark room, even though it’s not there.

Notice that I’m trying to remain as polite and constructive as possible, even though you deserve that much less right now than I deserve your accusations.

Copy link
Contributor Author

@Fell-x27 Fell-x27 Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

However, this has been going on for many years and MANY people have told you not to do this.

Are you sure you're not confusing me with someone else? What "many years"? What "many people"? Medusa V1 was quite a modest project with a very limited audience. I know almost every my old user. And we are in connect. The only person who brought this up to me was Priyank, at the very beginning. And at that time, it was the only available implementation. And I said that yep, I'll change it when possible. I mean the plain text sending due to cardano-wallet limitations. But I've never denied that the ability to enter the mnemonic once and use the wallet from anywhere is its main advantage. And I never intended to abandon this. Not all wallets have to be reskins of Yoroi without their own features.

Every time you get caught

Caught? Seba, what are you even talking about? Who has "caught" me doing what? How about you start providing some proof for your claims? I've never hidden anything from anyone.

The one who got caught is you. Caught lying. Repeatedly. You know this, and you keep doing it to avoid admitting the truth, going further and further. Without any proof.

you come back later with a new security feature that makes it slightly harder to detect the fact that you're still sending information that can be used to drain the user's wallet to your server

This is also an absolute lie and pure fiction. How about some proof? When you joined this discussion, it was already clear you had no idea what you were talking about, but now you've started to openly lie and make things up. Medusa had exactly two iterations. There was never any security feature that makes it slightly harder to detect. Detect what? The core feature that makes it convenient for people, allowing them not to enter their mnemonic every time they want to check the balance?

I only tried to introduce Medusa here once, but it was in beta, and Tommy said that wasn't allowed. And I completely forgot about it until recently when someone reminded me.

There was an old engine and now there's a new one. But you're lying. And you're twisting the story to save face. Making up things as you go, hoping people will believe you because you're a well-known figure. That's low.

You’re making some very strong claims but providing no proof whatsoever. And when you did try to inspect the code, you only managed to disprove your own lies.

you have to understand that this isn't personal and it's normal for technical members of the community to vet the security of projects before they're added to lists such as this

Except you didn’t check anything. You barged in with accusations, made-up facts, not knowing anything about what you were talking about. And when you tried to verify your claims, it turned out they were baseless.

After that, you resorted to personal attacks and kept appealing to emotions like you're standing in front of a jury. And you're even making up incredible stories now.

Your main argument is that someone could theoretically do something. Well, let’s play this game together then. Seba—I think you were stealing mnemonics through Flint Wallet because you could, and you were abusing people's trust. Because any application dealing with private data is inherently not trustless. And if you had the opportunity, you took it.

There will be no proof, but instead, I can come up with a lot of interesting stories.

Is that how this works?

Copy link
Contributor Author

@Fell-x27 Fell-x27 Oct 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't even fathom how that would be possible given how many times you've been rejected for this reason.

Many times? Why imagine things? There's the Git history. Let's take a look!

https://github.com/cardano-foundation/developer-portal/pulls?q=Medusa

Oops... it seems I'm right again, and you're just making things up.

@katomm, can I share our conversation regarding the rejected PR? To show, why it was rejected?

Updated: oh, you wrote it as a reason right there, ok, thanks.

@rphair rphair dismissed their stale review October 19, 2024 05:18

Withholding approval pending resolution of #1344 (review)

@Fell-x27 Fell-x27 requested a review from rphair October 19, 2024 05:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
showcase Indicates a PR/issue on showcase
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants