forked from Mirrors/keyoxide-web
Add hash utilities + tests
This commit is contained in:
parent
23df3a9d00
commit
769fd3232a
11 changed files with 322 additions and 10 deletions
|
@ -31,6 +31,9 @@ import express from 'express'
|
|||
|
||||
const router = express.Router()
|
||||
|
||||
router.get('/', function(req, res) {
|
||||
res.render('util/index')
|
||||
})
|
||||
router.get('/profile-url', function(req, res) {
|
||||
res.render('util/profile-url')
|
||||
})
|
||||
|
@ -59,4 +62,18 @@ router.get('/wkd/:input', function(req, res) {
|
|||
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
|
||||
|
|
|
@ -92,6 +92,13 @@ body {
|
|||
display: block;
|
||||
width: 100% !important;
|
||||
}
|
||||
.half-width {
|
||||
display: block;
|
||||
width: 50% !important;
|
||||
}
|
||||
.select-all {
|
||||
user-select: all;
|
||||
}
|
||||
|
||||
/* LAYOUT */
|
||||
header {
|
||||
|
@ -401,7 +408,8 @@ pre code {
|
|||
form input[type="text"], form input[type="search"] {
|
||||
margin: 8px 0;
|
||||
padding: 4px;
|
||||
border: 1px solid #444;
|
||||
border: 1px solid #999;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
form textarea {
|
||||
|
@ -410,7 +418,7 @@ form textarea {
|
|||
margin: 8px 0;
|
||||
resize: vertical;
|
||||
font-size: 0.9rem;
|
||||
border: 1px solid #444;
|
||||
border: 1px solid #999;
|
||||
}
|
||||
.button-wrapper {
|
||||
display: flex;
|
||||
|
|
102
static-src/ui.js
102
static-src/ui.js
|
@ -43,6 +43,12 @@ const elUtilQRFP = document.body.querySelector("#form-util-qrfp")
|
|||
const elUtilQR = document.body.querySelector("#form-util-qr")
|
||||
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
|
||||
export function init() {
|
||||
// Register modals
|
||||
|
@ -84,6 +90,22 @@ export function init() {
|
|||
if (elUtilProfileURL) {
|
||||
runProfileURLUtility()
|
||||
}
|
||||
|
||||
if (elUtilArgon2Generation) {
|
||||
runArgon2GenerationUtility()
|
||||
}
|
||||
|
||||
if (elUtilArgon2Verification) {
|
||||
runArgon2VerificationUtility()
|
||||
}
|
||||
|
||||
if (elUtilBcryptGeneration) {
|
||||
runBcryptGenerationUtility()
|
||||
}
|
||||
|
||||
if (elUtilBcryptVerification) {
|
||||
runBcryptVerificationUtility()
|
||||
}
|
||||
}
|
||||
|
||||
const runEncryptionForm = () => {
|
||||
|
@ -314,5 +336,85 @@ const runProfileURLUtility = () => {
|
|||
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"));
|
||||
}
|
|
@ -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 QRCode from 'qrcode'
|
||||
import { argon2id, argon2Verify, bcrypt, bcryptVerify } from 'hash-wasm'
|
||||
let _crypto = (typeof window === 'undefined') ? null : crypto
|
||||
|
||||
// Compute local part of Web Key Directory URL
|
||||
|
@ -47,7 +48,7 @@ export async function generateProfileURL(data) {
|
|||
let hostname = data.hostname || window.location.hostname;
|
||||
|
||||
if (data.input == "") {
|
||||
return "Waiting for input...";
|
||||
return "Waiting for input…";
|
||||
}
|
||||
switch (data.source) {
|
||||
case "wkd":
|
||||
|
@ -170,4 +171,73 @@ export function encodeZBase32(data) {
|
|||
result += ALPHABET[MASK & (buffer >> bitsLeft)];
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -25,6 +25,46 @@ describe('browser', function () {
|
|||
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 () {
|
||||
it('should handle a WKD URL', async function () {
|
||||
const local = await utils.generateProfileURL({
|
||||
|
|
|
@ -5,6 +5,8 @@ footer
|
|||
h1 Keyoxide
|
||||
a(href="/") Homepage
|
||||
br
|
||||
a(href="/util") Utilities
|
||||
br
|
||||
a(href="/privacy") Privacy policy
|
||||
|
||||
div
|
||||
|
|
29
views/util/argon2.pug
Normal file
29
views/util/argon2.pug
Normal 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
29
views/util/bcrypt.pug
Normal 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
15
views/util/index.pug
Normal 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
|
|
@ -12,10 +12,10 @@ block content
|
|||
option(value='hkp') keys.openpgp.org
|
||||
option(value='keybase') Keybase
|
||||
br
|
||||
input#input(type='text' name='input' placeholder='Input' value='')
|
||||
input#input.half-width(type='text' name='input' placeholder='Input' value='')
|
||||
h3 Profile URL
|
||||
pre
|
||||
code#output Waiting for input...
|
||||
code#output Waiting for input…
|
||||
h3 Help
|
||||
p
|
||||
| When using the
|
||||
|
|
|
@ -2,11 +2,11 @@ extends ../templates/base.pug
|
|||
|
||||
block content
|
||||
section.narrow
|
||||
h1 Web Key Directory generator
|
||||
h1 Web Key Directory URL generator
|
||||
form#form-util-wkd(method='post')
|
||||
p
|
||||
| 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
|
||||
| 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
|
||||
h4 Local part
|
||||
pre
|
||||
code#output.full-width Waiting for input...
|
||||
code#output.full-width Waiting for input…
|
||||
h4 Direct URL
|
||||
pre
|
||||
code#output_url_direct.full-width Waiting for input...
|
||||
code#output_url_direct.full-width Waiting for input…
|
||||
h4 Advanced URL
|
||||
pre
|
||||
code#output_url_advanced.full-width Waiting for input...
|
||||
code#output_url_advanced.full-width Waiting for input…
|
||||
|
|
Loading…
Reference in a new issue