Add hash utilities + tests

This commit is contained in:
Yarmo Mackenbach 2022-09-28 13:08:12 +02:00
parent 23df3a9d00
commit 769fd3232a
No known key found for this signature in database
GPG key ID: 37367F4AF4087AD1
11 changed files with 322 additions and 10 deletions

View file

@ -31,6 +31,9 @@ import express from 'express'
const router = express.Router() const router = express.Router()
router.get('/', function(req, res) {
res.render('util/index')
})
router.get('/profile-url', function(req, res) { router.get('/profile-url', function(req, res) {
res.render('util/profile-url') res.render('util/profile-url')
}) })
@ -59,4 +62,18 @@ router.get('/wkd/:input', function(req, res) {
res.render('util/wkd', { input: req.params.input }) res.render('util/wkd', { input: req.params.input })
}) })
router.get('/argon2', function(req, res) {
res.render('util/argon2')
})
router.get('/argon2/:input', function(req, res) {
res.render('util/argon2', { input: req.params.input })
})
router.get('/bcrypt', function(req, res) {
res.render('util/bcrypt')
})
router.get('/bcrypt/:input', function(req, res) {
res.render('util/bcrypt', { input: req.params.input })
})
export default router export default router

View file

@ -92,6 +92,13 @@ body {
display: block; display: block;
width: 100% !important; width: 100% !important;
} }
.half-width {
display: block;
width: 50% !important;
}
.select-all {
user-select: all;
}
/* LAYOUT */ /* LAYOUT */
header { header {
@ -401,7 +408,8 @@ pre code {
form input[type="text"], form input[type="search"] { form input[type="text"], form input[type="search"] {
margin: 8px 0; margin: 8px 0;
padding: 4px; padding: 4px;
border: 1px solid #444; border: 1px solid #999;
border-radius: 3px;
font-size: 0.9rem; font-size: 0.9rem;
} }
form textarea { form textarea {
@ -410,7 +418,7 @@ form textarea {
margin: 8px 0; margin: 8px 0;
resize: vertical; resize: vertical;
font-size: 0.9rem; font-size: 0.9rem;
border: 1px solid #444; border: 1px solid #999;
} }
.button-wrapper { .button-wrapper {
display: flex; display: flex;

View file

@ -43,6 +43,12 @@ const elUtilQRFP = document.body.querySelector("#form-util-qrfp")
const elUtilQR = document.body.querySelector("#form-util-qr") const elUtilQR = document.body.querySelector("#form-util-qr")
const elUtilProfileURL = document.body.querySelector("#form-util-profile-url") const elUtilProfileURL = document.body.querySelector("#form-util-profile-url")
const elUtilArgon2Generation = document.body.querySelector("#form-util-argon2-generate")
const elUtilArgon2Verification = document.body.querySelector("#form-util-argon2-verify")
const elUtilBcryptGeneration = document.body.querySelector("#form-util-bcrypt-generate")
const elUtilBcryptVerification = document.body.querySelector("#form-util-bcrypt-verify")
// Initialize UI elements and event listeners // Initialize UI elements and event listeners
export function init() { export function init() {
// Register modals // Register modals
@ -84,6 +90,22 @@ export function init() {
if (elUtilProfileURL) { if (elUtilProfileURL) {
runProfileURLUtility() runProfileURLUtility()
} }
if (elUtilArgon2Generation) {
runArgon2GenerationUtility()
}
if (elUtilArgon2Verification) {
runArgon2VerificationUtility()
}
if (elUtilBcryptGeneration) {
runBcryptGenerationUtility()
}
if (elUtilBcryptVerification) {
runBcryptVerificationUtility()
}
} }
const runEncryptionForm = () => { const runEncryptionForm = () => {
@ -314,5 +336,85 @@ const runProfileURLUtility = () => {
elOutput.innerText = await utils.generateProfileURL(data); elOutput.innerText = await utils.generateProfileURL(data);
}); });
elInput.dispatchEvent(new Event("input"));
}
const runArgon2GenerationUtility = () => {
elUtilArgon2Generation.onsubmit = function (evt) {
evt.preventDefault();
}
const elInput = elUtilArgon2Generation.querySelector(".input"),
elOutput = elUtilArgon2Generation.querySelector(".output");
elInput.addEventListener("input", async function(evt) {
elOutput.innerText = await utils.generateArgon2Hash(elInput.value);
});
elInput.dispatchEvent(new Event("input"));
}
const runArgon2VerificationUtility = () => {
elUtilArgon2Verification.onsubmit = function (evt) {
evt.preventDefault();
}
const elInput = elUtilArgon2Verification.querySelector(".input"),
elHash = elUtilArgon2Verification.querySelector(".hash"),
elOutput = elUtilArgon2Verification.querySelector(".output");
const onInput = async function(evt) {
if (elInput.value && elHash.value) {
elOutput.innerText = await utils.verifyArgon2Hash(elInput.value, elHash.value)
? "✅ Hash matches the input"
: "❌ Hash does not match the input";
} else {
elOutput.innerText = "Waiting for input…"
}
}
elInput.addEventListener("input", onInput);
elHash.addEventListener("input", onInput);
elInput.dispatchEvent(new Event("input"));
}
const runBcryptGenerationUtility = () => {
elUtilBcryptGeneration.onsubmit = function (evt) {
evt.preventDefault();
}
const elInput = elUtilBcryptGeneration.querySelector(".input"),
elOutput = elUtilBcryptGeneration.querySelector(".output");
elInput.addEventListener("input", async function(evt) {
elOutput.innerText = await utils.generateBcryptHash(elInput.value);
});
elInput.dispatchEvent(new Event("input"));
}
const runBcryptVerificationUtility = () => {
elUtilBcryptVerification.onsubmit = function (evt) {
evt.preventDefault();
}
const elInput = elUtilBcryptVerification.querySelector(".input"),
elHash = elUtilBcryptVerification.querySelector(".hash"),
elOutput = elUtilBcryptVerification.querySelector(".output");
const onInput = async function(evt) {
if (elInput.value && elHash.value) {
elOutput.innerText = await utils.verifyBcryptHash(elInput.value, elHash.value)
? "✅ Hash matches the input"
: "❌ Hash does not match the input";
} else {
elOutput.innerText = "Waiting for input…"
}
}
elInput.addEventListener("input", onInput);
elHash.addEventListener("input", onInput);
elInput.dispatchEvent(new Event("input")); elInput.dispatchEvent(new Event("input"));
} }

View file

@ -29,6 +29,7 @@ more information on this, and how to apply and follow the GNU AGPL, see <https:/
*/ */
import * as openpgp from 'openpgp' import * as openpgp from 'openpgp'
import QRCode from 'qrcode' import QRCode from 'qrcode'
import { argon2id, argon2Verify, bcrypt, bcryptVerify } from 'hash-wasm'
let _crypto = (typeof window === 'undefined') ? null : crypto let _crypto = (typeof window === 'undefined') ? null : crypto
// Compute local part of Web Key Directory URL // Compute local part of Web Key Directory URL
@ -47,7 +48,7 @@ export async function generateProfileURL(data) {
let hostname = data.hostname || window.location.hostname; let hostname = data.hostname || window.location.hostname;
if (data.input == "") { if (data.input == "") {
return "Waiting for input..."; return "Waiting for input";
} }
switch (data.source) { switch (data.source) {
case "wkd": case "wkd":
@ -170,4 +171,73 @@ export function encodeZBase32(data) {
result += ALPHABET[MASK & (buffer >> bitsLeft)]; result += ALPHABET[MASK & (buffer >> bitsLeft)];
} }
return result; return result;
}
// Generate Argon2 hash
export async function generateArgon2Hash(input) {
if (!_crypto) {
_crypto = (await import('crypto')).webcrypto
}
const salt = new Uint8Array(16);
_crypto.getRandomValues(salt);
try {
return await argon2id({
password: input,
salt,
parallelism: 2,
iterations: 512,
memorySize: 64,
hashLength: 16,
outputType: 'encoded',
});
} catch (_) {
return "Waiting for input…";
}
}
// Verify Argon2 hash
export async function verifyArgon2Hash(input, hash) {
try {
return await argon2Verify({
password: input,
hash: hash,
});
} catch (_) {
return false;
}
}
// Generate bcrypt hash
export async function generateBcryptHash(input) {
if (!_crypto) {
_crypto = (await import('crypto')).webcrypto
}
const salt = new Uint8Array(16);
_crypto.getRandomValues(salt);
try {
return await bcrypt({
password: input,
salt,
costFactor: 11,
outputType: 'encoded',
});
} catch (_) {
return "Waiting for input…";
}
}
// Verify bcrypt hash
export async function verifyBcryptHash(input, hash) {
try {
return await bcryptVerify({
password: input,
hash: hash,
});
} catch (_) {
return false;
}
} }

View file

@ -25,6 +25,46 @@ describe('browser', function () {
local.should.equal('iffe93qcsgp4c8ncbb378rxjo6cn9q6u') local.should.equal('iffe93qcsgp4c8ncbb378rxjo6cn9q6u')
}) })
}) })
describe('generateArgon2Hash()', async function () {
it('should generate an Argon2 hash', async function () {
const hash = await utils.generateArgon2Hash("123")
hash.should.match(/\$argon2id\$(?:.*)/)
})
})
describe('verifyArgon2Hash()', async function () {
it('should verify a valid Argon2 hash', async function () {
const hash = await utils.verifyArgon2Hash(
"123",
"$argon2id$v=19$m=64,t=512,p=2$2ZmmBcXDEMl6M1Bz6fvgEw$WPni+yUmwLYny1JSHcjKOQ")
hash.should.be.true
})
it('should reject an invalid Argon2 hash', async function () {
const hash = await utils.verifyArgon2Hash(
"321",
"$argon2id$v=19$m=64,t=512,p=2$2ZmmBcXDEMl6M1Bz6fvgEw$WPni+yUmwLYny1JSHcjKOQ")
hash.should.be.false
})
})
describe('generateBcryptHash()', async function () {
it('should generate a bcrypt hash', async function () {
const hash = await utils.generateBcryptHash("123")
hash.should.match(/\$2a\$(?:.*)/)
})
})
describe('verifyBcryptHash()', async function () {
it('should verify a valid bcrypt hash', async function () {
const hash = await utils.verifyBcryptHash(
"123",
"$2a$11$yi5BcfAMmDZNbIvIeaRxzOjRCJ.GPWoKBRwGCf8iK7pYrVwiDaQdC")
hash.should.be.true
})
it('should reject an invalid bcrypt hash', async function () {
const hash = await utils.verifyBcryptHash(
"321",
"$2a$11$yi5BcfAMmDZNbIvIeaRxzOjRCJ.GPWoKBRwGCf8iK7pYrVwiDaQdC")
hash.should.be.false
})
})
describe('generateProfileURL()', function () { describe('generateProfileURL()', function () {
it('should handle a WKD URL', async function () { it('should handle a WKD URL', async function () {
const local = await utils.generateProfileURL({ const local = await utils.generateProfileURL({

View file

@ -5,6 +5,8 @@ footer
h1 Keyoxide h1 Keyoxide
a(href="/") Homepage a(href="/") Homepage
br br
a(href="/util") Utilities
br
a(href="/privacy") Privacy policy a(href="/privacy") Privacy policy
div div

29
views/util/argon2.pug Normal file
View file

@ -0,0 +1,29 @@
extends ../templates/base.pug
block content
section.narrow
h1 Argon2 utility
h2 Generate Argon2 hash
form#form-util-argon2-generate(method='post')
p
| This tool generates
a(href='https://en.wikipedia.org/wiki/Argon2') Argon2
| hashes useful to
a(href='https://docs.keyoxide.org/understanding-keyoxide/identity-proof-formats/#Hashed_URI') conceal identity proofs
| . Be sure to include "openpgp4fpr:" for a valid proof!
h3 Input
input.input.half-width(type='text' name='input' placeholder='openpgp4fpr:…' value=input)
h3 Hash
pre
code.output.full-width.select-all Waiting for input…
h2 Verify Argon2 hash
form#form-util-argon2-verify(method='post')
h3 Input
input.input.half-width(type='text' name='input' placeholder='openpgp4fpr:…' value=input)
h3 Hash
input.hash.half-width(type='text' name='hash' placeholder='$argon2…')
h3 Output
pre
code.output.full-width Waiting for input…

29
views/util/bcrypt.pug Normal file
View file

@ -0,0 +1,29 @@
extends ../templates/base.pug
block content
section.narrow
h1 bcrypt utility
h2 Generate bcrypt hash
form#form-util-bcrypt-generate(method='post')
p
| This tool generates
a(href='https://en.wikipedia.org/wiki/Bcrypt') bcrypt
| hashes useful to
a(href='https://docs.keyoxide.org/understanding-keyoxide/identity-proof-formats/#Hashed_URI') conceal identity proofs
| . Be sure to include "openpgp4fpr:" for a valid proof!
h3 Input
input.input(type='text' name='input' placeholder='openpgp4fpr:…' value=input)
h3 Hash
pre
code.output.full-width.select-all Waiting for input…
h2 Verify bcrypt hash
form#form-util-bcrypt-verify(method='post')
h3 Input
input.input(type='text' name='input' placeholder='openpgp4fpr:…' value=input)
h3 Hash
input.hash(type='text' name='hash' placeholder='$2a$…')
h3 Output
pre
code.output.full-width Waiting for input…

15
views/util/index.pug Normal file
View file

@ -0,0 +1,15 @@
extends ../templates/base.pug
block content
section.narrow
h1 Keyoxide utilities
p
a(href="/util/profile-url") Get the URL for a Keyoxide profile
p
a(href="/util/wkd") Get the URLs for WKD keys
p
a(href="/util/qrfp") Generate the QR code for fingerprints
p
a(href="/util/argon2") Generate and verify Argon2 hashes
p
a(href="/util/bcrypt") Generate and verify bcrypt hashes

View file

@ -12,10 +12,10 @@ block content
option(value='hkp') keys.openpgp.org option(value='hkp') keys.openpgp.org
option(value='keybase') Keybase option(value='keybase') Keybase
br br
input#input(type='text' name='input' placeholder='Input' value='') input#input.half-width(type='text' name='input' placeholder='Input' value='')
h3 Profile URL h3 Profile URL
pre pre
code#output Waiting for input... code#output Waiting for input
h3 Help h3 Help
p p
| When using the | When using the

View file

@ -2,11 +2,11 @@ extends ../templates/base.pug
block content block content
section.narrow section.narrow
h1 Web Key Directory generator h1 Web Key Directory URL generator
form#form-util-wkd(method='post') form#form-util-wkd(method='post')
p p
| This tool computes the part of the WKD URL that corresponds to the username when | This tool computes the part of the WKD URL that corresponds to the username when
a(href='/guides/web-key-directory') uploading keys using web key directory a(href='https://docs.keyoxide.org/web-key-directory') uploading keys using web key directory
| . | .
p p
| If you enter the entire WKD identifier (username@domain.org), this tool will also generate the complete URLs. | If you enter the entire WKD identifier (username@domain.org), this tool will also generate the complete URLs.
@ -17,10 +17,10 @@ block content
h3 Output h3 Output
h4 Local part h4 Local part
pre pre
code#output.full-width Waiting for input... code#output.full-width Waiting for input
h4 Direct URL h4 Direct URL
pre pre
code#output_url_direct.full-width Waiting for input... code#output_url_direct.full-width Waiting for input
h4 Advanced URL h4 Advanced URL
pre pre
code#output_url_advanced.full-width Waiting for input... code#output_url_advanced.full-width Waiting for input