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

BIP38 decrypt may have a browser dependency (Safari 6.x issue) #56

Open
cantonbecker opened this issue Jan 8, 2014 · 71 comments
Open
Assignees

Comments

@cantonbecker
Copy link

I've been doing some testing with BIP38 decryption at bitaddress.org and ran into a funny issue when encrypting/decrypting wallets using the passphrase "घोडा स्टेपल" (no quotes) -- which is what you get when you use google translate to translate "horse staple" (no quotes) into Nepali.

My own BIP38 generator is, weirdly enough, giving me two different BIP38 encodings for the same wallet, whose details are:
public key 1ABCDF5v4oaodTPYnKfYvkfwuoa8PJkjMC
WIF key 5J4pcwBDPwPY1cdNqxTdmZWr7yCK8rXi9avFvezgYbmoatJpKGn

If I use Safari to encrypt with घोडा स्टेपल, I get
6PRNXA7M57uqSYXX2TXHkfNJEVMiWarkPkqUv3AsZa5r41u3VpXHLkUD9q

If I use Chrome / Firefox / IE and encrypt with घोडा स्टेपल, I get
6PRNXA7M4qEppBJCHM2SEizfna7XTomzXwdCBrEG6Mjo3nU6iziS6vWWXA

I'm not sure if this is my own bug, or something native to the BIP38 implementation I'm borrowing from bitaddress.org. Hard to test because I don't think bitaddress.org will let me BIP38 encrypt my own vanity address or brain wallet.

But here's something you can replicate / experience. I've been using bitaddress.org to check the validity of my BIP38 wallets. What I do is I fire up bitaddress.org, open up the "wallet details" tab, and use this to decrypt my "6P..." keys. And I'm getting browser-dependent results:

*** Using the 'wallet details' tab on bitaddress.org to decrypt the BIP38 key, if using safari, the safari-generated key (_9q) works, and the other-browsers key (_XA) fails to decrypt. If using chrome/FF/IE, the reverse is true. ***

I'm experiencing the same browser-dependent decrypting success/failure using the "decrypt private key" function at bit2factor.org.

Any ideas on why bitaddress.org is unable to decrypt my Nepali-encoded private keys unless I'm using the same browser I used to generate those keys?

@cantonbecker
Copy link
Author

PS I realize it's silly (maybe even obnoxious) for me to be doing tests between two completely different projects -- my project (bitcoinpaperwallet.com) and bitaddress.org. As such I did some playing around and succeeded in generating a wallet using bitaddress.org that can only be recovered if you use the same browser to decrypt it.

Here's what I did:

  1. Using bitaddress.org and OS X Safari I set my BIP38 passphrase to "घोडा स्टेपल" (without the quotes) which is the google translates response for translating "horse staple" to Nepali.

  2. Generated one BIP38 wallet. See http://cl.ly/image/0m0S1N1p2Y3P

  3. Took the resulting private key 6PfUfMYT9PZpJ5q5NCosyqW3pMxwpu24G6mqCVbS41ND7aWctqeqBqz7NJ and fed it into the "Wallet Details" function at bitaddress.org along with the "घोडा स्टेपल" Nepali passphrase.

Result: Decrypts fine. http://cl.ly/image/3p2X332x370X

  1. Opened up firefox. Opened up Wallet Details and tried to decrypt the same key with the same password.

Result: "Incorrect passphrase for this encrypted private key".

I haven't confirmed this myself yet, but mannkind (bit2factor.org) was unable to successfully decode this wallet at bitaddress.org using his own Safari. (Furthermore, his own bit2factor.org service appears to have some browser-dependent decryption ability.) If true, this means you may even need the same version of the browser, which would be pretty alarming as it significantly reduces the ease of retrieval.

What's worrisome about this possible issue is that someone generating a BIP38 wallet might very reasonably test their ability to decrypt the wallet before loading the wallet with bitcoins. But they'd almost certainly be using the exact same browser, which would circumvent this possible issue.

NB: I have only been testing the matrix of OS X Safari 6.x, OS X Chrome, OS X Firefox, Windows IE9, and Ubuntu Firefox. I haven't done any more extensive tests with Windows or especially Opera. If someone is feeling ambitious, a UTF passphrase test suite should probably be run testing encryption & decryption round trips using a god-awful extended charset passphrase like "घोडा स्टेपलʏpsilɔn⡍⠜⠇⠑⠹ŠŸžΚαλημέンニチอขัน".

At the moment I'm thinking the problem here is something to do with the BIP38 code where depending on the browser you're using (and with Safari in particular) some extended charsets are interpreted differently -- whether encrypting or decrypting -- which is why you are obligated to use the same browser to decrypt as you used to encrypt.

@cantonbecker
Copy link
Author

NB: Back on the bitcoinpaperwallet.com, artiomchi did some testing with various versions of safari and may have narrowed things down significantly. It might be that BIP38 is encrypted (and decrypted) differently specifically when using Safari 6 as compared with 5 or 7. See:

cantonbecker#6 (comment)

@pointbiz
Copy link
Owner

pointbiz commented Jan 9, 2014

In Safari 6 do all the async unit tests pass?
Query string:
?asyncunittests=true

@cantonbecker
Copy link
Author

Unfortunately, at least using safari 6.05, no.

See: http://cl.ly/image/2L0P1y1p0U3r

running 8 tests named testDecryptBip38
running 4 tests named testBip38Encrypt
running 2 tests named cycleBip38
running 5 tests named testBip38Intermediate
pass testDecryptBip38 #0
pass testDecryptBip38 #1
pass testDecryptBip38 #2
pass testDecryptBip38 #3
pass testDecryptBip38 #4
fail testDecryptBip38 #5, error: Incorrect passphrase for this encrypted private key.
pass testDecryptBip38 #6
fail testDecryptBip38 #7, error: Incorrect passphrase for this encrypted private key.
pass testBip38Encrypt #0
pass testBip38Encrypt #1
pass testBip38Encrypt #2
pass testBip38Encrypt #3
pass cycleBip38 test #0
pass cycleBip38 test #1
pass testBip38Intermediate #0
pass testBip38Intermediate #1
fail testBip38Intermediate #2, error: Incorrect passphrase for this encrypted private key.
fail testBip38Intermediate #3, error: Incorrect passphrase for this encrypted private key.
fail testBip38Intermediate #4, error: Incorrect passphrase for this encrypted private key.
running of asynchronous unit tests complete!

