You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

412 lines
22 KiB

<!doctype html>
<head>
<link rel="stylesheet" href="css/style.v2.css">
<link rel="shortcut icon" href="https://www.pathcheck.org/hubfs/Favicon.png">
<title>Vaccine Distribution Credentials Generator</title>
</head>
<body>
<div class="center">
<div class="full-div">
<h3>Vaccine Distribution Credentials Generator (<a href="https://github.com/Path-Check/paper-cred/">Spec</a>, <a href="https://vaccine-docs.pathcheck.org/">Main Page</a>, <a href="https://github.com/Path-Check/paper-cred-demo/">Source Code</a>)</h3>
<div class="quarter">
<h4>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</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>DoB</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</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</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 <small>(openssl ecparam -name secp256k1 -genkey -out private.key)</small></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 <small>(openssl ec -in private.key -pubout -out public.key)</small></label><br/>
<label for="pubkey"><small>1. DNS TXT Record FQDN</small></label><br/>
<label for="pubkey"><small>2. Direct URL to PEM Download</small></label><br/>
<label for="pubkey"><small>3. ID.DATABASE from <a href="https://github.com/Path-Check/paper-cred/tree/benefits_limitations/keys/pcf">PCF</a></small></label><br/>
<label for="qr-link">KeyID: </label><input id="qr-link" type="text" value="1A9.PCF"></input>
<br><br>
<span class="checkboxes" ><input id="include-logos" type="checkbox"><label for="include-logos">Draw Symbols</label></span>
<h4>Read More: <a href="https://github.com/Path-Check/paper-cred/">Spec</a>, <a href="https://vaccine-docs.pathcheck.org/">Project</a>, <a href="https://github.com/Path-Check/paper-cred-demo/">Source</a></h4>
</div>
</div>
<div class="full-div">
<br><br>
<div style="margin: 0 auto; width:500px; height:50px;">
<div class="center-in-div">
<button class="qr-btn" onclick="generateQRCodes()">Create Certificates</button>
</div>
</div>
<br><br>
</div>
<div class="full-div">
<div class="quarter">
<h4 id="qr-coupon-code-label" style="display: none;">QR Format</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-verified"></pre>
<pre id="qr-coupon-bytes"></pre>
</div>
<div class="quarter">
<h4 id="qr-passkey-code-label" style="display: none;">QR Format</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-verified"></pre>
<pre id="qr-passkey-bytes"></pre>
</div>
<div class="quarter">
<h4 id="qr-badge-code-label" style="display: none;">QR Format</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-verified"></pre>
<pre id="qr-badge-bytes"></pre>
</div>
<div class="quarter">
<h4 id="qr-status-code-label" style="display: none;">QR Format</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-verified"></pre>
<pre id="qr-status-bytes"></pre>
</div>
<div class="quarter">
<label for="verify">Verify a QR Code</label>
<div id="reader" style="margin-top: 10px;margin-bottom: 10px;"></div>
<label for="verify">Or Paste the Code here:</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>cred:<span class='protocol'>type:version</span>:<span class='signature'>Signature</span>:<span class='pub-key'>PubKey</span>:<span class='message'>Payload</span></pre>
<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/elliptic.min.js"></script>
<script src="js/sha256.js"></script>
<script src="js/asn1.min.js"></script>
<script src="js/base32.min.js"></script>
<script src="js/html5-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.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+'-verified').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) {
const uri = PCF.sign(_type, _version, priKeyPEM, pubKeyId, payloadValueArray);
const [schema, type, version, signature, pubKeyLink, payload] = PCF.parseURI(uri);
const params = { margin:0, width:275, errorCorrectionLevel: 'L', color: {dark: '#3654DD' }};
// Builds QR Element
QRCode.toCanvas(e(elemPref+'-code'), uri, params, function (error) {
if (e("include-logos").checked) {
//adding a logo at center
const imgDim={width:50,height:50}; //logo dimention
var context = e(elemPref+'-code').getContext('2d');
var imageObj = new Image();
if (e(elemPref+'-private')) {
if (e(elemPref+'-private').checked)
imageObj.src = './img/closelocker.jpg';
else
imageObj.src = './img/openlocker.jpg';
} else if (elemPref === 'qr-passkey') {
imageObj.src = './img/passkey.png';
} else {
imageObj.src = './img/logo.jpg';
}
imageObj.onload = function() {
context.drawImage(imageObj,
e(elemPref+'-code').width / 2 - imgDim.width / 2 +1,
e(elemPref+'-code').height / 2 - imgDim.height / 2,imgDim.width,imgDim.height);
};
}
})
PDF417.draw(uri, e(elemPref+"-pdf"), 6, 1, 1);
e(elemPref+"-code-label").style.display = '';
e(elemPref+"-pdf-label").style.display = '';
// Updates screen elements.
e(elemPref+"-result").innerHTML= schema+":<span class='protocol'>"+type+":"+version+"</span>:" +
"<span class='signature'>" + signature + "</span>" + ":" +
"<span class='pub-key'>" + pubKeyLink + "</span>" + ":" +
"<span class='message'>" + payload + "</span><br>";
// "Signature DER: <span class='signature'>" + signatureDER + "</span><br><br>"+
// "Payload SHA256 Hash: <span class='message'>" + payloadSHA256 + "</span><br><br>";
e(elemPref+"-verified").innerHTML = "Signature: Not Verified";
//e(elemPref+"-bytes").innerHTML = "QR Size: "+ Math.round((uri.length / 2) * 11/8) + " bytes";
let qr = QRCode.create(uri, params);
let qrQ = QRCode.create(uri, { margin:0, width:275, errorCorrectionLevel: 'Q', color: {dark: '#3654DD' }});
let qrH = QRCode.create(uri, { margin:0, width:275, errorCorrectionLevel: 'H', color: {dark: '#3654DD' }});
let qrM = QRCode.create(uri, { margin:0, width:275, errorCorrectionLevel: 'M', color: {dark: '#3654DD' }});
let qrL = QRCode.create(uri, { margin:0, width:275, errorCorrectionLevel: 'L', color: {dark: '#3654DD' }});
e(elemPref+"-bytes").innerHTML = "URI in A/N (5.5bit/char): "+ Math.round(uri.length * 5.5/8) + " bytes<br>";
e(elemPref+"-bytes").innerHTML += "<br>QR Size Analysis: ";
e(elemPref+"-bytes").innerHTML += "<br>- ECC L 7% " + describe(qrL);
e(elemPref+"-bytes").innerHTML += "<br>- ECC M 15% " + describe(qrM);
e(elemPref+"-bytes").innerHTML += "<br>- ECC Q 25% " + describe(qrQ);
e(elemPref+"-bytes").innerHTML += "<br>- ECC H 30% " + describe(qrH);
e(elemPref+"-bytes").innerHTML += "<br><br>QR built with " + qr.segments.length + " segments";
for (i=0; i<qr.segments.length; i++) {
e(elemPref+"-bytes").innerHTML += "<br>- " + i + ": " + qr.segments[i].mode.id + " " + qr.segments[i].data;
}
e(elemPref + "-verified").innerHTML = PCF.debugDownloadVerify(pubKeyLink, payload, signature);
}
function describe(qr) {
return qr.modules.size + "x" + qr.modules.size + " bits " + Math.round((qr.modules.size*qr.modules.size)/8) + " bytes ";
}
function generateQRCodes() {
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 = PCF.getPayloadHash(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() {
e("qr-verify-result").innerHTML = PCF.debugParseURI(e("qr-verify").value);
e('qr-verify-verified').innerHTML = PCF.debugVerify(e("qr-verify").value);
}
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();
let html5QrcodeScanner = new Html5QrcodeScanner("reader", { fps: 10, qrbox: 150 }, /* verbose= */ true);
html5QrcodeScanner.render(onScanSuccess, onScanFailure);
</script>
<script>
async function preloadKey() {
PCF.getKeyId(e("qr-link").value);
}
window.onload = function() {
preloadKey();
}
</script>
</body>
</html>