<!doctype html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<link rel="stylesheet" href="css/style.v2.css">
|
|
<link rel="stylesheet" href="css/topnav.css">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<link rel="shortcut icon" href="https://www.pathcheck.org/hubfs/Favicon.png">
|
|
<title>Vaccine Distribution Credentials Generator</title>
|
|
|
|
<!-- Global site tag (gtag.js) - Google Analytics -->
|
|
<script async src="https://www.googletagmanager.com/gtag/js?id=G-DTDMHW3NV6"></script>
|
|
<script>
|
|
window.dataLayer = window.dataLayer || [];
|
|
function gtag(){dataLayer.push(arguments);}
|
|
gtag('js', new Date());
|
|
gtag('config', 'G-DTDMHW3NV6');
|
|
</script>
|
|
</head>
|
|
<body>
|
|
<div class="topnav">
|
|
<div class="topnavContainer">
|
|
<a class="active" href="index.html">Signers</a>
|
|
<a href="verify.html"><span class="xs-hidden">Universal </span>Verifier</a>
|
|
<a class="xs-hidden" href="debug.html">QR Debugger</a>
|
|
<a class="xs-hidden" href="https://github.com/Path-Check/paper-cred-demo">Source Code</a>
|
|
<a href="https://github.com/Path-Check/paper-cred"><span class="xs-hidden"> QR </span>Specs</a>
|
|
<a href="http://vaccine-docs.pathcheck.org"><span class="xs-hidden">Vaccine </span>Docs</a>
|
|
<a class="xs-hidden" href="http://pathcheck.org">About PathCheck</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="subnav">
|
|
<div class="subnavContainer">
|
|
<a class="active" href="index.v5.html">PCF<span class="xs-hidden">'s 4 QRs</span></a>
|
|
<a href="eu.dgc.html">EU<span class="xs-hidden"> Green Pass</span></a>
|
|
<a href="icao.html">ICAO<span class="xs-hidden"> Seals</span></a>
|
|
<a href="cowin.html">DIVOC<span class="xs-hidden">/India</span></a>
|
|
<a href="liberty.html">IBM/NY<span class="xs-hidden"> Excelsior</span></a>
|
|
<a class="xs-hidden" href="opencerta.html"><span class="xs-hidden">Open</span>Certa</a>
|
|
<a href="vial.html">Vial<span class="xs-hidden"> Label</span></a>
|
|
<a href="us.ma.id.html"><span class="xs-hidden">Mass </span>ID</a>
|
|
<a href="banknote.html">Cash<span class="xs-hidden"> Bills</span></a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="center">
|
|
<h1>PCF Vaccination Journey</h1>
|
|
<div class="full-div">
|
|
<div class="quarter">
|
|
<h4>Eligibility Coupon</h4>
|
|
<table>
|
|
<tr><td>ID</td><td><input id="qr-coupon-id" type="text" placeholder="2"/></td></tr>
|
|
<tr><td>Coupons</td><td><input id="qr-coupon-coupons" type="text" placeholder="5000"/></td></tr>
|
|
<tr><td>Phase</td><td><input id="qr-coupon-phase" type="text" placeholder="1A, 2A, ..."/></td></tr>
|
|
<tr><td>City</td><td><input id="qr-coupon-city" type="text" placeholder="Somerville, MA, US..."/></td></tr>
|
|
<tr><td>Limits</td><td><input id="qr-coupon-indicator" type="text" placeholder="Diabetes, Teacher, Healthworker, Under 65, Over 65,...."/></td></tr>
|
|
</table>
|
|
</div>
|
|
<div class="quarter">
|
|
<h4>PassKey (Identity)</h4>
|
|
<table>
|
|
<tr><td>Name</td><td><input id="qr-passkey-name" type="text" placeholder="Your Full Name ..."/></td></tr>
|
|
<tr><td>Phone #</td><td><input id="qr-passkey-phone" type="text" placeholder="1617 .."/></td></tr>
|
|
<tr><td>Birthdate</td><td><input id="qr-passkey-dob" type="text" placeholder="YYYYMMDD"/></td></tr>
|
|
<tr><td>Salt</td><td><input id="qr-passkey-salt" type="text" placeholder="2342342"/></td></tr>
|
|
</table>
|
|
</div>
|
|
<div class="quarter">
|
|
<h4>Badge (Immunization)</h4>
|
|
<table>
|
|
<tr><td>Date</td><td><input id="qr-badge-date" type="text" placeholder=""/></td></tr>
|
|
<tr><td>Manuf</td><td><input id="qr-badge-manuf" type="text" placeholder="Pfizer, Moderna, ..."/></td></tr>
|
|
<tr><td>Product</td><td><input id="qr-badge-product" type="text" placeholder="COVID-19"/></td></tr>
|
|
<tr><td>Lot#</td><td><input id="qr-badge-lot" type="text" placeholder="012L20A, ..."/></td></tr>
|
|
<tr><td>Route</td><td><input id="qr-badge-route" type="text" placeholder="C38238, C38276, ..."/></td></tr>
|
|
<tr><td>Site</td><td><input id="qr-badge-site" type="text" placeholder="LA, RA, ..."/></td></tr>
|
|
<tr><td>Dosage</td><td><input id="qr-badge-dose" type="text" placeholder="1000, 500, ..."/></td></tr>
|
|
<tr><td>Boosts</td><td><input id="qr-badge-required_doses" type="text" placeholder="28, 21, 0, ..."/></td></tr>
|
|
<tr><td>Private?</td><td><input id="qr-badge-private" type="checkbox"></td></tr>
|
|
<tr id="qr-badge-line-passkey" style="display: none;"><td>Pass #</td><td><input id="qr-badge-vaccinee" type="text" placeholder="User Hash" readonly/></td></tr>
|
|
<tr id="qr-badge-line-passkey2" style="display: none; visibility: hidden;" ><td>Pass #</td><td><input id="qr-badge-vaccinee2" type="text" placeholder="User Hash" readonly/></td></tr>
|
|
<tr id="qr-badge-line-name"><td>Name</td><td><input id="qr-badge-name" type="text" placeholder="Patient Name" readonly/></td></tr>
|
|
<tr id="qr-badge-line-dob"><td>DoB</td><td><input id="qr-badge-dob" type="text" placeholder="YYYYMMDD" readonly/></td></tr>
|
|
</table>
|
|
</div>
|
|
<div class="quarter">
|
|
<h4>Status (Health Pass)</h4>
|
|
<table>
|
|
<tr><td>Doses</td><td><input id="qr-status-vaccinated" type="text" placeholder="0, 1, 2"/></td></tr>
|
|
<tr><td>Private?</td><td><input id="qr-status-private" type="checkbox"></td></tr>
|
|
<tr id="qr-status-line-passkey" style="display: none;"><td>Pass #</td><td><input id="qr-status-vaccinee" type="text" placeholder="User Hash" readonly/></td></tr>
|
|
<tr id="qr-status-line-name"><td>Holder</td><td><input id="qr-status-vaccinee-red" type="text" placeholder="FLYY" readonly/></td></tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="quarter">
|
|
<h4>Credentials</h4>
|
|
<label for="privkey">Private Key</label><br/>
|
|
<textarea id="privkey" rows="10" style="width: 100%;">-----BEGIN EC PARAMETERS-----
|
|
BgUrgQQACg==
|
|
-----END EC PARAMETERS-----
|
|
-----BEGIN EC PRIVATE KEY-----
|
|
MHQCAQEEIPWKbSezZMY1gCpvN42yaVv76Lo47FvSsVZpQl0a5lWRoAcGBSuBBAAK
|
|
oUQDQgAE6DeIun4EgMBLUmbtjQw7DilMJ82YIvOR2jz/IK0R/F7/zXY1z+gqvFXf
|
|
DcJqR5clbAYlO9lHmvb4lsPLZHjugQ==
|
|
-----END EC PRIVATE KEY-----</textarea>
|
|
<br><br>
|
|
<label for="pubkey">Public Key</label><br/>
|
|
<textarea id="qr-link" rows="1" cols="30">1A9.PCF.PW</textarea>
|
|
</div>
|
|
</div>
|
|
<br>
|
|
<div class="four-quarter">
|
|
<button class="qr-btn" onclick="generateQRCodes()">Create Certificates</button>
|
|
<br>
|
|
</div>
|
|
<div class="full-div">
|
|
<div class="quarter">
|
|
<h4 id="qr-coupon-code-label" style="display: none;">Coupon QR</h4>
|
|
<canvas id="qr-coupon-code"></canvas><br/>
|
|
<h4 id="qr-coupon-pdf-label" style="display: none;">PDF 417 Format</h4>
|
|
<canvas id="qr-coupon-pdf"></canvas><br/>
|
|
<pre id="qr-coupon-result"></pre>
|
|
<pre id="qr-coupon-bytes" class="xs-hidden"></pre>
|
|
</div>
|
|
<div class="quarter">
|
|
<h4 id="qr-passkey-code-label" style="display: none;">PassKey QR</h4>
|
|
<canvas id="qr-passkey-code"></canvas><br/>
|
|
<h4 id="qr-passkey-pdf-label" style="display: none;">PDF 417 Format</h4>
|
|
<canvas id="qr-passkey-pdf"></canvas><br/>
|
|
<pre id="qr-passkey-result"></pre>
|
|
<pre id="qr-passkey-bytes" class="xs-hidden"></pre>
|
|
</div>
|
|
<div class="quarter">
|
|
<h4 id="qr-badge-code-label" style="display: none;">Badge QR</h4>
|
|
<canvas id="qr-badge-code"></canvas><br/>
|
|
<h4 id="qr-badge-pdf-label" style="display: none;">PDF 417 Format</h4>
|
|
<canvas id="qr-badge-pdf"></canvas><br/>
|
|
<pre id="qr-badge-result"></pre>
|
|
<pre id="qr-badge-bytes" class="xs-hidden"></pre>
|
|
</div>
|
|
<div class="quarter">
|
|
<h4 id="qr-status-code-label" style="display: none;">Status QR</h4>
|
|
<canvas id="qr-status-code"></canvas><br/>
|
|
<h4 id="qr-status-pdf-label" style="display: none;">PDF 417 Format</h4>
|
|
<canvas id="qr-status-pdf"></canvas><br/>
|
|
<pre id="qr-status-result"></pre>
|
|
<pre id="qr-status-bytes" class="xs-hidden"></pre>
|
|
</div>
|
|
<div class="quarter xs-hidden">
|
|
<label for="verify">Verify a QR Code</label>
|
|
<textarea id="qr-verify" rows="10" cols="33" placeholder="cred:type:version:signature:pubkey:payload"></textarea>
|
|
<br><br>
|
|
<button class="qr-btn" onclick="verifyQRCode()">Verify</button>
|
|
<br><br>
|
|
<pre id="qr-verify-result"></pre>
|
|
<pre id="qr-verify-verified"></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="js/qrcode.min.js"></script>
|
|
|
|
<script src="js/libbcmath.js" type="text/javascript"></script>
|
|
<script src="js/bcmath.js" type="text/javascript"></script>
|
|
<script src="js/pdf417.js" type="text/javascript"></script>
|
|
|
|
<script src="js/pcf.sdk.min.js"></script>
|
|
|
|
<script src="js/pcf-utils.js"></script>
|
|
<script src="js/ui-utils.js"></script>
|
|
|
|
<script>
|
|
function e(elem) { return document.getElementById(elem); }
|
|
|
|
function getValueArray(elemArray) {
|
|
const fields = elemArray.map(function(elemId) {
|
|
return e(elemId).value;
|
|
})
|
|
return fields;
|
|
}
|
|
|
|
function clearQR(elemPrefix) {
|
|
e(elemPrefix+'-code').getContext('2d').clearRect(0, 0, e(elemPrefix+'-code').width, e(elemPrefix+'-code').height);
|
|
e(elemPrefix+'-result').innerHTML = "";
|
|
e(elemPrefix+'-bytes').innerHTML = "";
|
|
}
|
|
|
|
async function clear() {
|
|
clearQR('qr-status');
|
|
clearQR('qr-passkey');
|
|
clearQR('qr-badge');
|
|
clearQR('qr-coupon');
|
|
}
|
|
|
|
function signAndDisplayQR(elemPref, _type, _version, priKeyPEM, pubKeyId, payloadValueArray) {
|
|
PCF.signAndPack(_type, _version, priKeyPEM, pubKeyId, payloadValueArray).then(uri => {
|
|
PCFUtils.debugURI(uri).then( debugInfo => {
|
|
UIUtils.drawsQR(elemPref, uri, debugInfo);
|
|
|
|
PCF.unpackAndVerify(uri).then(result => {
|
|
UIUtils.drawVerifiedSymbol(elemPref+'-code',result);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
async function generateQRCodes() {
|
|
gtag('event', 'generateQR');
|
|
|
|
clear();
|
|
|
|
// Where to Download the public key
|
|
const pubKeyLink = e("qr-link").value.trim().replace("http://","");
|
|
|
|
// PEM code of the private key
|
|
const priKeyPEM = e('privkey').value;
|
|
|
|
// Build PassKeyHash
|
|
const hashPassKeyBase32 = await PCF.hashPayload(getValueArray(["qr-passkey-name", "qr-passkey-dob", "qr-passkey-salt", "qr-passkey-phone"]));
|
|
|
|
if (e("qr-badge-private").checked) {
|
|
// Update fields on Screen
|
|
e("qr-badge-vaccinee").value = hashPassKeyBase32;
|
|
e("qr-badge-name").value = "";
|
|
e("qr-badge-dob").value = "";
|
|
} else {
|
|
e("qr-badge-vaccinee").value = "";
|
|
e("qr-badge-name").value = e("qr-passkey-name").value;
|
|
e("qr-badge-dob").value = e("qr-passkey-dob").value;
|
|
}
|
|
|
|
if (e("qr-status-private").checked) {
|
|
e("qr-status-vaccinee").value = hashPassKeyBase32;
|
|
e("qr-status-vaccinee-red").value = ""
|
|
} else {
|
|
e("qr-status-vaccinee").value = ""
|
|
e("qr-status-vaccinee-red").value = getInitialsAndYear(e("qr-passkey-name").value, e('qr-passkey-dob').value);
|
|
}
|
|
|
|
// Coupon QR
|
|
const couponArray = getValueArray(["qr-coupon-id", "qr-coupon-coupons", "qr-coupon-city", "qr-coupon-phase", "qr-coupon-indicator"]);
|
|
signAndDisplayQR("qr-coupon", "coupon","1", priKeyPEM, pubKeyLink, couponArray);
|
|
|
|
// PassKey QR
|
|
const passKeyArray = getValueArray(["qr-passkey-name", "qr-passkey-dob", "qr-passkey-salt", "qr-passkey-phone"]);
|
|
signAndDisplayQR("qr-passkey", "passkey","1", priKeyPEM, pubKeyLink, passKeyArray);
|
|
|
|
// Badge QR
|
|
const badgeArray = getValueArray(["qr-badge-date", "qr-badge-manuf", "qr-badge-product", "qr-badge-lot",
|
|
"qr-badge-required_doses", "qr-badge-vaccinee", "qr-badge-route",
|
|
"qr-badge-site", "qr-badge-dose", "qr-badge-name", "qr-badge-dob"]);
|
|
signAndDisplayQR("qr-badge", "badge","2", priKeyPEM, pubKeyLink, badgeArray);
|
|
|
|
// Status QR
|
|
const statusArray = getValueArray(["qr-status-vaccinated", "qr-status-vaccinee", "qr-status-vaccinee-red"]);
|
|
signAndDisplayQR("qr-status", "status","2", priKeyPEM, pubKeyLink, statusArray);
|
|
}
|
|
|
|
function verifyQRCode() {
|
|
gtag('event', 'verifyQR');
|
|
|
|
PCFUtils.debugParseURI(e("qr-verify").value).then(result => {
|
|
e("qr-verify-result").innerHTML = result;
|
|
});
|
|
PCFUtils.debugVerify(e("qr-verify").value).then(debug => {
|
|
e('qr-verify-verified').innerHTML = debug;
|
|
});
|
|
}
|
|
|
|
function getInitialsAndYear(fullName, dob) {
|
|
var names = fullName.split(' '),
|
|
initials = names[0].substring(0, 1).toUpperCase();
|
|
|
|
if (names.length > 1) {
|
|
initials += names[names.length - 1].substring(0, 1).toUpperCase();
|
|
}
|
|
|
|
return initials + dob.substring(2,4);
|
|
}
|
|
|
|
function onScanSuccess(qrMessage) {
|
|
e("qr-verify").value = qrMessage;
|
|
verifyQRCode();
|
|
}
|
|
|
|
function onScanFailure(error) {
|
|
// handle scan failure, usually better to ignore and keep scanning
|
|
console.warn(`QR error = ${error}`);
|
|
}
|
|
</script>
|
|
|
|
<script>
|
|
e("qr-badge-private").addEventListener('change', () => {
|
|
e("qr-badge-line-name").style.display = e("qr-badge-private").checked ? 'none': '';
|
|
e("qr-badge-line-dob").style.display = e("qr-badge-private").checked ? 'none': '';
|
|
e("qr-badge-line-passkey").style.display = e("qr-badge-private").checked ? '' :'none';
|
|
e("qr-badge-line-passkey2").style.display = e("qr-badge-private").checked ? '' :'none';
|
|
|
|
if (e("qr-badge-private").checked) {
|
|
e("qr-badge-name").value = "";
|
|
e("qr-badge-dob").value = "";
|
|
} else {
|
|
e("qr-badge-vaccinee").value = "";
|
|
}
|
|
});
|
|
|
|
e("qr-status-private").addEventListener('change', () => {
|
|
e("qr-status-line-name").style.display = e("qr-status-private").checked ? 'none': '';
|
|
e("qr-status-line-passkey").style.display = e("qr-status-private").checked ? '' :'none';
|
|
|
|
if (e("qr-status-private").checked) {
|
|
e("qr-status-vaccinee-red").value = "";
|
|
} else {
|
|
e("qr-status-vaccinee").value = "";
|
|
}
|
|
});
|
|
|
|
// Defaults
|
|
e("qr-badge-date").value = new Date().toJSON().slice(0, 10).replaceAll("-","");
|
|
|
|
// Salt
|
|
e("qr-passkey-salt").value = Math.random().toString(36).substring(3);
|
|
|
|
function loadDemo() {
|
|
// Salt
|
|
e("qr-passkey-salt").value = Math.random().toString(36).substring(3);
|
|
|
|
e("qr-badge-name").value = "";
|
|
e("qr-badge-dob").value = "";
|
|
e("qr-badge-vaccinee").value = "";
|
|
e("qr-status-vaccinee-red").value = "";
|
|
|
|
e("qr-coupon-id").value = "1";
|
|
e("qr-coupon-coupons").value = 5000;
|
|
e("qr-coupon-phase").value = "1A";
|
|
e("qr-coupon-city").value = "Somerville MA US";
|
|
e("qr-coupon-indicator").value = ">65";
|
|
|
|
e("qr-passkey-name").value = "Jane Doe";
|
|
e("qr-passkey-phone").value = "16173332345";
|
|
e("qr-passkey-dob").value = "19820321";
|
|
|
|
e("qr-badge-manuf").value = "Moderna";
|
|
e("qr-badge-product").value = "COVID19";
|
|
e("qr-badge-lot").value = "012L20A";
|
|
e("qr-badge-route").value = "C28161";
|
|
e("qr-badge-site").value = "RA";
|
|
e("qr-badge-dose").value = "500";
|
|
e("qr-badge-required_doses").value = "28";
|
|
|
|
e("qr-status-vaccinated").value = "1";
|
|
e("qr-status-vaccinee").value = "";
|
|
}
|
|
|
|
loadDemo();
|
|
</script>
|
|
|
|
<script>
|
|
async function preloadKey() {
|
|
PCF.resolveKey(e("qr-link").value);
|
|
}
|
|
window.onload = function() {
|
|
preloadKey();
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|
|
|