@pointbiz
Copy link
Owner

pointbiz commented Jan 9, 2014

Well that is somewhat good news. Better that the unit test also fails. Are the failures intermittent?

Unfortunately, those unit tests take so long to run that we can't use them to detect there is a problem for average users.

@cantonbecker
Copy link
Author

PS: I should mention 3 things we've figured out recently:

  1. Has nothing to do with UTF8 / extended charset. Problem occurs with very ordinary passphrases like "my passphrase".

  2. May not occur on Safari 5, 7, or even 6.1. Might be limited to 6.05 and possibly 6.0 (the typical OS 10.7.x browser) which I estimate makes up around 2.5% of paper wallet users.

  3. It's intermittent. Notice the results from running the asyncunittests a second time fail in different spots:

running 8 tests named testDecryptBip38
running 4 tests named testBip38Encrypt
running 2 tests named cycleBip38
running 5 tests named testBip38Intermediate
pass testDecryptBip38 #0
pass testDecryptBip38 #1
fail testDecryptBip38 #2, error: Incorrect passphrase for this encrypted private key.
pass testDecryptBip38 #3
pass testDecryptBip38 #4
pass testDecryptBip38 #5
pass testDecryptBip38 #6
pass testDecryptBip38 #7
pass testBip38Encrypt #0
pass testBip38Encrypt #1
fail testBip38Encrypt #2
expected 6PYNKZ1EAgYgmQfmNVamxyXVWHzK5s6DGhwP4J5o44cvXdoY7sRzhtpUeo
received 6PYNKZ1EBJQFCNyr5QsjAa7GtL3e5Z4tZbbtoA3M26Sp6Ljmot8FAwofwG
pass testBip38Encrypt #3
pass cycleBip38 test #0
pass cycleBip38 test #1
fail testBip38Intermediate #0, error: Incorrect passphrase for this encrypted private key.
pass testBip38Intermediate #1
fail testBip38Intermediate #2, error: Incorrect passphrase for this encrypted private key.
pass testBip38Intermediate #3
pass testBip38Intermediate #4
running of asynchronous unit tests complete!

@artiomchi
Copy link

After spending a bunch of time (way more than I should have today), I have traced the issue to Crypto_Scrypt library so far. I'm not sure if it's this library, or something that it depends on.

Here's a simple example to demonstrate the issue:
http://jsutftest.azurewebsites.net/testCrypto.html
( page blocking, wait for it to process first )

@artiomchi
Copy link

Ok, got some further progress on it. I've narrowed it down to the scrypt library, and managed to replicate it with minimal code.

Since no other library is required to replicate the test, I've opened an issue in the scrypt-js project, where we can follow-up, and hopefully find a fix.

cheongwy/node-scrypt-js#2

@scintill
Copy link
Contributor

scintill commented Jan 9, 2014

I am also seeing something nondeterministic. Starting with @artiomchi's minimal testcase, I am dumping some intermediate state and comparing between Chrome and Safari 6.0.5. I have just observed several times in a row, that the first run of the script in a new window gives incorrect intermediate state, while refreshing gives correct answers (or at least the bytes I am looking at are correct.) I was closing windows because sometimes the long calculations seem to have hung... not sure if just impatient or what. Refreshing does not make @artiomchi's unaltered testcase come out correctly.

There shouldn't be any way for JS code to be causing this, right? Even if you're using globals, they should be refreshed with the page? I can't think of any standard caching effects that should be involved here. If I'm correct, there is something wrong with Safari 6.0.5 (and others?) and I'm not sure we can work around it.

I don't know if the maintainer of cheongwy/node-scrypt-js will help, because the project is built for Node where presumably this issue does not arise. I'm the one who repackaged that scrypt code and added web workers, for bitaddress.org.

@artiomchi
Copy link

That's indeed the case, @scintill. Basically, I think Safari (not sure if it's limited to v6 or not) has some strange "optimisation" for javascript, which is causing a bunch of issues. For example: A bunch of times I added some console.log statements to log states, and figure out what was going wrong - and the values in the console were different to the values that the variables actually had in code.

For example:

B = [large array];
console.log(B);
B = [values changed in array];

The console output would actually sometimes show the new state of the array when I triggered that in Safari 6. Took me a while to figure this out =/

Anyway, regarding the minimal testcase, you're absolutely right to note that if you re-run the test case it will give correct answers. But if you refresh it - it will give incorrect results.

Try something else. Open the page, open the developer console, and then try refreshing the page. If I understood this correctly, having the developer console open slows down the page just enough to avoid whatever issue we are having, and gives the correct results during the first go. At least it did that to me half the time. The other half the time the browser hung in the VM (it really didn't like the dev console) :)

Could you check that out please, @scintill, and report back?

I understand that the original project may not be updated, but I created the issue there, since that's where the bug is, and so far the problem is tracked among 4 different projects in GitHub, I thought it would be better to point it to the origin.

Great job repackaging the code for the web workers, by the way! Very well done! :)

@pointbiz
Copy link
Owner

I closed #48 as a duplicate and updated the subject of this issue to note it is a Safari 6.x only issue

@scintill
Copy link
Contributor

Aha, I believe we are experiencing the bug reported here, a WebKit JIT bug present in several versions and flavors of Safari.

This explains the symptoms, such as it running much faster when it gives the wrong answer (JIT speeds up execution but does it incorrectly); and working, but slowly, when the dev console is enabled (the linked WebKit bug report says JIT is disabled when the console is open.)

This is quite alarming and frustrating that such a severe bug made it into relatively wide deployment, ugh. Even if we appear to work around it, I am a bit reluctant to let people risk money in a broken JS engine. On the other hand, I'm not sure we can even reliably detect every browser that is affected.

