QR Codes are very common nowdays - they are so common that they are used to attack users by replacing the real QR Code with a manipulated version so the user comes to a phising site instead of the real site.
So, why not adding a digital signature to a QR Code to be sure about its origin and that it has not been tampered with? In this article I will show you all the basic building blocks you need to do this in Blazor. And because most of the code is acutally JavaScript, you will be able to use it in any SPA as well.
TL;DR
The code is on my GitHub.
Generating QR Codes
At first we need to be able to genereat QR Codes. For this we are using a QR Code Library. I have chosen QRious. The JavaScript code to generat a QR Code is:
var qrCode;
window.qrGenerator = {
initializeQrCode: function (container) {
var containerElement = document.getElementById(container);
if (containerElement !== null && qrCode === undefined) {
qrCode = new QRious({element: containerElement, size: 300});
}
},
generateQrCode: function (data) {
// stringify json the data
const jsonData = JSON.stringify(data);
qrCode.set({value: jsonData});
},
clearQrCode: function () {
qrCode.set({value: ''});
}
}
The initialize function takes the name of a container element which must be a canvas.Then a QRious object is created on that element. To generate a QR-Code we can call the set funtion and pass a value. In the code above we pass an object to the function that we turn into JSON before setting it to the QR Code.On the Balzor end we need a way to call to the JavaScript, which is done via the IJSRuntime interface:
@page "/"
@inject IJSRuntime JS
<canvas id="qrcode" />
@code {
override protected async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("qrGenerator.initializeQrCode", "qrcode");
await JS.InvokeAsync<string>("qrGenerator.generateQrCode", "Hello World!!");
}
}
}
Adding a cryptographic signature
The code to generate a public / private key pair is not C# but JavaScript again. I was hoping to be able to use C# for this in Blazor, but it turns out that the needed APIs are not supported in Blazor. So we have to fall back to the native JavaScript crypto.subtle APIs. These are low level cryptographic APIs which involve a high risk of getting things wrong, sacrificing any security. So make sure to check this with a security expert.
The code to generate a key pair is as follows.
window.cryptoHelper = {
generateKeyPair: async function () {
const keyPair = await window.crypto.subtle.generateKey(
{
name: "RSA-OAEP",
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256"
},
true,
["encrypt", "decrypt"]
);
const publicKey = await window.crypto.subtle.exportKey("spki", keyPair.publicKey);
const privateKey = await window.crypto.subtle.exportKey("pkcs8", keyPair.privateKey);
return {
publicKey: btoa(String.fromCharCode(...new Uint8Array(publicKey))),
privateKey: btoa(String.fromCharCode(...new Uint8Array(privateKey)))
};
}
}
The generateKey funciton is used, and the first parameter is the algorithm. RSA-OAEP generates an asymetric key pair. The function can also create symmetric keys. The second parameter tells the api that the key is exportable. And the third parameter tells us what we can do with the key, which is irellevant in the code above, because the key is not used, it is just exported. This is done by the exportKey function. The public and private key are exported in suitable formats (spki and pkcs8) and returned to the calling Blazor app as base64 encoded strings.
Calling this is done in the same way as above using the IJSRuntime.
public class KeyPair
{
public string PublicKey { get; set; }
public string PrivateKey { get; set; }
}
@page "/signaturePlayground"
@inject IJSRuntime JS
code {
private async Task GenerateKeyPair(MouseEventArgs args)
{
var keyPair = await JS.InvokeAsync<KeyPair>("cryptoHelper.generateKeyPair");
}
}
For signing and validating a signature the respecive key must be imported first: for signing we need the private key, for validation the public key:
const privateKey = await window.crypto.subtle.importKey(
"pkcs8",
Uint8Array.from(atob(privateKey), c => c.charCodeAt(0)),
{
name: "RSA-PSS",
hash: { name: "SHA-256" }
},
false,
["sign"]
);
For the public key and the virification the code is basically the same, you just have to switch the format from pkcs8 to spki and the purpose of the imported key from sign to verify.
Siging is done by the sign function:
const signature = await window.crypto.subtle.sign(
{
name: "RSA-PSS",
saltLength: 32
},
privateKey,
new TextEncoder().encode(data)
);
And verification by the verify function:
const isValid = await window.crypto.subtle.verify(
{
name: "RSA-PSS",
saltLength: 32
},
publicKey,
Uint8Array.from(atob(signature), c => c.charCodeAt(0)),
new TextEncoder().encode(data)
);
Scanning a QR Code
For scanning a QR Code I choose the nimic libarary. You need to download the qr-scanner-umd.min.js and the qr-scanner-worker.min.js, the latter one is a dependency to the first one.
Starting a scan needs a video element for the QrScanner class and a callback into Balzor which is called OnQrCodeScanned.
startScan: function (dotNetObject) {
qrScanner = new QrScanner(
document.getElementById("qrScanner"),
result => {
console.log('decoded qr code:', result)
dotNetObject.invokeMethodAsync('OnQrCodeScanned', result.data);
},
{
highlightScanRegion: true,
highlightCodeOutline: true,
},
);
qrScanner.start();
}
The dotNetObject being passed to the function allows the callback to Blazor. This has to be passed in from the Balzor side:
@page "/scanQrCode"
@inject IJSRuntime JS
<video id="qrScanner"></video>
<textarea @bind="qrCodeData"></textarea>
@code {
private string qrCodeData = string.Empty;
override protected async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JS.InvokeVoidAsync("qrScanHelper.startScan", DotNetObjectReference.Create(this));
}
}
[JSInvokable]
public async Task OnQrCodeScanned(string qrCodeData)
{
this.qrCodeData = qrCodeData;
StateHasChanged();
}
}
This is achived by the DotNetObjectReference.Create method that gets a reference to this, the object representing the page. By marking a method with the [JSInvokable] attribute Blazor knows that this method is allowed to be called from JavaScript.
Just glue it together
These are all the moving parts you need to add a digital signature to your QR Code and verify that you have a real QR code. Working code for all this is on my GitHub.