v2.5.1 BIP38 passphrase protected paper wallets. Thanks to casascius, scintill, Zeilap.

This commit is contained in:
pointbiz 2013-10-24 23:28:53 -04:00
parent a19c436fff
commit 5453778cac
8 changed files with 804 additions and 116 deletions

File diff suppressed because one or more lines are too long

View file

@ -165,11 +165,19 @@
<div id="paperarea"> <div id="paperarea">
<div id="papercommands" class="commands"> <div id="papercommands" class="commands">
<span><label id="paperlabelhideart">Hide Art?</label> <input type="checkbox" id="paperart" onchange="ninja.wallets.paperwallet.toggleArt(this);" /></span> <span><label id="paperlabelhideart">Hide Art?</label> <input type="checkbox" id="paperart" onchange="ninja.wallets.paperwallet.toggleArt(this);" /></span>
<span><label id="paperlabeladdressesperpage">Addresses per page:</label> <input type="text" id="paperlimitperpage" /></span>
<span><label id="paperlabeladdressestogenerate">Addresses to generate:</label> <input type="text" id="paperlimit" /></span> <span><label id="paperlabeladdressestogenerate">Addresses to generate:</label> <input type="text" id="paperlimit" /></span>
<span><input type="button" id="papergenerate" value="Generate" onclick="ninja.wallets.paperwallet.build(document.getElementById('paperlimit').value * 1, document.getElementById('paperlimitperpage').value * 1, !document.getElementById('paperart').checked);" /></span> <span><input type="button" id="papergenerate" value="Generate" onclick="ninja.wallets.paperwallet.build(document.getElementById('paperlimit').value * 1, document.getElementById('paperlimitperpage').value * 1, !document.getElementById('paperart').checked, document.getElementById('paperpassphrase').value);" /></span>
<span class="print"><input type="button" name="print" value="Print" id="paperprint" onclick="window.print();" /></span> <span class="print"><input type="button" name="print" value="Print" id="paperprint" onclick="window.print();" /></span>
</div> </div>
<div id="paperadvancedcommandsexpandable" class="commands expandable sideborders" onclick="ninja.wallets.paperwallet.openCloseAdvanced();">
<span><label id="paperadvancedcommandslabel">Advanced Options</label></span>
<div id="paperadvancedcommandsicon" class="more"></div>
</div>
<div id="paperadvancedcommands" class="commands sideborders hide">
<span><label id="paperlabelencrypt">BIP38 Encrypt?</label> <input type="checkbox" id="paperencrypt" onchange="ninja.wallets.paperwallet.toggleEncrypt(this);" /></span>
<span><label id="paperlabelBIPpassphrase">Passphrase:</label> <input type="text" id="paperpassphrase" /></span>
<span><label id="paperlabeladdressesperpage">Addresses per page:</label> <input type="text" id="paperlimitperpage" /></span>
</div>
<div id="paperkeyarea"></div> <div id="paperkeyarea"></div>
</div> </div>
@ -380,8 +388,11 @@
</div> </div>
</div> </div>
<div class="authorpgp"> <div class="authorpgp">
<span class="item"><a href="ninja_bitaddress.org.txt" target="_blank" id="footerlabelpgp">PGP Public Key</a></span> <span class="item">
<span class="item"><a href="pgpsignedmsg.txt" target="_blank"><span id="footerlabelversion">Signed Version History</span> (v2.5)</a></span> <a href="pgpsignedmsg.txt" target="_blank"><span id="footerlabelversion">Version History</span> (v2.5.1)</a>
(<a href="ninja_bitaddress.org.txt" target="_blank" id="footerlabelpgp">PGP</a>)
</span>
<span class="item">527B 5C82 B1F6 B2DB 72A0<br />ECBF 8749 7B91 6397 4F5A</span>
<span class="item"><a href="https://github.com/pointbiz/bitaddress.org" target="_blank" id="footerlabelgithub">GitHub Repository</a></span> <span class="item"><a href="https://github.com/pointbiz/bitaddress.org" target="_blank" id="footerlabelgithub">GitHub Repository</a></span>
</div> </div>
<div class="copyright"> <div class="copyright">

File diff suppressed because one or more lines are too long

View file

@ -36,6 +36,7 @@ ninja.privateKey = {
}, },
// 58 base58 characters starting with 6P // 58 base58 characters starting with 6P
isBIP38Format: function (key) { isBIP38Format: function (key) {
key = key.toString();
return (/^6P[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{56}$/.test(key)); return (/^6P[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]{56}$/.test(key));
}, },
BIP38EncryptedKeyToByteArrayAsync: function (base58Encrypted, passphrase, callback) { BIP38EncryptedKeyToByteArrayAsync: function (base58Encrypted, passphrase, callback) {
@ -159,6 +160,135 @@ ninja.privateKey = {
}); });
}); });
} }
},
BIP38PrivateKeyToEncryptedKeyAsync: function (base58Key, passphrase, compressed, callback) {
var privKey = new Bitcoin.ECKey(base58Key);
var privKeyBytes = privKey.getBitcoinPrivateKeyByteArray();
var address = privKey.setCompressed(compressed).getBitcoinAddress();
// compute sha256(sha256(address)) and take first 4 bytes
var salt = Bitcoin.Util.dsha256(address).slice(0, 4);
// derive key using scrypt
var AES_opts = { mode: new Crypto.mode.ECB(Crypto.pad.NoPadding), asBytes: true };
Crypto_scrypt(passphrase, salt, 16384, 8, 8, 64, function (derivedBytes) {
for (var i = 0; i < 32; ++i) {
privKeyBytes[i] ^= derivedBytes[i];
}
// 0x01 0x42 + flagbyte + salt + encryptedhalf1 + encryptedhalf2
var flagByte = compressed ? 0xe0 : 0xc0;
var encryptedKey = [0x01, 0x42, flagByte].concat(salt);
encryptedKey = encryptedKey.concat(Crypto.AES.encrypt(privKeyBytes, derivedBytes.slice(32), AES_opts));
encryptedKey = encryptedKey.concat(Bitcoin.Util.dsha256(encryptedKey).slice(0, 4));
callback(Bitcoin.Base58.encode(encryptedKey));
});
},
BIP38GenerateIntermediatePointAsync: function (passphrase, lotNum, sequenceNum, callback) {
var noNumbers = lotNum === null || sequenceNum === null;
var rng = new SecureRandom();
var ownerEntropy, ownerSalt;
if (noNumbers) {
ownerSalt = ownerEntropy = new Array(8);
rng.nextBytes(ownerEntropy);
}
else {
// 1) generate 4 random bytes
ownerSalt = new Array(4);
rng.nextBytes(ownerSalt);
// 2) Encode the lot and sequence numbers as a 4 byte quantity (big-endian):
// lotnumber * 4096 + sequencenumber. Call these four bytes lotsequence.
var lotSequence = BigInteger(4096 * lotNum + sequenceNum).toByteArrayUnsigned();
// 3) Concatenate ownersalt + lotsequence and call this ownerentropy.
var ownerEntropy = ownerSalt.concat(lotSequence);
}
// 4) Derive a key from the passphrase using scrypt
Crypto_scrypt(passphrase, ownerSalt, 16384, 8, 8, 32, function (prefactor) {
// Take SHA256(SHA256(prefactor + ownerentropy)) and call this passfactor
var passfactorBytes = noNumbers ? prefactor : Bitcoin.Util.dsha256(prefactor.concat(ownerEntropy));
var passfactor = BigInteger.fromByteArrayUnsigned(passfactorBytes);
// 5) Compute the elliptic curve point G * passfactor, and convert the result to compressed notation (33 bytes)
var ellipticCurve = EllipticCurve.getSECCurveByName("secp256k1");
var passpoint = ellipticCurve.getG().multiply(passfactor).getEncoded(1);
// 6) Convey ownersalt and passpoint to the party generating the keys, along with a checksum to ensure integrity.
// magic bytes "2C E9 B3 E1 FF 39 E2 51" followed by ownerentropy, and then passpoint
var magicBytes = [0x2C, 0xE9, 0xB3, 0xE1, 0xFF, 0x39, 0xE2, 0x51];
if (noNumbers) magicBytes[7] = 0x53;
var intermediate = magicBytes.concat(ownerEntropy).concat(passpoint);
// base58check encode
intermediate = intermediate.concat(Bitcoin.Util.dsha256(intermediate).slice(0, 4));
callback(Bitcoin.Base58.encode(intermediate));
});
},
BIP38GenerateECAddressAsync: function (intermediate, compressed, callback) {
// decode IPS
var x = Bitcoin.Base58.decode(intermediate);
//if(x.slice(49, 4) !== Bitcoin.Util.dsha256(x.slice(0,49)).slice(0,4)) {
// callback({error: 'Invalid intermediate passphrase string'});
//}
var noNumbers = (x[7] === 0x53);
var ownerEntropy = x.slice(8, 8 + 8);
var passpoint = x.slice(16, 16 + 33);
// 1) Set flagbyte.
// set bit 0x20 for compressed key
// set bit 0x04 if ownerentropy contains a value for lotsequence
var flagByte = (compressed ? 0x20 : 0x00) | (noNumbers ? 0x00 : 0x04);
// 2) Generate 24 random bytes, call this seedb.
var seedB = new Array(24);
var rng = new SecureRandom();
rng.nextBytes(seedB);
// Take SHA256(SHA256(seedb)) to yield 32 bytes, call this factorb.
var factorB = Bitcoin.Util.dsha256(seedB);
// 3) ECMultiply passpoint by factorb. Use the resulting EC point as a public key and hash it into a Bitcoin
// address using either compressed or uncompressed public key methodology (specify which methodology is used
// inside flagbyte). This is the generated Bitcoin address, call it generatedaddress.
var ec = EllipticCurve.getSECCurveByName("secp256k1").getCurve();
var generatedPoint = ec.decodePointHex(ninja.publicKey.getHexFromByteArray(passpoint));
var generatedBytes = generatedPoint.multiply(BigInteger.fromByteArrayUnsigned(factorB)).getEncoded(compressed);
var generatedAddress = (new Bitcoin.Address(Bitcoin.Util.sha256ripe160(generatedBytes))).toString();
// 4) Take the first four bytes of SHA256(SHA256(generatedaddress)) and call it addresshash.
var addressHash = Bitcoin.Util.dsha256(generatedAddress).slice(0, 4);
// 5) Now we will encrypt seedb. Derive a second key from passpoint using scrypt
Crypto_scrypt(passpoint, addressHash.concat(ownerEntropy), 1024, 1, 1, 64, function (derivedBytes) {
// 6) Do AES256Encrypt(seedb[0...15]] xor derivedhalf1[0...15], derivedhalf2), call the 16-byte result encryptedpart1
for (var i = 0; i < 16; ++i) {
seedB[i] ^= derivedBytes[i];
}
var AES_opts = { mode: new Crypto.mode.ECB(Crypto.pad.NoPadding), asBytes: true };
var encryptedPart1 = Crypto.AES.encrypt(seedB.slice(0, 16), derivedBytes.slice(32), AES_opts);
// 7) Do AES256Encrypt((encryptedpart1[8...15] + seedb[16...23]) xor derivedhalf1[16...31], derivedhalf2), call the 16-byte result encryptedseedb.
var message2 = encryptedPart1.slice(8, 8 + 8).concat(seedB.slice(16, 16 + 8));
for (var i = 0; i < 16; ++i) {
message2[i] ^= derivedBytes[i + 16];
}
var encryptedSeedB = Crypto.AES.encrypt(message2, derivedBytes.slice(32), AES_opts);
// 0x01 0x43 + flagbyte + addresshash + ownerentropy + encryptedpart1[0...7] + encryptedpart2
var encryptedKey = [0x01, 0x43, flagByte].concat(addressHash).concat(ownerEntropy).concat(encryptedPart1.slice(0, 8)).concat(encryptedSeedB);
// base58check encode
encryptedKey = encryptedKey.concat(Bitcoin.Util.dsha256(encryptedKey).slice(0, 4));
callback(generatedAddress, Bitcoin.Base58.encode(encryptedKey));
});
} }
}; };