I've had about 6 successful runs, with devconsole closed, with a modified testcase here. It's not a thorough test, but I think it's a good indicator. I am disabling JIT by wrapping every function body in try/catch as advised at StackOverflow. Unfortunately this is much slower, even on Chrome (multiple "This script appears to be hung" popups.) I suspect speed could be improved by de-JIT'ing fewer functions and possibly rewriting things to work around the JIT bugs. But again, I am reluctant to be conservative at all given what's at stake.

Let's see if I do this right. Cc other bugs: cantonbecker#6, mannkind/bit2factor.org#2, cheongwy/node-scrypt-js#2.

@artiomchi
Copy link

Great job pointing it out!

By the way, it's enough putting try/catch blocks only around the salsa20_8() method.. It's still considerably slower (10sec vs 60sec on my machine), but it ends up working on Safari 6 as well...

In fact. After trying out different things, I realised that if I comment out the following line in salsa20_8(), the results will be the same in Chrome and Safari 6

B32[i] |= (B[i * 4 + 3] & 0xff) << 24;

Anyway, working on after that, I believe I managed to get my testcase working by calling salsa20_8() ahead of time with a dummy array. Since only the first execution yielded bad results, it seems like this call may have gotten it working.. Could you guys test it on your side, since my VM is incredibly slow, and it would be good to have multiple test inputs...

I've updated my gist, and the hosted version above (http://testcrypto.azurewebsites.net/) with the potential "fix"

@cantonbecker
Copy link
Author

Hi Joey,

"I am disabling JIT by wrapping every function body in try/catch as advised at StackOverflow."

Nice research you did, and this sounds like a fine work-around. I just wanted to write a reminder to everyone impacted that before implementing any kind of a fix on a site like bitaddress.org, there should be some plan on how to support wallets already made using the broken Safari.

Right now those users are still able to decrypt their BIP38 wallets by using the same version of Safari as they used to create it. If this bug is worked-around, then those same users would not only have to use the correct version of safari, but also hunt down a pre-2014 release of bitaddress.org to do the decrypting. Ideally, a 2014 release of bitaddress should have some facility for attempting to decrypt using the old bug-exposing code as well as the new bug-work-arounding code.

@artiomchi
Copy link

One disappointing thing to note is that it's not a bug in the javascript code, but instead a bug in the browser's execution of the code. That is to say - the browser is executing the code incorrectly in some way (I think the execution order of some statements bay be skewed).

As such, the only way to get this to work is to find which order did Safari actually execute the statements, and create a "dummy" method that replicates that behaviour.. That would require a whole bunch of changing/breaking the code, executing it on a proper browser like Chrome, and hope you get the "correct" invalid result.. Eek!

Although if I'm right, and the issue is indeed with salsa20_8() that narrows things down a lot!

@scintill
Copy link
Contributor

Artiom, good work on narrowing that down! I verified your finding, that a dummy call to salsa20_8() beforehand makes the whole scrypt work (or at least the first 50 bytes we are comparing.) I also made some small modifications, to only do the dummy call once, and to re-try the calculation without refreshing the page -- these were successful.

I also verified that taking out the << 24 line seems to make consistent (but incorrect) results between Chrome and Safari, without the Safari dummy call. Good work on finding that too. I've tried tweaking the surrounding lines to see if I could learn anything else, but little luck yet. I did find that "unrolling" that 0-16 "B32" loop (writing out B32[0] = (B[0]...) caused a different incorrect result in Safari, but I am not sure if that helps.

I am a little hesitant to say this dummy call is a "fix" since we don't fully understand the issue, but it's a great start, seems to be working, and may end up being good enough.

I would also like to replicate the broken Safari calculation in other browsers, but it seems like the only options are randomly twisting the code until Chrome gives the same "broken" result, or trying to reverse-engineer the JIT to find out what the code it creates is really doing. Neither is going to be quick or pleasant...

I am wondering if parts of the smix -> blockmix_salsa8 -> salsa20_8 call chain is being inlined since blockmix_salsa8 is called in long loops in smix (N = 16384, twice.) Maybe calling salsa20_8 somewhere else causes the JIT to make a different decision here. I don't know if this helps us, though.

@cantonbecker
Copy link
Author

Just wondering if this issue is still being tracked for bitaddress.org? At the very least it might be worth having an alert pop up for Safari 6.05 users saying "don't make BIP32 wallets using this browser!"

Meanwhile, I'm rolling out BIP32 for bitcoinpaperwallet.com in the next few days with the work-around implemented by artiomchi. It seems to make Safari 6.05 reliable for proper address encoding/decoding.

@pointbiz pointbiz self-assigned this May 1, 2014
FuzzyBearBTC added a commit to peercoin/bitaddress.ppc that referenced this issue Mar 12, 2015
Fix donation link (HTTPS not working) + HTTPS for ppc.blockr.io
@abkaya
Copy link

abkaya commented Jul 26, 2015

This may be too late to reopen the subject, but I have this particular issue. I saved a couple bitcoins over 4 wallets generated and BIP-38 encrypted in safari 6 at the time.

Apple doesn't offer a way to revert to safari 6.05. Would you have any suggestions for me? It would be saddening to see it lost, because of this.

Thank you for the work you guys put into it nonetheless. At least now, I've got an idea as to why the passphrase was 'wrong'.

@pointbiz
Copy link
Owner

@abkaya I would not consider your bitcoins lost. I need to review the issue but I believe Safari 6 was behaving incorrectly in a deterministic way. Meaning myself or someone on this thread like @scintill or @cantonbecker or @artiomchi could create a branch that mimics the Safari 6 behavior. So users like yourself could decrypt their keys.

So keep watching this issue.

@abkaya
Copy link

abkaya commented Jul 26, 2015

@pointbiz Thank you very much, I will definitely keep an eye out.

@artiomchi
Copy link

The problem is - I'm not really sure where Safari messed up right there - it's definitely an issue, but replicating it on any other browser is a whole different project.

