Skip to content

Instantly share code, notes, and snippets.

@NeilMadden
Created November 19, 2019 12:32
Show Gist options
  • Save NeilMadden/70e1b232a3b273de02ed731eb36ec4a7 to your computer and use it in GitHub Desktop.
Save NeilMadden/70e1b232a3b273de02ed731eb36ec4a7 to your computer and use it in GitHub Desktop.
Sketch ECDH DPoP implementation using WebCrypto
const crypto = window.crypto.subtle;
const UTF8 = new TextEncoder('utf-8');
function genKeyPair() {
return crypto.generateKey({
name: "ECDH",
namedCurve: "P-256"
}, false, ['deriveKey', 'deriveBits']);
}
const keyPair= genKeyPair();
function deriveHmacKey(challenge, accessToken, origin) {
const rawEpk = JSON.parse(window.atob(challenge));
console.log('Challenge epk = ', rawEpk);
const importedEpk = crypto.importKey(
'jwk', rawEpk, {name: 'ECDH', namedCurve: 'P-256'}, true, []);
return keyPair.then(keys =>
importedEpk.then(epk =>
crypto.deriveBits( {name: 'ECDH', public: epk}, keys.privateKey, 256)))
.then(bits => crypto.digest('SHA-256', kdfContext(bits, accessToken, origin)))
.then(bits => crypto.importKey('raw', bits, {name:'HMAC',hash:'SHA-256'}, true, ['sign', 'verify']));
}
function kdfContext(sharedSecret, apu, apv) {
const alg = 'HS256';
const buffer = new ArrayBuffer(4 + sharedSecret.byteLength + 4 + alg.length + 4 + apu.length + 4 + apv.length + 4);
const byteArray = new Uint8Array(buffer);
const dataView = new DataView(buffer);
var offset = 0;
dataView.setUint32(offset, 1, false); // Iteration count (always 1)
offset += 4;
byteArray.subarray(offset, offset + sharedSecret.byteLength).set(sharedSecret);
offset += sharedSecret.byteLength;
dataView.setUint32(offset, alg.length, false);
offset += 4;
byteArray.subarray(offset, offset + alg.length).set(UTF8.encode(alg));
offset += alg.length;
dataView.setUint32(offset, apu.length, false);
offset += 4;
byteArray.subarray(offset, offset + apu.length).set(UTF8.encode(apu));
offset += apu.length;
dataView.setUint32(offset, apv.length, false);
offset += 4;
byteArray.subarray(offset, offset + apv.length).set(UTF8.encode(apv));
offset += apv.length;
dataView.setUint32(offset, 256, false); // keydatalen
return buffer;
}
function base64url(data) {
let b64 = btoa(String.fromCharCode(...new Uint8Array(data)));
return b64.replace(/[\+]/g, '-').replace(/[\/]/g, '_').replace(/[=]/g, '');
}
const challenge = 'eyJrdHkiOiJFQyIsImtpZCI6Ik1FRUNBUUF3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRUp6QWxBZ0VCQkNBTmpMVndkREpmX1ZGMmhxajZSdFM5Tkk4Y0ZqVjF4SWNqR3NYZi1EQmNidyIsIngiOiJ0NHk1aE1kQjZjbEp3dTNLRTRfREtsVS1udEphZDROamNoRFl6ZERMVWlFIiwieSI6Im84eFFXZFRHTDJ3Y0lCenZVUERoWlpYei0wampVdGVGQnlRYTVHMm5jRlUiLCJjcnYiOiJQLTI1NiJ9';
const accessToken = "some_access_token";
const origin = "https://api.example.com:443";
deriveHmacKey(challenge, accessToken, origin)
.then(key => {
const kid = JSON.parse(atob(challenge)).kid;
const header = JSON.stringify({typ:"dpop+jwt",alg:"HS256","kid":kid});
const claims = '{"htm":"POST","htu":"https://api.example.com/foo"}';
const prefix = base64url(UTF8.encode(header)) + '.' + base64url(UTF8.encode(claims));
return crypto.sign('HMAC', key, UTF8.encode(prefix))
.then(sig => prefix + '.' + base64url(sig));
})
.then(jwt => console.log(jwt));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment