mirror of
https://codeberg.org/keyoxide/keyoxide-web.git
synced 2024-12-22 23:09:29 -07:00
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()
|
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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
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 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 = () => {
|
||||||
|
@ -316,3 +338,83 @@ const runProfileURLUtility = () => {
|
||||||
|
|
||||||
elInput.dispatchEvent(new Event("input"));
|
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 * 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":
|
||||||
|
@ -171,3 +172,72 @@ export function encodeZBase32(data) {
|
||||||
}
|
}
|
||||||
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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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({
|
||||||
|
|
|
@ -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
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='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
|
||||||
|
|
|
@ -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…
|
||||||
|
|
Loading…
Reference in a new issue