@abkaya - in your case, it's not all lost, actually. You should be able to use BrowserStack to create a vm with this version of Safari (that's how I was able to test it), to get the private key decrypted (although I'd suggest moving them to a new wallet straight away after that)

Good luck!

@artiomchi
Copy link

I should probably note that Safari 6.05 was indeed behaving incorrectly (hence you can't decode your wallet now), but in a consistent manner. So to decode your wallet, you'd just need to replicate the same environment. Last time I was working on this issue (looking at the comments) we seem to have found a "workaround" to make it work in the Safari browser as well. I'm not sure if this is live now, so if the latest copy of the website can't decode it, you can just go back in history, and use an older version of bitaddress.org/bitcoinpaperwallet.com that didn't have the patch implemented..

If that doesn't work, ping us a message, and I'm sure we'll find a solution one way or another :)

@scintill
Copy link
Contributor

Yes, I also think it's deterministic, the only redeeming factor in this. I would second the BrowserStack suggestion as I used it for testing back then too, but I don't see Safari 6.0.5 on their supported browser list anymore. :( I can't remember if we had any data on what Safari 6.0 did, but it's listed under Mac OS X Lion (64-bit) so we might try that.

You might try finding a download of that specific version on some site like this. I would be extremely cautious with this, though! Verify a code signature from Apple if possible (this might help), and install on a fresh disposable machine (maybe a virtual machine) to limit the damage from any malware. This may be above @abkaya's skills to do securely -- if so, I'd be willing to test it so we can see if it's worth pursuing further.

@abkaya, similar to how artiomchi mentioned, I would recommend trying to reproduce the original testcase (first post) first, rather than repeatedly trying your real keys and passphrase into BrowserStack or a questionable download.

I have a few old notes about trying to make a corresponding WebKit build, which might have the same bug -- unfortunately I don't have notes or memory about whether I had success. I'll look into it again. This could help developers dissect and reproduce the bug in straight JS, or be a more accessible way for affected users to decrypt their key.

If we don't write bug emulation in JavaScript, it would be nice to at least document how to build a system that can reproduce the bug, ideally in a way that will work 5+ years from now.

@cantonbecker
Copy link
Author

Hi there,

We figured out a way to make Safari 6.05 behave as it ought to, but we don’t have a way to make newer versions of Safari produce the wonky behavior. Your best option is just to install an old version the Mac OS on a computer — or even just as a virtual PC using VM Ware. Your coins are definitely not lost — it’s just a matter of finding an older mac OS to run the decryption that once.

  • Canton

On Jul 26, 2015, at 2:15 PM, abkaya notifications@github.com wrote:

This may be too late to reopen the subject, but I have this particular issue. I saved a couple bitcoins over 4 wallets generated and BIP-38 encrypted in safari 6 at the time.

Apple doesn't offer a way to revert to safari 6.05. Would you have any suggestions for me? It would be saddening to see it lost, because of this.

Thank you for the work you guys put into it nonetheless. At least now, I've got an idea as to why the passphrase was 'wrong'.


Reply to this email directly or view it on GitHub.


Canton Becker
canton@gmail.com • (505) 570-0635 • http://cantonbecker.com

@abkaya
Copy link

abkaya commented Aug 2, 2015

Thank you all for your responses and help. I currently haven't had success with BrowserStack, but I will try to reproduce the original testcase.

The error I get is: "The text you entered is not a private key", that is after I enter the password to decrypt BIP38

@scintill
Copy link
Contributor

scintill commented Oct 4, 2015

I've been working on this today, and I believe it's reproducing on a fresh install of OS X 10.7.5 (11G63) on a "MacBook4,1" model with this WebKit nightly build (the SHA256 hash of what I have is bd4751d6287e280cbf7cce6d4e9c3cc2d542b251b565b961ba574142bb54db82). @abkaya, if you can, you might try setting that up and attempting decrypting with it.

I'm not certain it's the exact same issue, because the testcases given here don't seem to work/not-work as described, on the current bitaddress.org. But I am seeing differences in my own minimal testcases (turned the scrypt parameters down to something that computes fast but often incorrect in Safari.) It does not seem entirely deterministic, either -- I am seeing 2-4 possible wrong results, plus the correct result, so maybe that has something to do with my not being able to use others' testcases. (I never open the JS console, as we've discovered that changes the behavior of the JS VM.) I'm afraid it could also be that the bitaddress.org code has changed enough to make the buggy JS engine compute differently than it did back then. So, @abkaya, be thinking of exactly which version/date you used. If you have the public addresses, looking at the blockchain timestamps may help.

Thanks to artiomchi's work, I'm comparing the execution of salsa20_8() between Chrome and the above WebKit nightly. I'm seeing patterns of certain computations being slightly off (values in B32; they differ by less than 256, which is probably a clue since 4 bytes are being assembled there), and it seems to cascade from there due to the bit rotations etc. I'm still trying to determine what exactly the pattern is, and hoping I can emulate it for the full BIP38 scrypt in other browsers. Here is an example of what I'm looking at, although you'd have to see the test code to really know what's going on there. I can share it if anyone's interested. I believe the first difference is due to the JIT bug, and the following are the cascading effects. It seems to usually be B32[0] that is wrong, although not every computation of B32[0] is wrong.

There's also a corresponding Webkit nightly source tarball that I may try building to dig deeper into what exactly the VM is doing.

@Dal78
Copy link

Dal78 commented Jul 11, 2017

Hmm ok thanks i'll work on that. The webkit doesnt run on my version so im guessing its a 32-bit 64-bit issues. Will look into it later. Thx for the help guys.

@artiomchi
Copy link

Please update us on whether you find a version where you can reproduce the issue. It would be good to have a known version that could be used to replicate the issue. Furthermore - if it's an issue in a WebKit nightly, someone better than me could check the commit history, find what was the bug (since it's been patched since then), and we could even replicate it in software for bitaddress!

@cantonbecker
Copy link
Author

cantonbecker commented Jul 11, 2017 via email

@Dal78
Copy link

Dal78 commented Dec 11, 2017

Ok i gave up on this a while back but im back now.

I could get odd behaviour out of the browser and was hopeful but i couldn't get my key to decrypt so i wondered if it wasnt this bug all along and went away depressed. Today i realised i had a few litecoin and when i tried to decrypt these (generated at the same time) they failed too. The likelyhood that i bungled the password in such an important moment twice is pritty small so im back to thinking this bug played its part.

I see on another thread that the bug only occurs on first initialisation? Does this mean for each decryption attempt i will need to close safari and re open the site or are further / less steps needed? This might have been the bit of info i didnt spot and so on repeated attempts it didn't decrypt because it only bugs on the first pass.

@Dal78
Copy link

Dal78 commented Dec 12, 2017

Ok i got into my litecoins, wasnt the bug for the litecoin at least.

I wonder, does anyone have a bugged system available to them? I have a wallet with no currency in it encrypted at the same time. We could test on this wallet.

@lathom
Copy link

lathom commented Dec 12, 2017

Hello, I've been following this thread ever since I learned of the bug that may have caused a dreadful amount BTC to be lost in wallets generated with bitaddress.org in the first week of 2014.

In hopes of resurrecting what I thought was lost, I bought an old iMac and installed Mountain Lion which luckily had Safari 6.05 and I was able to reproduce the bug on a test wallet. It would decrypt on safari 6.05 but not on any other browser.

Thinking I had found the answer to my prayers I tried to remember the mnemonic triggers that I had used when writing down a very complicated passphrase, but after nearly four years of second guessing and trying to remember it's all so jumbled up I can't seem to get the right one.

Considering the amount BTC at stake (too embarassed to even say how many), I couldn't just let it go, so I researched ways to automate Safari with Selenium. I have a Python script that uses SafariDriver to check 1 passphrase every 50 seconds and after over 10,000 bad guesses I'm still hoping someday there will be a miraculous success.

I resurrected this old github account to reply to @Dal78 about helping him out and maybe getting some help for myself too. I have a bugged system that I'd be happy to use for testing the empty wallet and also curious about the comment referring to bug occurring only on the first initialisation. If that is true my selenium script will need to be modified and hoping to get more information about that.

Thanks in advance to anyone that might be able to help!

@cantonbecker
Copy link
Author

cantonbecker commented Dec 12, 2017 via email

@Dal78
Copy link

Dal78 commented Dec 13, 2017

Hi @lathom ,

I have managed to get another mountain lion VM setup, using VMware player 12. Running Mountain lion which had version 6.0 safari and upgrading to 6.0.5.
I can get safari to generate bugged addresses which chrome will not decrypt. Only safari will decrypt it. On the first try, after the first try it also cannot decrypt the bugged address. So to decrypt i have to re-open safari each time and then it regularly does so.

You can try this yourself before you change any scripts tho.

Unfortunately this didn't save my key so i think i need to move on from this bug.

The idea of Dave is nice but 20% stings. I dont really want to cash in my bitcoins yet anyway so i might sit on this and maybe a nice open source BIP cracker will come out as this problem will only get bigger as people experience the same issues.

@lathom
Copy link

lathom commented Dec 14, 2017

Thanks @cantonbecker for the words of encouragement and suggestion. While I'd gladly take 80% over 0%, the challenge is enjoyable for now, but wallet recovery services is a great option I will keep in mind!

Thanks as well to @Dal78 for the update on the first decryption attempt behavior. I will try that now and see if it makes a difference. Glad you got your litecoin back and best of luck with the rest!

@rouslanovitch
Copy link

rouslanovitch commented Dec 20, 2017

@Dal78 did you succeed to decrypt your bugged Private Key? And on which version of bitaddress did you tested it?

@Dal78
Copy link

Dal78 commented Dec 20, 2017 via email

@rouslanovitch
Copy link

And on which version of bitaddress did you tested it? online / offline ? release number?

@rouslanovitch
Copy link

@Dal78 @lathom @cantonbecker this is the code which created wrong BIP38 encryption https://gist.github.com/rouslanovitch/3f85357bada792e662868f26c46a8927

@Dal78
Copy link

Dal78 commented Dec 20, 2017

@rouslanovitch
Online, inside my VM OSX 10.8, version 2.7.2

I can create broken keys, not decrypt them in the clean browser, decrypt them on the same broken browser on first try, not subsequent trys until re-opening the browser.

However on attempted to decrypt my broken ( or lost ) key i still get rejected. I have tried multiple times and therefore assumed that my keys is infact not afflicted with the bug and im just an idiot who typoed their password.

I still have the VM and environment, i'll keep it in case something comes up.

@rouslanovitch
Copy link

Thanks @Dal78 which version of Safari is it? I am creating up my OSX Lion stack right now in Parallels

@Dal78
Copy link

Dal78 commented Dec 20, 2017

@rouslanovitch Safari 6.0.5 (7536.30.1)

@rouslanovitch
Copy link

@Dal78 where could I download this Safari version?

@Dal78
Copy link

Dal78 commented Dec 20, 2017

@rouslanovitch
Copy link

Ok just reproduced the issue on my side:

http://jsutftest.azurewebsites.net/testCrypto.html ==> Fails the two lines are not similar

The code generates BIP38 encrypted keys which decryption fails on current version of Chrome/Safari in High Sierra but decryption works on Safari 6.0.5 / OSX Lion.

Legacy Encrypted BIP38 PKey not working with the "historical" passphrase.

Having other wallets on my side from the same batch using the same password this will validate if the issue is coming from wrong password or crypto version issue.

@cantonbecker
Copy link
Author

cantonbecker commented Dec 21, 2017 via email

@Dal78
Copy link

Dal78 commented Dec 22, 2017 via email

@rouslanovitch
Copy link

@cantonbecker
I checked on Safari 6.0.5 the current bitcoinpaperwallet offline generator and it generates a private BIP38 encoded key which cannot be decrypted on any version of Safari current & 6.0.5

Hope this helps

@zgbee
Copy link

zgbee commented Dec 26, 2017

Hey All - recently ran across this thread after having given up on a few wallets I gave out for the holidays back in 2013, and have renewed hope I'll be able to decrypt them, but haven't had any luck yet. (pretty sure I created BIP38 wallets on bitaddress.org using Safari, thinking it'd be more secure than using my 'primary' browser which was Chrome)

I've so far tried two of the encrypted private keys I distributed, and both have so far failed to decrypt in a VM (Parallels Lite, which is free from Mac App Store if you're not using it to run Windows).

Try 1: OSX 10.7, immediately upgraded to Safari 6.0.5 (7536.30.1)

In addition to attempting to decrypt one wallet, I ran the link provided by @artiomchi (http://jsutftest.azurewebsites.net/testCrypto.html) with the following results (note that the results were the same every time I refreshed the page - I was unable to find a difference on the first run after re-opening the browser):

Expected: 80,84,90,200,243,142,148,51,18,52,58,127,144,147,42,40,172,31,5,249,4,211,149,232,100,179,13,200,182,100,49,54,146,202,93,103,54,30,244,238,119,50,209,252,46,75,99,180,13,105

Actual: 31,122,37,156,131,63,37,145,75,239,33,116,191,180,213,94,254,146,47,164,51,146,46,20,77,146,141,65,210,169,136,215,251,233,177,113,136,38,185,243,128,8,202,56,242,39,110,80,106,255

Try 2: I downloaded/installed the 10.8 upgrade from the App Store (was in my purchase history), which installed Safari 6.0.5 (8536.30.1) by default.

  • had the exact same results as above, and also failed to decrypt an additional key that was originally generated at the same time as first key I tried

@scintill Do you still happen to have a copy of that WebKit nightly build? It doesn't seem to be online anymore

edit: just dug up some old backups, and had Safari 6.0.5 (8536.30.1) installed at the time

second edit: i got the wallet back! but it doesn't seem to have been related to the Safari bug, oddly enough. I found a clue in the backup - I had been very slightly mis-remembering the key. And now that I have the correct key, it also decrypts in a current version of Chrome. @Dal78 if you'd like me to try anything in the VM environment anyway, let me know

@rouslanovitch
Copy link

@zgbee what do you mean by "I had been very slightly mis-remembering the key" ?

  • your encrypted key was containing wrong letters?
  • you mis-typed your BIP38 decryption password?

Thanks!

@zgbee
Copy link

zgbee commented Jan 6, 2018

@rouslanovitch I was missing a single character from the end of the password I had used to encrypt the wallets.

@DavidVeksler
Copy link

image

FYI, I wrote a script that wraps the bitaddress.org libraries to run brute force in the web browser:

https://github.com/DavidVeksler/bitaddress.org

@brianshould
Copy link

I've been looking at this old bug for a while. Sure, there's a workaround, but reverting to Safari 6.0.5 to reclaim you bitcoin is a hassle. And there's another reason for this post: maybe some of you have experienced that remembering an old passphrase is hard and there might be a few variations you need to try. @DavidVeksler kindly offers the solution to this, by providing automated brute-forcing for bitaddress. However, I suspect that this may not work correctly for the bugged version of bitaddress (see 'Caching' below).

Please note that my work may depend on the hardware and/or software that I used, so results may be different for other setups. If you have a setup that produces different results, the procedure below may help you to track down the bug in your case. If you do, please share your results. I did my research using VMWare Fusion Pro (version 11.5.1). I ran this on a 2016 MacBook Pro with macOS High Sierra (macOS 10.13.6, 16 GB memory). The VMWare client OS runs Mac OS X 10.7.2 (2 GB memory) with Safari 6.0.5. I used Bitaddress version 2.9.3 because that is the version I was interested in. I have looked at the differences between version 2.9.3 and newer versions of Bitaddress, and expect they do not change the way the bug works.

If anything appears to be incorrect in my post, please reply so we can all learn.

My approach to this bug was pretty straightforward. Bitaddress produces incorrect results on Safari 6.0.5. Let's run two copies of Bitaddress, one on Safari 6.0.5 (buggy), and one on a recent version of Firefox (correct). Then add logging to track down where the buggy version goes wrong. First, I created a few test vectors and I saw that the versions indeed produce different results and that the buggy version produces incorrect ones as expected. Second, I added logging. And then the bug disappeared.

So what happened? As @artiomchi noticed, an extra call to salsa20_8() removes the bug. So does adding logging. Why? Some Googling taught me that the bug is caused by an error in the Javascript optimisation in Safari. So it makes sense that significant changes to the code alter the way in which it gets optimised. Luckily, I managed to work around this quirk by adding minimal logging only. Another issue that @artiomchi noticed is that if you log an array to console, it may give you the final state of the array, rather than the intermediate value when you logged it. I worked around that by using .toString(). In most cases I also used .slice(a, b) to only log part of the array, because the arrays used by Bitaddress can be pretty large. So my logging statements often looked like: console.log(" salt: " + salt.slice(0,32).toString());

Caching

Just when I thought I had fixed the issue of the bug disappearing, it disappeared again. This time, the issue was most probably caused by browser caching. When a piece of Javascript gets optimised by the browser, the browser usually caches the optimised Javascript, just in case it gets called again. So when I was testing multiple test vectors, only the first test vector would be processed in the 'correct buggy way'. For my setup, all subsequent test vectors would be processed in a non-buggy way. So for those of you who created a wallet with bitaddress in Safari 6.0.5, the result may be correct if you started with a test run before you created your actual wallet. Due to browser caching, @DavidVeksler 's brute-forcing version of Bitaddress will probably not work for brute-forcing buggy wallets. For my tests I needed a buggy Safari, so I closed Safari after each run, and used a different html file with a different name for each run.

Tracking down the bug

With these hurdles out of the way, it was time to track down where the bug occurs exactly. This pointed me towards the function scrypt. This function makes 8 calls to scryptCore, and these calls are made in parallel if possible. In my VM setup, the guest OS only had 1 processor core available, so the calls were always made successively. With a few logging statements I found out that the bug appears in the first call to scryptCore. So I went one level deeper.

scryptCore makes multiple calls to ROMix. ROMix is called smix in Bitaddress; I chose to follow the function names used in the Wikipedia description of Scrypt (https://en.wikipedia.org/wiki/Scrypt). Again the bug already appears in the first call. So I went one level deeper again. ROMix makes multiple calls to BlockMix (called blockmix_salsa8 in Bitaddress). To be precise, ROMix has 16384 iterations in each of which it calls BlockMix twice. In this case, the outputs of the buggy and non-buggy version were identical after the first iteration. So I performed a binary search, comparing the results after 8192, 4096, 2048, etc. iterations, to find that the bug first occurs on iteration 10 counting from 0, so the 11-th time it gets called.

BlockMix makes multiple calls to Salsa20/8 (called salsa20_8 in Bitaddress). Another binary search showed that the bug occurs on iteration 6 counting from 0, so the 7-th time it gets called. Before I reached this conclusion, I went through multiple detours exploring other functions like blockxor and arraycopy, to find that they were not causing the bug.

Salsa20/8 performs lots of operations on integer arrays, and it makes a call to arraycopy. arraycopy was not causing the bug, so finally I had arrived at the level where the bug actually happens. At this point I needed to be careful to not produce too much logging, because this would remove the bug again. I worked around this problem by producing logging only for the iteration I was interested in. After looking at different parts of logged arrays and comparing them between the correct and the buggy version, I finaly spotted the bug! The bug didn't actually happen in Salsa20/8, but in BlockMix. For some reason, it sets the first byte of X to 0 after calling Salsa20/8. So effectively, the buggy version works as normal, except that it may be thought to contain an extra statement:

X[0] = 0;

I wasn't done yet. The bug appeared at ROMix iteration 10, BlockMix iteration 6, but was only persistent for a while. After some more binary searches, I found that it disappeared after ROMix iteration 15, BlockMix iteration 7. And now I was done, at least in a practical sense. I still wanted to know why the bug appears after some number of iterations, then disappears again some number of iterations later.

Javascript optimisation

Some more Googling provided a plausible explanation for this. Javascript engines have multiple levels of optimisation. When the script starts, code is executed 'as is'. But the engine keeps track of functions that get called a lot so that it can optimise these. The engine has multiple levels of optimisation, so if a function gets called even more often, it gets optimised a second time, and possibly even a third time. So what we are seeing here is a bug that occurs in either the first or the second optimisation layer. The first part works fine, as does the last part. Somewhere in the middle we have a buggy Javascript optimisation.

Another observation is that Bitaddress works in a non-buggy way if the passphrase is longer than - I think - 16 characters. It makes sense that the code behaves in a slightly different way for a long passphrase: the passphrase enters into PBKDF2, where it acts as an HMAC key. If the HMAC key is longer than the block size of the underlying hash function, an additional call to the hash function is needed to compress it. I believe SHA-256 is used as a hash function, which has a block size of 32 bytes. So this makes sense if the passphrase would be encoded with 2 bytes per character, which I'm not sure is the case.

Multithreading

As I mentioned, my VM's guest OS only had a single core at its disposal, causing scrypt to run 8 successive iterations of scryptCore. One wonders what happens if the OS has multiple cores. It is unclear how this will affect the tallies that tell the Javascript engine when to optimise a function. Behaviour may become non-deterministic as we now have multiple threads with race conditions. So I gave my VM's guest OS two cores, and guess what? The bug disappeared. At this point I'm not sure my VM correctly simulates the way Bitaddress would run on an old MacBook with two cores. If anyone has an old MacBook with Safari 6.0.5 lying around, I would be interested how it handles Bitaddress.

Attachments

I have attached a 'buggy' version of Bitaddress 2.9.3. This should reproduce the bug explained above when run in a normal browser. I expect this may easily be transferred to other versions of Bitaddress by applying the same diffs as I did to the code. This should also work with @DavidVeksler 's brute-forcing version of Bitaddress. The result should be a brute-forcing 'buggy' version of Bitaddress that runs in any browser.

bitaddress.org_buggy.zip

I have also attached a few test vector for the buggy version of Bitaddress.

test_vectors_buggy.txt

Questions and issues

I'm happy to help others who cannot access their BIP-38 encrypted wallet. That is exactly my reason for this post. However, this analysis has taken me quite some time as I'm not a very experienced programmer. So I'm not going to make a buggy branch for other versions of Bitaddress, nor am I building a buggy version of @DavidVeksler 's brute-forcing Bitaddress. Nonetheless, I'm available for questions, so do not hesitate to raise any questions or issues if you run into them.

@cantonbecker
Copy link
Author

cantonbecker commented Oct 28, 2020 via email

@rouslanovitch
Copy link

I've been looking at this old bug for a while. Sure, there's a workaround, but reverting to Safari 6.0.5 to reclaim you bitcoin is a hassle. And there's another reason for this post: maybe some of you have experienced that remembering an old passphrase is hard and there might be a few variations you need to try. @DavidVeksler kindly offers the solution to this, by providing automated brute-forcing for bitaddress. However, I suspect that this may not work correctly for the bugged version of bitaddress (see 'Caching' below).

Please note that my work may depend on the hardware and/or software that I used, so results may be different for other setups. If you have a setup that produces different results, the procedure below may help you to track down the bug in your case. If you do, please share your results. I did my research using VMWare Fusion Pro (version 11.5.1). I ran this on a 2016 MacBook Pro with macOS High Sierra (macOS 10.13.6, 16 GB memory). The VMWare client OS runs Mac OS X 10.7.2 (2 GB memory) with Safari 6.0.5. I used Bitaddress version 2.9.3 because that is the version I was interested in. I have looked at the differences between version 2.9.3 and newer versions of Bitaddress, and expect they do not change the way the bug works.

If anything appears to be incorrect in my post, please reply so we can all learn.

My approach to this bug was pretty straightforward. Bitaddress produces incorrect results on Safari 6.0.5. Let's run two copies of Bitaddress, one on Safari 6.0.5 (buggy), and one on a recent version of Firefox (correct). Then add logging to track down where the buggy version goes wrong. First, I created a few test vectors and I saw that the versions indeed produce different results and that the buggy version produces incorrect ones as expected. Second, I added logging. And then the bug disappeared.

So what happened? As @artiomchi noticed, an extra call to salsa20_8() removes the bug. So does adding logging. Why? Some Googling taught me that the bug is caused by an error in the Javascript optimisation in Safari. So it makes sense that significant changes to the code alter the way in which it gets optimised. Luckily, I managed to work around this quirk by adding minimal logging only. Another issue that @artiomchi noticed is that if you log an array to console, it may give you the final state of the array, rather than the intermediate value when you logged it. I worked around that by using .toString(). In most cases I also used .slice(a, b) to only log part of the array, because the arrays used by Bitaddress can be pretty large. So my logging statements often looked like: console.log(" salt: " + salt.slice(0,32).toString());

Caching

Just when I thought I had fixed the issue of the bug disappearing, it disappeared again. This time, the issue was most probably caused by browser caching. When a piece of Javascript gets optimised by the browser, the browser usually caches the optimised Javascript, just in case it gets called again. So when I was testing multiple test vectors, only the first test vector would be processed in the 'correct buggy way'. For my setup, all subsequent test vectors would be processed in a non-buggy way. So for those of you who created a wallet with bitaddress in Safari 6.0.5, the result may be correct if you started with a test run before you created your actual wallet. Due to browser caching, @DavidVeksler 's brute-forcing version of Bitaddress will probably not work for brute-forcing buggy wallets. For my tests I needed a buggy Safari, so I closed Safari after each run, and used a different html file with a different name for each run.

Tracking down the bug

With these hurdles out of the way, it was time to track down where the bug occurs exactly. This pointed me towards the function scrypt. This function makes 8 calls to scryptCore, and these calls are made in parallel if possible. In my VM setup, the guest OS only had 1 processor core available, so the calls were always made successively. With a few logging statements I found out that the bug appears in the first call to scryptCore. So I went one level deeper.

scryptCore makes multiple calls to ROMix. ROMix is called smix in Bitaddress; I chose to follow the function names used in the Wikipedia description of Scrypt (https://en.wikipedia.org/wiki/Scrypt). Again the bug already appears in the first call. So I went one level deeper again. ROMix makes multiple calls to BlockMix (called blockmix_salsa8 in Bitaddress). To be precise, ROMix has 16384 iterations in each of which it calls BlockMix twice. In this case, the outputs of the buggy and non-buggy version were identical after the first iteration. So I performed a binary search, comparing the results after 8192, 4096, 2048, etc. iterations, to find that the bug first occurs on iteration 10 counting from 0, so the 11-th time it gets called.

BlockMix makes multiple calls to Salsa20/8 (called salsa20_8 in Bitaddress). Another binary search showed that the bug occurs on iteration 6 counting from 0, so the 7-th time it gets called. Before I reached this conclusion, I went through multiple detours exploring other functions like blockxor and arraycopy, to find that they were not causing the bug.

Salsa20/8 performs lots of operations on integer arrays, and it makes a call to arraycopy. arraycopy was not causing the bug, so finally I had arrived at the level where the bug actually happens. At this point I needed to be careful to not produce too much logging, because this would remove the bug again. I worked around this problem by producing logging only for the iteration I was interested in. After looking at different parts of logged arrays and comparing them between the correct and the buggy version, I finaly spotted the bug! The bug didn't actually happen in Salsa20/8, but in BlockMix. For some reason, it sets the first byte of X to 0 after calling Salsa20/8. So effectively, the buggy version works as normal, except that it may be thought to contain an extra statement:

X[0] = 0;

I wasn't done yet. The bug appeared at ROMix iteration 10, BlockMix iteration 6, but was only persistent for a while. After some more binary searches, I found that it disappeared after ROMix iteration 15, BlockMix iteration 7. And now I was done, at least in a practical sense. I still wanted to know why the bug appears after some number of iterations, then disappears again some number of iterations later.

Javascript optimisation

Some more Googling provided a plausible explanation for this. Javascript engines have multiple levels of optimisation. When the script starts, code is executed 'as is'. But the engine keeps track of functions that get called a lot so that it can optimise these. The engine has multiple levels of optimisation, so if a function gets called even more often, it gets optimised a second time, and possibly even a third time. So what we are seeing here is a bug that occurs in either the first or the second optimisation layer. The first part works fine, as does the last part. Somewhere in the middle we have a buggy Javascript optimisation.

Another observation is that Bitaddress works in a non-buggy way if the passphrase is longer than - I think - 16 characters. It makes sense that the code behaves in a slightly different way for a long passphrase: the passphrase enters into PBKDF2, where it acts as an HMAC key. If the HMAC key is longer than the block size of the underlying hash function, an additional call to the hash function is needed to compress it. I believe SHA-256 is used as a hash function, which has a block size of 32 bytes. So this makes sense if the passphrase would be encoded with 2 bytes per character, which I'm not sure is the case.

Multithreading

As I mentioned, my VM's guest OS only had a single core at its disposal, causing scrypt to run 8 successive iterations of scryptCore. One wonders what happens if the OS has multiple cores. It is unclear how this will affect the tallies that tell the Javascript engine when to optimise a function. Behaviour may become non-deterministic as we now have multiple threads with race conditions. So I gave my VM's guest OS two cores, and guess what? The bug disappeared. At this point I'm not sure my VM correctly simulates the way Bitaddress would run on an old MacBook with two cores. If anyone has an old MacBook with Safari 6.0.5 lying around, I would be interested how it handles Bitaddress.

Attachments

I have attached a 'buggy' version of Bitaddress 2.9.3. This should reproduce the bug explained above when run in a normal browser. I expect this may easily be transferred to other versions of Bitaddress by applying the same diffs as I did to the code. This should also work with @DavidVeksler 's brute-forcing version of Bitaddress. The result should be a brute-forcing 'buggy' version of Bitaddress that runs in any browser.

bitaddress.org_buggy.zip

I have also attached a few test vector for the buggy version of Bitaddress.

test_vectors_buggy.txt

Questions and issues

I'm happy to help others who cannot access their BIP-38 encrypted wallet. That is exactly my reason for this post. However, this analysis has taken me quite some time as I'm not a very experienced programmer. So I'm not going to make a buggy branch for other versions of Bitaddress, nor am I building a buggy version of @DavidVeksler 's brute-forcing Bitaddress. Nonetheless, I'm available for questions, so do not hesitate to raise any questions or issues if you run into them.

Hello @brianshould

I still have my MBP from the time I generated my PKs, I will clean it and roll-back to OSX Lion with Safari 6.0.5 to see if its really deterministic.
If you have any other options to unlock my BIP38 wallet

@hlbfrancois
Copy link

hlbfrancois commented Jul 3, 2023

@brianshould Does the buggy branch only support decryption or also encryption with bip38? I haven't been able to create any "buggy paper wallets" with it.

Would @DavidVeksler 's brute forcing tool work with browser caching disabled?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests