diff --git a/.gitignore b/.gitignore index 7227d6ef0fb440eddc6643cc6e38a76cafe55755..f8edd6e05d25e9969f82ca5b2462078886b49d95 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *generated* node_modules/ .idea/ +vendors/ diff --git a/build.js b/build.js new file mode 100644 index 0000000000000000000000000000000000000000..3eed99b8997d198b97fe698e3a0f883a008c414e --- /dev/null +++ b/build.js @@ -0,0 +1,14 @@ +const fs = require("fs"); +try{fs.rmdirSync("public/vendors");} catch (e) {console.error(e);} +try{fs.mkdirSync("public/vendors");} catch (e) {console.error(e);} +try{fs.copyFileSync("node_modules/tweetnacl/nacl-fast.js","public/vendors/nacl.js");} catch (e) {console.error(e);} +try{fs.copyFileSync("node_modules/scrypt-async-modern/src/index.js","public/vendors/scrypt.js");} catch (e) {console.error(e);} +try{fs.writeFileSync("public/vendors/nacl.js",(fs.readFileSync("public/vendors/nacl.js","utf8")) + .replace("(function(nacl) {","var nacl = {};") + .replace("})(typeof module !== 'undefined' && module.exports ? module.exports : (self.nacl = self.nacl || {}));","export {nacl};") + ,"utf8");} catch (e) {console.error(e);} +// let js = fs.readFileSync("node_modules/jparticles/production/jparticles.js","utf8"); +// js += fs.readFileSync("node_modules/jparticles/production/particle.js","utf8"); +// js += fs.readFileSync("main.js","utf8"); +// fs.writeFileSync("main.js",js,"utf8"); +// fs.writeFileSync("index.html",(fs.readFileSync("index.html","utf8")).replace(/(.*node_modules.*)/g,''),"utf8"); diff --git a/package.json b/package.json index 1ad60e1018dde291d0d0c3756a98e6d525bf4673..bac24a1ce948e71775790d6fb7acf1295077a039 100644 --- a/package.json +++ b/package.json @@ -4,12 +4,20 @@ "description": "Ǧ'Perdu l'accès à mon compte, Ǧ'spère le retrouver", "main": "public/index.html", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "postinstall": "node build.js", + "test": "echo open public/test.html in your browser" }, "repository": { "type": "git", "url": "https://git.duniter.org/clients/Gsper.git" }, "author": "[1000i100] Millicent Billette <git@1000i100.fr> (https://1forma-tic.fr/)", - "license": "AGPL-3.0" + "license": "AGPL-3.0", + "dependencies": { + "scrypt-async-modern": "^3.0.6", + "tweetnacl": "^1.0.0" + }, + "devDependencies": { + "jasmine-core": "^3.1.0" + } } diff --git a/public/crypto.js b/public/crypto.js new file mode 100644 index 0000000000000000000000000000000000000000..c483ce12ef622d501be66b193729573ba6693a8d --- /dev/null +++ b/public/crypto.js @@ -0,0 +1,91 @@ +export {b58, saltPass2seed, seed2keyPair,idSecPass2rawAll, raw2b58, idSecPass2cleanKeys} +import {nacl} from "./vendors/nacl.js"; + +async function idSecPass2rawAll(idSec,pass) { + const rawSeed = await saltPass2seed(idSec,pass); + const keyPair = seed2keyPair(rawSeed); + return { + seed:rawSeed, + publicKey:keyPair.publicKey, + secretKey:keyPair.secretKey + } +} +function raw2b58(raws){ + const res = {}; + for(let r in raws) res[r] = b58.encode(raws[r]); + return res; +} +async function idSecPass2cleanKeys(idSec,pass){ + const raw = await idSecPass2rawAll(idSec,pass); + return Object.assign(raw2b58(raw),{idSec,password:pass}); +} +function seed2keyPair(seed){ + return nacl.sign.keyPair.fromSeed(seed); +} +import scrypt from "./vendors/scrypt.js"; +async function saltPass2seed(idSec,pass) { + const options = { + logN: 12, + r: 16, + p: 1, + //dkLen: 32, + encoding: 'binary' + }; + return await scrypt(pass.normalize('NFKC'), idSec.normalize('NFKC'), options); +} +//inspired by bs58 and base-x module +const ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'; +const b58 = basex(ALPHABET); +function basex (ALPHABET) { + const ALPHABET_MAP = {}; + const BASE = ALPHABET.length; + const LEADER = ALPHABET.charAt(0); + // pre-compute lookup table + for (let z = 0; z < ALPHABET.length; z++) { + let x = ALPHABET.charAt(z); + if (ALPHABET_MAP[x] !== undefined) throw new TypeError(x + ' is ambiguous'); + ALPHABET_MAP[x] = z; + } + function encode (source) { + if (source.length === 0) return ''; + const digits = [0]; + for (let i = 0; i < source.length; ++i) { + let carry = source[i]; + for (let j = 0; j < digits.length; ++j) { + carry += digits[j] << 8; + digits[j] = carry % BASE; + carry = (carry / BASE) | 0; + } + while (carry > 0) { digits.push(carry % BASE); carry = (carry / BASE) | 0; } + } + let string = ''; + for (let k = 0; source[k] === 0 && k < source.length - 1; ++k) string += LEADER; // deal with leading zeros + for (let q = digits.length - 1; q >= 0; --q) string += ALPHABET[digits[q]]; // convert digits to a string + return string; + } + + function decodeUnsafe (string) { + if (typeof string !== 'string') throw new TypeError('Expected String'); + if (string.length === 0) return new Uint8Array(0); + const bytes = [0]; + for (let i = 0; i < string.length; i++) { + const value = ALPHABET_MAP[string[i]]; + if (value === undefined) return ; + let carry = value; + for (let j = 0; j < bytes.length; ++j) { + carry += bytes[j] * BASE; + bytes[j] = carry & 0xff; + carry >>= 8; + } + while (carry > 0) { bytes.push(carry & 0xff); carry >>= 8; } + } + for (let k = 0; string[k] === LEADER && k < string.length - 1; ++k) bytes.push(0); // deal with leading zeros + return new Uint8Array(bytes.reverse()); + } + function decode (string) { + const buffer = decodeUnsafe(string); + if (buffer) return buffer; + throw new Error('Non-base' + BASE + ' character') + } + return { encode, decodeUnsafe, decode } +} diff --git a/public/crypto.test.js b/public/crypto.test.js new file mode 100644 index 0000000000000000000000000000000000000000..95830da1620bc8051ffa7ca679d84e06d8feca1f --- /dev/null +++ b/public/crypto.test.js @@ -0,0 +1,33 @@ +const idSec = "a"; +const mdp = "b"; +//base58 +const pubKey = "AoxVA41dGL2s4ogMNdbCw3FFYjFo5FPK36LuiW1tjGbG"; +const secretKey = "3ZsmZhnRv137dS1s7Q3jFGKLTDyhkwguPHfnWBxzDCTTHKWGnYw9zBk3gcCUJCc72TEUuyzM7cqpo7c5LYhs1Qtv"; +const seed = "9eADqX8V6VcPdJCHCVYiE1Vnift9nFNrvr9aTaXA5RJc"; + +import * as app from "./crypto.js"; +describe('crypto', () => { + it('b58 should decode/encode well', () => { + expect(app.b58.encode(app.b58.decode(pubKey))).toEqual(pubKey); + }); + it('saltPass2seed should convert salt & password to seed with scrypt', async () => { + expect(app.b58.encode(await app.saltPass2seed(idSec,mdp))).toEqual(seed); + }); + it('seed2keyPair should generate public and private key nacl/sodium way.', async () => { + const rawSeed = app.b58.decode(seed); + const rawKeyPair = app.seed2keyPair(rawSeed); + expect(app.b58.encode(rawKeyPair.publicKey)).toEqual(pubKey); + expect(app.b58.encode(rawKeyPair.secretKey)).toEqual(secretKey); + }); + it('idSecPass2cleanKeys should output clean base58 keys and seed', async () => { + const res = await app.idSecPass2cleanKeys(idSec,mdp); + expect(res.publicKey).toEqual(pubKey); + expect(res.secretKey).toEqual(secretKey); + expect(res.seed).toEqual(seed); + expect(res.idSec).toEqual(idSec); + expect(res.password).toEqual(mdp); + }); + +}); + + diff --git a/public/index.html b/public/index.html index bbf0665aace84fc9ab0dd2403a0b5444e9dc40a5..1c0ea73b4d71a99b568eda5d488e6329b09ec716 100644 --- a/public/index.html +++ b/public/index.html @@ -13,50 +13,7 @@ <meta property="og:url" content="https://gsper.duniter.io/" /> <meta property="og:image" content="https://gsper.duniter.io/img/gsper-logo.png" /> <meta property="og:description" content="Ǧ'Perdu l'accès à mon compte, Ǧ'spère le retrouver ! ǦéDéClé à tester !" /> - <style> - * {font-family: sans-serif; box-sizing: border-box; margin: 0; padding: 0} - h1{font-weight: normal; width: 100%; text-align: center; line-height: 70px;} - input[type="text"],textarea{border: 0; padding: 5px;} - legend {margin-left: 10px; padding: 0 5px} - em{font-size: 70%} - .pasEncore{display: none} - fieldset, #info {margin: 10px;} - header, #lists, header fieldset{ - display: flex; - flex-wrap: wrap; - } - header fieldset {flex: 8; margin-top: 0; display: block;} - header #info {flex: 2} - header fieldset label, header fieldset input { padding: 5px;line-height: 60px; font-size: 16px;} - header fieldset label {white-space: nowrap; flex: 1;} - header fieldset input {flex: 99; width: calc(100% - 130px);} - #lists fieldset {flex: 1} - textarea {height: 100%; width: 100%;} - button {width: 100%; height: 32px} - header {height: 180px;position: fixed;top:0;left:0;right: 0;} - #lists{position: fixed;top:180px;left:0;right: 0;bottom: 0;} - - h1{padding-left: 3em; margin-top: 10px;} - .logo {height: 3em; position: absolute; margin: -0.6em -2.2em;} - .g1june {height: 1.2em; vertical-align: text-top;} - - a {color: #666; text-decoration-style: dotted; text-decoration-color: #ccc;} - a:hover {color: #666; text-decoration-style: solid; text-decoration-color: #ccc;} - - #resultat { - position: fixed; - z-index: 2; - top: 50%; - left: 50%; - width: 1000px; - margin-left: -500px; - height: 200px; - margin-top: -100px; - background-color: #efe; - padding: 20px; - border: 2px groove grey; - } - </style> + <link href="style.css" rel="stylesheet"/> </head> <body> <header> @@ -105,7 +62,6 @@ password </style> </a> </footer> -<script src="https://paperwallet.duniter.io/lib/duniter_tools.js"></script> -<script src="main.js"></script> +<script type="module" src="main.js"></script> </body> </html> diff --git a/public/main.js b/public/main.js index 4742f2f35b6d4bf2e6bcef98668a8fb547bac7fb..4ca9724a884b2dbeee4ae0aac0f9ed33d0ac7fa3 100644 --- a/public/main.js +++ b/public/main.js @@ -1,4 +1,5 @@ const cores = navigator.hardwareConcurrency; +import * as crypto from "./crypto.js"; addEventsListeners(document.querySelectorAll("#salt, #pass"),"change keyup",updateEstimate); updateEstimate(); @@ -8,18 +9,19 @@ function updateEstimate(){ document.getElementById("combi").innerHTML = idSecList.length*passList.length; document.getElementById("temps").innerHTML = (idSecList.length*passList.length)+"s"; } -document.getElementById("compute").addEventListener("click",e=>{ +document.getElementById("compute").addEventListener("click", async e=>{ const pub = document.getElementById("pubkey").value.trim(); - const pubRaw = Base58.decode(pub); + const pubRaw = crypto.b58.decode(pub); const idSecList = multiLineString2cleanArray(document.getElementById("salt").value); const passList = multiLineString2cleanArray(document.getElementById("pass").value); + const combi = idSecList.length*passList.length; updateEstimate(); document.getElementById("percent").innerHTML = "0%"; - + let count = 0; for(let idSec of idSecList){ for (let pass of passList){ - const seed = generate_seed_from_scrypt(idSec,pass) - if(pub === short_publickey_from_seed(seed)){ + const keys = await crypto.idSecPass2cleanKeys(idSec,pass); + if(pub === keys.publicKey){ const resArea = document.getElementById("resultat"); resArea.classList.remove("pasEncore"); resArea.innerHTML = `<h2>Ǧ'trouvé ! ǦéLéClé !</h2><br/> @@ -27,43 +29,22 @@ document.getElementById("compute").addEventListener("click",e=>{ Mot de passe : ${pass}<br/> -- Avancé --<br/> Clef publique : ${pub}<br/> - Clef privée : ${privatekey_from_seed(seed)}<br/> - Graine : ${Base58.encode(seed)}<br/> + Clef privée : ${keys.secretKey}<br/> + Graine : ${keys.seed}<br/> `; document.getElementById("percent").innerHTML = "Trouvé :)"; return; } + count++; + document.getElementById("percent").innerHTML = `${Math.round(10000*count/combi)/100}%`; } } document.getElementById("percent").innerHTML = "Fini sans résultats"; }); - - -async function main(){ - console.log(short_publickey_from_seed(generate_seed_from_scrypt("a","b"))); -} -main(); - -async function crypt(password, salt){ - return new Promise( (resolve ,reject) => { - const options = { - logN: 12, - r: 16, - p: 1, - //dkLen: 32, - encoding: 'binary' - }; - scrypt(password, salt, options, (res)=>{ - resolve(res); - }); - }); - -} function addEventsListeners(triggerNodes,events,functions){ if(!triggerNodes.length) triggerNodes = [triggerNodes]; if(typeof events !== "object") events = events.split(" "); if(typeof functions !== "object") functions = [functions]; - console.log(triggerNodes[0].addEventListener, events, functions); for(let n of triggerNodes) events.forEach(e=> functions.forEach(f=>n.addEventListener(e,f))); } function multiLineString2cleanArray(rawStr){ return rawStr.split("\n").map(str=>str.trim()).filter(str=>str !== ""); } diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000000000000000000000000000000000000..ddcd11b3ee112f5b3bf5fa538f11b0acdd0bc24f --- /dev/null +++ b/public/style.css @@ -0,0 +1,42 @@ +* {font-family: sans-serif; box-sizing: border-box; margin: 0; padding: 0} +h1{font-weight: normal; width: 100%; text-align: center; line-height: 70px;} +input[type="text"],textarea{border: 0; padding: 5px;} +legend {margin-left: 10px; padding: 0 5px} +em{font-size: 70%} +.pasEncore{display: none} +fieldset, #info {margin: 10px;} +header, #lists, header fieldset{ + display: flex; + flex-wrap: wrap; +} +header fieldset {flex: 8; margin-top: 0; display: block;} +header #info {flex: 2} +header fieldset label, header fieldset input { padding: 5px;line-height: 60px; font-size: 16px;} +header fieldset label {white-space: nowrap; flex: 1;} +header fieldset input {flex: 99; width: calc(100% - 130px);} +#lists fieldset {flex: 1} +textarea {height: 100%; width: 100%;} +button {width: 100%; height: 32px} +header {height: 180px;position: fixed;top:0;left:0;right: 0;} +#lists{position: fixed;top:180px;left:0;right: 0;bottom: 0;} + +h1{padding-left: 3em; margin-top: 10px;} +.logo {height: 3em; position: absolute; margin: -0.6em -2.2em;} +.g1june {height: 1.2em; vertical-align: text-top;} + +a {color: #666; text-decoration-style: dotted; text-decoration-color: #ccc;} +a:hover {color: #666; text-decoration-style: solid; text-decoration-color: #ccc;} + +#resultat { + position: fixed; + z-index: 2; + top: 50%; + left: 50%; + width: 1000px; + margin-left: -500px; + height: 200px; + margin-top: -100px; + background-color: #efe; + padding: 20px; + border: 2px groove grey; +} diff --git a/public/test.html b/public/test.html new file mode 100644 index 0000000000000000000000000000000000000000..bec82767c065dda5a3fa8c7664bdb663851a76f0 --- /dev/null +++ b/public/test.html @@ -0,0 +1,9 @@ +<meta charset="utf-8" /> +<link rel="shortcut icon" type="image/png" href="../node_modules/jasmine-core/images/jasmine_favicon.png"> +<link rel="stylesheet" type="text/css" href="../node_modules/jasmine-core/lib/jasmine-core/jasmine.css"> + +<script type="text/javascript" src="../node_modules/jasmine-core/lib/jasmine-core/jasmine.js"></script> +<script type="text/javascript" src="../node_modules/jasmine-core/lib/jasmine-core/jasmine-html.js"></script> +<script type="text/javascript" src="../node_modules/jasmine-core/lib/jasmine-core/boot.js"></script> + +<script type="module" src="crypto.test.js"></script>