<!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 Certificates 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 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="center">
|
|
<h1>PCF Vaccination Journey (RSA)</h1>
|
|
|
|
<div class="full-div">
|
|
<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, Cambridge, ..."/></td></tr>
|
|
<tr><td>Age</td><td><input id="qr-coupon-age" type="text" placeholder="Under 65, Over 65, ...>"/></td></tr>
|
|
<tr><td>Conditions</td><td><input id="qr-coupon-conditions" type="text" placeholder="Diabetes, ...."/></td></tr>
|
|
<tr><td>Job</td><td><input id="qr-coupon-job" type="text" placeholder="Teacher, Healthworker, ..."/></td></tr>
|
|
</table>
|
|
</div>
|
|
<div class="quarter">
|
|
<h4>PassKey</h4>
|
|
<table>
|
|
<tr><td>Name</td><td><input id="qr-passkey-name" type="text" placeholder="Patient Name"/></td></tr>
|
|
<tr><td>Phone</td><td><input id="qr-passkey-phone" type="text" placeholder="617 .."/></td></tr>
|
|
<tr><td>Birthdate</td><td><input id="qr-passkey-dob" type="text" placeholder="YYYY-MM-DD"/></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, etc"/></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="Intramuscular, Intranasal, Subcut, Oral, ..."/></td></tr>
|
|
<tr><td>Site</td><td><input id="qr-badge-site" type="text" placeholder="Right Arm, Left Arm, ..."/></td></tr>
|
|
<tr><td>Dosage</td><td><input id="qr-badge-dose" type="text" placeholder="1.0ml, 0.5ml, ..."/></td></tr>
|
|
<tr><td>Doses</td><td><input id="qr-badge-required_doses" type="text" placeholder="1, 2, 3, ..."/></td></tr>
|
|
<tr><td>Next Dose</td><td><input id="qr-badge-next_dose_in_days" type="text" placeholder="21, 28, ..."/></td></tr>
|
|
<tr><td>Vaccinator</td><td><input id="qr-badge-vacinator" type="text" placeholder="Pharmacy Name, City, ..."/></td></tr>
|
|
<tr><td>PassKey</td><td><input id="qr-badge-vaccinee" type="text" placeholder="User Hash" 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>PassKey</td><td><input id="qr-status-vaccinee" type="text" placeholder="User Hash" readonly/></td></tr>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="quarter">
|
|
<h4>Credentials</h4>
|
|
<label for="privkey">Private Key</label><br/>
|
|
<textarea id="privkey" rows="10" cols="30">
|
|
-----BEGIN RSA PRIVATE KEY-----
|
|
MIICXQIBAAKBgQDlOJu6TyygqxfWT7eLtGDwajtNFOb9I5XRb6khyfD1Yt3YiCgQ
|
|
WMNW649887VGJiGr/L5i2osbl8C9+WJTeucF+S76xFxdU6jE0NQ+Z+zEdhUTooNR
|
|
aY5nZiu5PgDB0ED/ZKBUSLKL7eibMxZtMlUDHjm4gwQco1KRMDSmXSMkDwIDAQAB
|
|
AoGAfY9LpnuWK5Bs50UVep5c93SJdUi82u7yMx4iHFMc/Z2hfenfYEzu+57fI4fv
|
|
xTQ//5DbzRR/XKb8ulNv6+CHyPF31xk7YOBfkGI8qjLoq06V+FyBfDSwL8KbLyeH
|
|
m7KUZnLNQbk8yGLzB3iYKkRHlmUanQGaNMIJziWOkN+N9dECQQD0ONYRNZeuM8zd
|
|
8XJTSdcIX4a3gy3GGCJxOzv16XHxD03GW6UNLmfPwenKu+cdrQeaqEixrCejXdAF
|
|
z/7+BSMpAkEA8EaSOeP5Xr3ZrbiKzi6TGMwHMvC7HdJxaBJbVRfApFrE0/mPwmP5
|
|
rN7QwjrMY+0+AbXcm8mRQyQ1+IGEembsdwJBAN6az8Rv7QnD/YBvi52POIlRSSIM
|
|
V7SwWvSK4WSMnGb1ZBbhgdg57DXaspcwHsFV7hByQ5BvMtIduHcT14ECfcECQATe
|
|
aTgjFnqE/lQ22Rk0eGaYO80cc643BXVGafNfd9fcvwBMnk0iGX0XRsOozVt5Azil
|
|
psLBYuApa66NcVHJpCECQQDTjI2AQhFc1yRnCU/YgDnSpJVm1nASoRUnU8Jfm3Oz
|
|
uku7JUXcVpt08DFSceCEX9unCuMcT72rAQlLpdZir876
|
|
-----END RSA PRIVATE KEY-----
|
|
</textarea>
|
|
<br><br>
|
|
<label for="pubkey">Link to Public Key</label><br/>
|
|
<textarea id="qr-link" rows="1" cols="30">github.pathcheck.org/keys/rsa_pub_key</textarea>
|
|
</div>
|
|
</div>
|
|
<br>
|
|
<div class="four-quarter">
|
|
<button class="qr-btn" onclick="generateQRCodes()">Create Certificates</button>
|
|
</div>
|
|
<div class="full-div">
|
|
<div class="quarter">
|
|
<h4>Coupon</h4>
|
|
<canvas id="qr-coupon-code"></canvas><br/>
|
|
<pre id="qr-coupon-result"></pre>
|
|
<p id="qr-coupon-verified"></p>
|
|
</div>
|
|
<div class="quarter">
|
|
<h4>Passkey</h4>
|
|
<canvas id="qr-passkey-code"></canvas><br/>
|
|
<pre id="qr-passkey-result"></pre>
|
|
<p id="qr-passkey-verified"></p>
|
|
</div>
|
|
<div class="quarter">
|
|
<h4>Badge</h4>
|
|
<canvas id="qr-badge-code"></canvas><br/>
|
|
<pre id="qr-badge-result"></pre>
|
|
<p id="qr-badge-verified"></p>
|
|
</div>
|
|
<div class="quarter">
|
|
<h4>Status</h4>
|
|
<canvas id="qr-status-code"></canvas><br/>
|
|
<pre id="qr-status-result"></pre>
|
|
<p id="qr-status-verified"></p>
|
|
</div>
|
|
<div class="quarter">
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="js/qrious.min.js"></script>
|
|
<script src="js/jsencrypt.min.js"></script>
|
|
<script src="js/sha256.js"></script>
|
|
<script>
|
|
function e(elem) { return document.getElementById(elem); }
|
|
|
|
function addIfExists(prefix, elemId) {
|
|
let value = encodeURIComponent(e(elemId).value);
|
|
return value ? prefix+value : "";
|
|
}
|
|
|
|
function verify(pubkey, message, signature, feedback_elem_id) {
|
|
// Download pubkey to verify
|
|
var client = new XMLHttpRequest();
|
|
client.open('GET', "https://" + pubkey);
|
|
client.addEventListener("load",
|
|
function() {
|
|
// Verify with the public key...
|
|
var verify = new JSEncrypt();
|
|
verify.setPublicKey(this.responseText);
|
|
var verified = verify.verify(message, signature, CryptoJS.SHA256);
|
|
e(feedback_elem_id).innerHTML = "Verified: " + verified;
|
|
}
|
|
);
|
|
client.send();
|
|
}
|
|
|
|
function signAndDisplayQR(elemPref, protocol, crypto, prikey, pubkey, message) {
|
|
var sign = new JSEncrypt();
|
|
sign.setPrivateKey(prikey);
|
|
|
|
var signature = sign.sign(message, CryptoJS.SHA256, crypto.toLowerCase());
|
|
|
|
var uri = protocol+":"+crypto+"\\"+signature+"@"+pubkey+"?"+message;
|
|
|
|
var qr = new QRious({ element: e(elemPref+'-code') });
|
|
qr.set({
|
|
foreground: '#3654DD',
|
|
size: e(elemPref+'-code').scrollWidth,
|
|
level: 'L',
|
|
value: uri
|
|
});
|
|
|
|
e(elemPref+"-result").innerHTML= "<span class='protocol'>"+protocol+"</span>:" +
|
|
"<span class='crypto-algo'>"+crypto+"</span>\\" +
|
|
"<span class='signature'>" + signature + "</span>" + "@" +
|
|
"<span class='pub-key'>" + pubkey + "</span>" + "?" +
|
|
"<span class='message'>" + message + "</span>";
|
|
e(elemPref+"-verified").innerHTML = "Verified: false";
|
|
|
|
verify(pubkey, message, signature, elemPref + "-verified");
|
|
}
|
|
|
|
function generateQRCodes() {
|
|
gtag('event', 'generateQR');
|
|
|
|
var pubkey = e("qr-link").value.trim().replace("http://","");
|
|
var prikey = e('privkey').value;
|
|
|
|
var messageCoupon = "type=coupon";
|
|
messageCoupon += addIfExists("&id=", "qr-coupon-id");
|
|
messageCoupon += addIfExists("&coupons=", "qr-coupon-coupons");
|
|
messageCoupon += addIfExists("&phase=", "qr-coupon-phase");
|
|
messageCoupon += addIfExists("&city=", "qr-coupon-city");
|
|
messageCoupon += addIfExists("&age=", "qr-coupon-age");
|
|
messageCoupon += addIfExists("&conditions=", "qr-coupon-conditions");
|
|
messageCoupon += addIfExists("&job=", "qr-coupon-job");
|
|
|
|
signAndDisplayQR("qr-coupon", "healthpass", "SHA256", prikey, pubkey, messageCoupon);
|
|
|
|
// PassKey
|
|
var messagePassKey = "type=passkey";
|
|
messagePassKey += addIfExists("&name=", "qr-passkey-name");
|
|
messagePassKey += addIfExists("&phone=", "qr-passkey-phone");
|
|
messagePassKey += addIfExists("&dob=", "qr-passkey-dob");
|
|
messagePassKey += addIfExists("&salt=", "qr-passkey-salt");
|
|
|
|
signAndDisplayQR("qr-passkey", "healthpass", "SHA256", prikey, pubkey, messagePassKey);
|
|
|
|
var hashPassKey = CryptoJS.SHA256(messagePassKey);
|
|
e("qr-status-vaccinee").value = hashPassKey;
|
|
e("qr-badge-vaccinee").value = hashPassKey;
|
|
|
|
// Badge
|
|
var messageBadge = "type=badge";
|
|
messageBadge += "&date="+e("qr-badge-date").value;
|
|
messageBadge += addIfExists("&vaccinee=", "qr-badge-vaccinee");
|
|
messageBadge += addIfExists("&vaccinator=", "qr-badge-vacinator");
|
|
messageBadge += addIfExists("&manuf=", "qr-badge-manuf");
|
|
messageBadge += addIfExists("&name=", "qr-badge-product");
|
|
messageBadge += addIfExists("&lot=", "qr-badge-lot");
|
|
messageBadge += addIfExists("&route=", "qr-badge-route");
|
|
messageBadge += addIfExists("&site=", "qr-badge-site");
|
|
messageBadge += addIfExists("&dose=", "qr-badge-dose");
|
|
messageBadge += addIfExists("&required_doses=", "qr-badge-required_doses");
|
|
messageBadge += addIfExists("&next_dose_in_days=", "qr-badge-next_dose_in_days");
|
|
|
|
signAndDisplayQR("qr-badge", "healthpass", "SHA256", prikey, pubkey, messageBadge);
|
|
|
|
// Status
|
|
var messageStatus = "type=status";
|
|
messageStatus += addIfExists("&vaccinated=", "qr-status-vaccinated");
|
|
messageStatus += addIfExists("&vaccinee=", "qr-status-vaccinee");
|
|
|
|
signAndDisplayQR("qr-status", "healthpass", "SHA256", prikey, pubkey, messageStatus);
|
|
}
|
|
</script>
|
|
|
|
<script>
|
|
// Defaults
|
|
e("qr-badge-date").value = new Date().toJSON().slice(0, 16);
|
|
|
|
// Salt
|
|
e("qr-passkey-salt").value = Math.random().toString(36).substring(3);
|
|
|
|
function loadDemo() {
|
|
e("qr-coupon-id").value = "US MA 0001";
|
|
e("qr-coupon-coupons").value = 5000;
|
|
e("qr-coupon-phase").value = "1A";
|
|
e("qr-coupon-city").value = "Somerville";
|
|
e("qr-coupon-age").value = "<65";
|
|
e("qr-coupon-conditions").value = "Chronic";
|
|
e("qr-coupon-job").value = "HealthWorkers, Teachers";
|
|
|
|
e("qr-passkey-name").value = "John Doe";
|
|
e("qr-passkey-phone").value = "+1 617 333 2345";
|
|
e("qr-passkey-dob").value = "1950-04-22";
|
|
|
|
e("qr-badge-vacinator").value = "CVS Minute Clinic";
|
|
e("qr-badge-manuf").value = "Moderna";
|
|
e("qr-badge-product").value = "COVID-19";
|
|
e("qr-badge-lot").value = "012L20A";
|
|
e("qr-badge-route").value = "IM";
|
|
e("qr-badge-site").value = "RA";
|
|
e("qr-badge-dose").value = ".5ml";
|
|
e("qr-badge-required_doses").value = "2";
|
|
e("qr-badge-next_dose_in_days").value = "28";
|
|
|
|
e("qr-status-vaccinated").value = "1";
|
|
}
|
|
|
|
loadDemo();
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|
|
|