View file

@ -7,12 +7,10 @@ if (ninja.getQueryString()["unittests"] == "true" || ninja.getQueryString()["uni
if (ninja.getQueryString()["asyncunittests"] == "true" || ninja.getQueryString()["asyncunittests"] == "1") { if (ninja.getQueryString()["asyncunittests"] == "true" || ninja.getQueryString()["asyncunittests"] == "1") {
ninja.unitTests.runAsynchronousTests(); ninja.unitTests.runAsynchronousTests();
} }
// change language // change language
if (ninja.getQueryString()["culture"] != undefined) { if (ninja.getQueryString()["culture"] != undefined) {
ninja.translator.translate(ninja.getQueryString()["culture"]); ninja.translator.translate(ninja.getQueryString()["culture"]);
} }
// testnet, check if testnet edition should be activated // testnet, check if testnet edition should be activated
if (ninja.getQueryString()["testnet"] == "true" || ninja.getQueryString()["testnet"] == "1") { if (ninja.getQueryString()["testnet"] == "true" || ninja.getQueryString()["testnet"] == "1") {
document.getElementById("testnet").innerHTML = ninja.translator.get("testneteditionactivated"); document.getElementById("testnet").innerHTML = ninja.translator.get("testneteditionactivated");
@ -23,6 +21,5 @@ if (ninja.getQueryString()["testnet"] == "true" || ninja.getQueryString()["testn
Bitcoin.ECKey.privateKeyPrefix = 0xEF; // testnet Bitcoin.ECKey.privateKeyPrefix = 0xEF; // testnet
ninja.testnetMode = true; ninja.testnetMode = true;
} }
// if users does not move mouse after random amount of time then generate the key anyway. // if users does not move mouse after random amount of time then generate the key anyway.
setTimeout(ninja.seeder.forceGenerate, ninja.seeder.seedLimit * 20); setTimeout(ninja.seeder.forceGenerate, ninja.seeder.seedLimit * 20);

File diff suppressed because one or more lines are too long

View file

@ -34,6 +34,7 @@ ninja.translator = {
"testneteditionactivated": "TESTNET EDITION ACTIVATED", "testneteditionactivated": "TESTNET EDITION ACTIVATED",
"paperlabelbitcoinaddress": "Bitcoin Address:", "paperlabelbitcoinaddress": "Bitcoin Address:",
"paperlabelprivatekey": "Private Key (Wallet Import Format):", "paperlabelprivatekey": "Private Key (Wallet Import Format):",
"paperlabelencryptedkey": "Encrypted Private Key (Password required)",
"bulkgeneratingaddresses": "Generating addresses... ", "bulkgeneratingaddresses": "Generating addresses... ",
"brainalertpassphrasetooshort": "The passphrase you entered is too short.\n\nWarning: Choosing a strong passphrase is important to avoid brute force attempts to guess your passphrase and steal your bitcoins.", "brainalertpassphrasetooshort": "The passphrase you entered is too short.\n\nWarning: Choosing a strong passphrase is important to avoid brute force attempts to guess your passphrase and steal your bitcoins.",
"brainalertpassphrasedoesnotmatch": "The passphrase does not match the confirm passphrase.", "brainalertpassphrasedoesnotmatch": "The passphrase does not match the confirm passphrase.",
@ -53,6 +54,7 @@ ninja.translator = {
"testneteditionactivated": "Testnet se activa", "testneteditionactivated": "Testnet se activa",
"paperlabelbitcoinaddress": "Dirección Bitcoin:", "paperlabelbitcoinaddress": "Dirección Bitcoin:",
"paperlabelprivatekey": "Clave privada (formato para importar):", "paperlabelprivatekey": "Clave privada (formato para importar):",
"paperlabelencryptedkey": "Clave privada cifrada (contraseña necesaria)",
"bulkgeneratingaddresses": "Generación de direcciones... ", "bulkgeneratingaddresses": "Generación de direcciones... ",
"brainalertpassphrasetooshort": "La contraseña introducida es demasiado corta.\n\nAviso: Es importante escoger una contraseña fuerte para evitar ataques de fuerza bruta a fin de adivinarla y robar tus bitcoins.", "brainalertpassphrasetooshort": "La contraseña introducida es demasiado corta.\n\nAviso: Es importante escoger una contraseña fuerte para evitar ataques de fuerza bruta a fin de adivinarla y robar tus bitcoins.",
"brainalertpassphrasedoesnotmatch": "Las contraseñas no coinciden.", "brainalertpassphrasedoesnotmatch": "Las contraseñas no coinciden.",
@ -80,7 +82,7 @@ ninja.translator = {
// footer html // footer html
"footerlabeldonations": "Donaciones:", "footerlabeldonations": "Donaciones:",
"footerlabeltranslatedby": "Traducción: <b>12345</b>Vypv2QSmuRXcciT5oEB27mPbWGeva", "footerlabeltranslatedby": "Traducción: <b>12345</b>Vypv2QSmuRXcciT5oEB27mPbWGeva",
"footerlabelpgp": "Clave pública PGP", "footerlabelpgp": "PGP",
"footerlabelversion": "Histórico de versiones", "footerlabelversion": "Histórico de versiones",
"footerlabelgithub": "Repositorio GitHub", "footerlabelgithub": "Repositorio GitHub",
"footerlabelcopyright1": "Copyright bitaddress.org.", "footerlabelcopyright1": "Copyright bitaddress.org.",
@ -99,6 +101,9 @@ ninja.translator = {
"paperlabeladdressestogenerate": "Direcciones en total:", "paperlabeladdressestogenerate": "Direcciones en total:",
"papergenerate": "Generar", "papergenerate": "Generar",
"paperprint": "Imprimir", "paperprint": "Imprimir",
"paperlabelBIPpassphrase": "Passphrase:", //TODO: please translate
"paperlabelencrypt": "BIP38 Encrypt?", //TODO: please translate
"paperadvancedcommandslabel": "Advanced Options", //TODO: please translate
// bulk wallet html // bulk wallet html
"bulklabelstartindex": "Empezar en:", "bulklabelstartindex": "Empezar en:",
@ -173,6 +178,7 @@ ninja.translator = {
"testneteditionactivated": "ÉDITION TESTNET ACTIVÉE", "testneteditionactivated": "ÉDITION TESTNET ACTIVÉE",
"paperlabelbitcoinaddress": "Adresse Bitcoin:", "paperlabelbitcoinaddress": "Adresse Bitcoin:",
"paperlabelprivatekey": "Clé Privée (Format d'importation de porte-monnaie):", "paperlabelprivatekey": "Clé Privée (Format d'importation de porte-monnaie):",
"paperlabelencryptedkey": "Encrypted Private Key (Password required)", //TODO: please translate
"bulkgeneratingaddresses": "Création de l'adresse... ", "bulkgeneratingaddresses": "Création de l'adresse... ",
"brainalertpassphrasetooshort": "Le mot de passe que vous avez entré est trop court.\n\nAttention: Choisir un mot de passe solide est important pour vous protéger des attaques bruteforce visant à trouver votre mot de passe et voler vos Bitcoins.", "brainalertpassphrasetooshort": "Le mot de passe que vous avez entré est trop court.\n\nAttention: Choisir un mot de passe solide est important pour vous protéger des attaques bruteforce visant à trouver votre mot de passe et voler vos Bitcoins.",
"brainalertpassphrasedoesnotmatch": "Le mot de passe ne correspond pas au mot de passe de vérification.", "brainalertpassphrasedoesnotmatch": "Le mot de passe ne correspond pas au mot de passe de vérification.",
@ -196,8 +202,8 @@ ninja.translator = {
"detailwallet": "Détails du Porte-Monnaie", "detailwallet": "Détails du Porte-Monnaie",
"footerlabeldonations": "Dons:", "footerlabeldonations": "Dons:",
"footerlabeltranslatedby": "Traduction: 1Gy7NYSJNUYqUdXTBow5d7bCUEJkUFDFSq", "footerlabeltranslatedby": "Traduction: 1Gy7NYSJNUYqUdXTBow5d7bCUEJkUFDFSq",
"footerlabelpgp": "Clé Publique PGP", "footerlabelpgp": "PGP",
"footerlabelversion": "Historique De Version Signé", "footerlabelversion": "Historique De Version",
"footerlabelgithub": "Dépôt GitHub", "footerlabelgithub": "Dépôt GitHub",
"footerlabelcopyright1": "Copyright bitaddress.org.", "footerlabelcopyright1": "Copyright bitaddress.org.",
"footerlabelcopyright2": "Les droits d'auteurs JavaScript sont inclus dans le code source.", "footerlabelcopyright2": "Les droits d'auteurs JavaScript sont inclus dans le code source.",
@ -211,6 +217,9 @@ ninja.translator = {
"paperlabeladdressestogenerate": "Nombre d'adresses à créer:", "paperlabeladdressestogenerate": "Nombre d'adresses à créer:",
"papergenerate": "Générer", "papergenerate": "Générer",
"paperprint": "Imprimer", "paperprint": "Imprimer",
"paperlabelBIPpassphrase": "Passphrase:", //TODO: please translate
"paperlabelencrypt": "BIP38 Encrypt?", //TODO: please translate
"paperadvancedcommandslabel": "Advanced Options", //TODO: please translate
"bulklabelstartindex": "Commencer à l'index:", "bulklabelstartindex": "Commencer à l'index:",
"bulklabelrowstogenerate": "Colonnes à générer:", "bulklabelrowstogenerate": "Colonnes à générer:",
"bulklabelcompressed": "Compressed addresses?", //TODO: please translate "bulklabelcompressed": "Compressed addresses?", //TODO: please translate
@ -278,6 +287,7 @@ ninja.translator = {
"testneteditionactivated": "ΕΝΕΡΓΗ ΕΚΔΟΣΗ TESTNET", "testneteditionactivated": "ΕΝΕΡΓΗ ΕΚΔΟΣΗ TESTNET",
"paperlabelbitcoinaddress": "Διεύθυνση Bitcoin:", "paperlabelbitcoinaddress": "Διεύθυνση Bitcoin:",
"paperlabelprivatekey": "Προσωπικό Κλειδί (Μορφή εισαγωγής σε πορτοφόλι):", "paperlabelprivatekey": "Προσωπικό Κλειδί (Μορφή εισαγωγής σε πορτοφόλι):",
"paperlabelencryptedkey": "Encrypted Private Key (Password required)", //TODO: please translate
"bulkgeneratingaddresses": "Δημιουργία διευθύνσεων... ", "bulkgeneratingaddresses": "Δημιουργία διευθύνσεων... ",
"brainalertpassphrasetooshort": "Η φράση κωδικός που δώσατε είναι πολύ αδύναμη.\n\nΠροσοχή: Είναι σημαντικό να επιλέξετε μια ισχυρή φράση κωδικό που θα σας προφυλάξει από απόπειρες παραβίασής της τύπου brute force και κλοπή των bitcoins σας.", "brainalertpassphrasetooshort": "Η φράση κωδικός που δώσατε είναι πολύ αδύναμη.\n\nΠροσοχή: Είναι σημαντικό να επιλέξετε μια ισχυρή φράση κωδικό που θα σας προφυλάξει από απόπειρες παραβίασής της τύπου brute force και κλοπή των bitcoins σας.",
"brainalertpassphrasedoesnotmatch": "Η φράση κωδικός και η επιβεβαίωση της δε συμφωνούν.", "brainalertpassphrasedoesnotmatch": "Η φράση κωδικός και η επιβεβαίωση της δε συμφωνούν.",
@ -305,8 +315,8 @@ ninja.translator = {
// footer html // footer html
"footerlabeldonations": "Δωρεές:", "footerlabeldonations": "Δωρεές:",
"footerlabeltranslatedby": "Μετάφραση: <a href='http://BitcoinX.gr/'><b>BitcoinX.gr</b></a> 1BitcoiNxkUPcTFxwMqxhRiPEiQRzYskf6", "footerlabeltranslatedby": "Μετάφραση: <a href='http://BitcoinX.gr/'><b>BitcoinX.gr</b></a> 1BitcoiNxkUPcTFxwMqxhRiPEiQRzYskf6",
"footerlabelpgp": "Δημόσιο Κλειδί PGP", "footerlabelpgp": "PGP",
"footerlabelversion": "Υπογεγραμένο ιστορικό εκδόσεων", "footerlabelversion": "ιστορικό εκδόσεων",
"footerlabelgithub": "Αποθετήριο GitHub", "footerlabelgithub": "Αποθετήριο GitHub",
"footerlabelcopyright1": "Copyright bitaddress.org.", "footerlabelcopyright1": "Copyright bitaddress.org.",
"footerlabelcopyright2": "Τα πνευματικά δικαιώματα της JavaScript περιλαμβάνονται στον κώδικα.", "footerlabelcopyright2": "Τα πνευματικά δικαιώματα της JavaScript περιλαμβάνονται στον κώδικα.",
@ -324,6 +334,9 @@ ninja.translator = {
"paperlabeladdressestogenerate": "Πλήθος διευθύνσεων:", "paperlabeladdressestogenerate": "Πλήθος διευθύνσεων:",
"papergenerate": "Δημιουργία", "papergenerate": "Δημιουργία",
"paperprint": "Εκτύπωση", "paperprint": "Εκτύπωση",
"paperlabelBIPpassphrase": "Passphrase:", //TODO: please translate
"paperlabelencrypt": "BIP38 Encrypt?", //TODO: please translate
"paperadvancedcommandslabel": "Advanced Options", //TODO: please translate
// bulk wallet html // bulk wallet html
"bulklabelstartindex": "Ξεκίνημα δείκτη:", "bulklabelstartindex": "Ξεκίνημα δείκτη:",
@ -423,7 +436,7 @@ ninja.translator.showEnglishJson = function () {
div.setAttribute("class", "englishjson"); div.setAttribute("class", "englishjson");
div.innerHTML = "<h3>English Json</h3>"; div.innerHTML = "<h3>English Json</h3>";
var elem = document.createElement("textarea"); var elem = document.createElement("textarea");
elem.setAttribute("rows", "35"); elem.setAttribute("rows", "15");
elem.setAttribute("cols", "110"); elem.setAttribute("cols", "110");
elem.setAttribute("wrap", "off"); elem.setAttribute("wrap", "off");
var langJson = "{\n"; var langJson = "{\n";

View file

@ -38,12 +38,18 @@
}, },
runAsynchronousTests: function () { runAsynchronousTests: function () {
document.getElementById("busyblock").className = "busy"; var div = document.createElement("div");
div.setAttribute("class", "unittests");
div.setAttribute("id", "asyncunittests");
div.innerHTML = "<h3>Async Unit Tests</h3><div id=\"asyncunittestresults\"></div><br/><br/><br/><br/>";
document.body.appendChild(div);
// run the asynchronous tests one after another so we don't crash the browser // run the asynchronous tests one after another so we don't crash the browser
ninja.foreachSerialized(ninja.unitTests.asynchronousTests, function (name, cb) { ninja.foreachSerialized(ninja.unitTests.asynchronousTests, function (name, cb) {
document.getElementById("busyblock").className = "busy";
ninja.unitTests.asynchronousTests[name](cb); ninja.unitTests.asynchronousTests[name](cb);
}, function () { }, function () {
document.getElementById("unittestresults").innerHTML += "running of asynchronous unit tests complete!<br/>"; document.getElementById("asyncunittestresults").innerHTML += "running of asynchronous unit tests complete!<br/>";
document.getElementById("busyblock").className = ""; document.getElementById("busyblock").className = "";
}); });
}, },
@ -458,44 +464,132 @@
}, },
asynchronousTests: { asynchronousTests: {
//https://en.bitcoin.it/wiki/BIP_0038
testBip38: function (done) { testBip38: function (done) {
var tests = [["6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg", "TestingOneTwoThree", "5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR"], var tests = [
["6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq", "Satoshi", "5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5"], //No compression, no EC multiply
["6PYNKZ1EAgYgmQfmNVamxyXVWHzK5s6DGhwP4J5o44cvXdoY7sRzhtpUeo", "TestingOneTwoThree", "L44B5gGEpqEDRS9vVPz7QT35jcBG2r3CZwSwQ4fCewXAhAhqGVpP"], ["6PRVWUbkzzsbcVac2qwfssoUJAN1Xhrg6bNk8J7Nzm5H7kxEbn2Nh2ZoGg", "TestingOneTwoThree", "5KN7MzqK5wt2TP1fQCYyHBtDrXdJuXbUzm4A9rKAteGu3Qi5CVR"],
["6PYLtMnXvfG3oJde97zRyLYFZCYizPU5T3LwgdYJz1fRhh16bU7u6PPmY7", "Satoshi", "KwYgW8gcxj1JWJXhPSu4Fqwzfhp5Yfi42mdYmMa4XqK7NJxXUSK7"], ["6PRNFFkZc2NZ6dJqFfhRoFNMR9Lnyj7dYGrzdgXXVMXcxoKTePPX1dWByq", "Satoshi", "5HtasZ6ofTHP6HCwTqTkLDuLQisYPah7aUnSKfC7h4hMUVw2gi5"],
["6PfQu77ygVyJLZjfvMLyhLMQbYnu5uguoJJ4kMCLqWwPEdfpwANVS76gTX", "TestingOneTwoThree", "5K4caxezwjGCGfnoPTZ8tMcJBLB7Jvyjv4xxeacadhq8nLisLR2"], //Compression, no EC multiply
["6PfLGnQs6VZnrNpmVKfjotbnQuaJK4KZoPFrAjx1JMJUa1Ft8gnf5WxfKd", "Satoshi", "5KJ51SgxWaAYR13zd9ReMhJpwrcX47xTJh2D3fGPG9CM8vkv5sH"], ["6PYNKZ1EAgYgmQfmNVamxyXVWHzK5s6DGhwP4J5o44cvXdoY7sRzhtpUeo", "TestingOneTwoThree", "L44B5gGEpqEDRS9vVPz7QT35jcBG2r3CZwSwQ4fCewXAhAhqGVpP"],
["6PgNBNNzDkKdhkT6uJntUXwwzQV8Rr2tZcbkDcuC9DZRsS6AtHts4Ypo1j", "MOLON LABE", "5JLdxTtcTHcfYcmJsNVy1v2PMDx432JPoYcBTVVRHpPaxUrdtf8"], ["6PYLtMnXvfG3oJde97zRyLYFZCYizPU5T3LwgdYJz1fRhh16bU7u6PPmY7", "Satoshi", "KwYgW8gcxj1JWJXhPSu4Fqwzfhp5Yfi42mdYmMa4XqK7NJxXUSK7"],
["6PgGWtx25kUg8QWvwuJAgorN6k9FbE25rv5dMRwu5SKMnfpfVe5mar2ngH", Crypto.charenc.UTF8.bytesToString([206, 156, 206, 159, 206, 155, 206, 169, 206, 157, 32, 206, 155, 206, 145, 206, 146, 206, 149])/*UTF-8 characters, encoded in source so they don't get corrupted*/, "5KMKKuUmAkiNbA3DazMQiLfDq47qs8MAEThm4yL8R2PhV1ov33D"]]; //EC multiply, no compression, no lot/sequence numbers
["6PfQu77ygVyJLZjfvMLyhLMQbYnu5uguoJJ4kMCLqWwPEdfpwANVS76gTX", "TestingOneTwoThree", "5K4caxezwjGCGfnoPTZ8tMcJBLB7Jvyjv4xxeacadhq8nLisLR2"],
["6PfLGnQs6VZnrNpmVKfjotbnQuaJK4KZoPFrAjx1JMJUa1Ft8gnf5WxfKd", "Satoshi", "5KJ51SgxWaAYR13zd9ReMhJpwrcX47xTJh2D3fGPG9CM8vkv5sH"],
//EC multiply, no compression, lot/sequence numbers
["6PgNBNNzDkKdhkT6uJntUXwwzQV8Rr2tZcbkDcuC9DZRsS6AtHts4Ypo1j", "MOLON LABE", "5JLdxTtcTHcfYcmJsNVy1v2PMDx432JPoYcBTVVRHpPaxUrdtf8"],
["6PgGWtx25kUg8QWvwuJAgorN6k9FbE25rv5dMRwu5SKMnfpfVe5mar2ngH", Crypto.charenc.UTF8.bytesToString([206, 156, 206, 159, 206, 155, 206, 169, 206, 157, 32, 206, 155, 206, 145, 206, 146, 206, 149])/*UTF-8 characters, encoded in source so they don't get corrupted*/, "5KMKKuUmAkiNbA3DazMQiLfDq47qs8MAEThm4yL8R2PhV1ov33D"]];
// running each test uses a lot of memory, which isn't freed // running each test uses a lot of memory, which isn't freed
// immediately, so give the VM a little time to reclaim memory // immediately, so give the VM a little time to reclaim memory
function waitThenCall(callback) { function waitThenCall(callback) {
return function () { setTimeout(callback, 6000); } return function () { setTimeout(callback, 10000); }
} }
var decryptTest = function (test, i, onComplete) { var decryptTest = function (test, i, onComplete) {
ninja.privateKey.BIP38EncryptedKeyToByteArrayAsync(test[0], test[1], function (privBytes) { ninja.privateKey.BIP38EncryptedKeyToByteArrayAsync(test[0], test[1], function (privBytes) {
if (privBytes.constructor == Error) { if (privBytes.constructor == Error) {
document.getElementById("unittestresults").innerHTML += "fail testDecryptBip38 #" + i + ", error: " + privBytes.message + "<br/>"; document.getElementById("asyncunittestresults").innerHTML += "fail testDecryptBip38 #" + i + ", error: " + privBytes.message + "<br/>";
} else { } else {
var btcKey = new Bitcoin.ECKey(privBytes); var btcKey = new Bitcoin.ECKey(privBytes);
var wif = !test[2].substr(0, 1).match(/[LK]/) ? btcKey.setCompressed(false).getBitcoinWalletImportFormat() : btcKey.setCompressed(true).getBitcoinWalletImportFormat(); var wif = !test[2].substr(0, 1).match(/[LK]/) ? btcKey.setCompressed(false).getBitcoinWalletImportFormat() : btcKey.setCompressed(true).getBitcoinWalletImportFormat();
if (wif != test[2]) { if (wif != test[2]) {
document.getElementById("unittestresults").innerHTML += "fail testDecryptBip38 #" + i + "<br/>"; document.getElementById("asyncunittestresults").innerHTML += "fail testDecryptBip38 #" + i + "<br/>";
} else { } else {
document.getElementById("unittestresults").innerHTML += "pass testDecryptBip38 #" + i + "<br/>"; document.getElementById("asyncunittestresults").innerHTML += "pass testDecryptBip38 #" + i + "<br/>";
} }
} }
onComplete(); onComplete();
}); });
};
var encryptTest = function (test, compressed, i, onComplete) {
ninja.privateKey.BIP38PrivateKeyToEncryptedKeyAsync(test[2], test[1], compressed, function (encryptedKey) {
if (encryptedKey === test[0]) {
document.getElementById("asyncunittestresults").innerHTML += "pass testBip38Encrypt #" + i + "<br/>";
} else {
document.getElementById("asyncunittestresults").innerHTML += "fail testBip38Encrypt #" + i + "<br/>";
document.getElementById("asyncunittestresults").innerHTML += "expected " + test[0] + "<br/>received " + encryptedKey + "<br/>";
}
onComplete();
});
};
// test randomly generated encryption-decryption cycle
var cycleTest = function (i, compress, onComplete) {
// create new private key
var privKey = (new Bitcoin.ECKey(false)).getBitcoinWalletImportFormat();
// encrypt private key
ninja.privateKey.BIP38PrivateKeyToEncryptedKeyAsync(privKey, 'testing', compress, function (encryptedKey) {
// decrypt encryptedKey
ninja.privateKey.BIP38EncryptedKeyToByteArrayAsync(encryptedKey, 'testing', function (decryptedBytes) {
var decryptedKey = (new Bitcoin.ECKey(decryptedBytes)).getBitcoinWalletImportFormat();
if (decryptedKey === privKey) {
document.getElementById("asyncunittestresults").innerHTML += "pass cycleBip38 test #" + i + "<br/>";
}
else {
document.getElementById("asyncunittestresults").innerHTML += "fail cycleBip38 test #" + i + " " + privKey + "<br/>";
document.getElementById("asyncunittestresults").innerHTML += "encrypted key: " + encryptedKey + "<br/>decrypted key: " + decryptedKey;
}
onComplete();
});
});
};
// intermediate test - create some encrypted keys from an intermediate
// then decrypt them to check that the private keys are recoverable
var intermediateTest = function (i, onComplete) {
var pass = Math.random().toString(36).substr(2);
ninja.privateKey.BIP38GenerateIntermediatePointAsync(pass, null, null, function (intermediatePoint) {
ninja.privateKey.BIP38GenerateECAddressAsync(intermediatePoint, false, function (address, encryptedKey) {
ninja.privateKey.BIP38EncryptedKeyToByteArrayAsync(encryptedKey, pass, function (privBytes) {
if (privBytes.constructor == Error) {
document.getElementById("asyncunittestresults").innerHTML += "fail testBip38Intermediate #" + i + ", error: " + privBytes.message + "<br/>";
} else {
var btcKey = new Bitcoin.ECKey(privBytes);
var btcAddress = btcKey.getBitcoinAddress();
if (address !== btcKey.getBitcoinAddress()) {
document.getElementById("asyncunittestresults").innerHTML += "fail testBip38Intermediate #" + i + "<br/>";
} else {
document.getElementById("asyncunittestresults").innerHTML += "pass testBip38Intermediate #" + i + "<br/>";
}
}
onComplete();
});
});
});
} }
document.getElementById("unittestresults").innerHTML += "running " + tests.length + " tests named testDecryptBip38<br/>"; document.getElementById("asyncunittestresults").innerHTML += "running " + tests.length + " tests named testDecryptBip38<br/>";
ninja.runSerialized([function (cb) { document.getElementById("asyncunittestresults").innerHTML += "running 4 tests named testBip38Encrypt<br/>";
ninja.forSerialized(0, tests.length, function (i, callback) { document.getElementById("asyncunittestresults").innerHTML += "running 2 tests named cycleBip38<br/>";
decryptTest(tests[i], i, waitThenCall(callback)); document.getElementById("asyncunittestresults").innerHTML += "running 5 tests named testBip38Intermediate<br/>";
}, waitThenCall(cb)); ninja.runSerialized([
} ], done); function (cb) {
ninja.forSerialized(0, tests.length, function (i, callback) {
decryptTest(tests[i], i, waitThenCall(callback));
}, waitThenCall(cb));
},
function (cb) {
ninja.forSerialized(0, 4, function (i, callback) {
// only first 4 test vectors are not EC-multiply,
// compression param false for i = 1,2 and true for i = 3,4
encryptTest(tests[i], i >= 2, i, waitThenCall(callback));
}, waitThenCall(cb));
},
function (cb) {
ninja.forSerialized(0, 2, function (i, callback) {
cycleTest(i, i % 2 ? true : false, waitThenCall(callback));
}, waitThenCall(cb));
},
function (cb) {
ninja.forSerialized(0, 5, function (i, callback) {
intermediateTest(i, waitThenCall(callback));
}, cb);
}
], done);
} }
} }
}; };