Skip to content

Commit

Permalink
url safe encoding base64
Browse files Browse the repository at this point in the history
  • Loading branch information
peintnermax committed Jun 6, 2023
1 parent ebca7b6 commit 058b706
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 94 deletions.
63 changes: 63 additions & 0 deletions internal/api/ui/login/static/resources/scripts/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
export function coerceToBase64Url(thing, name) {
// Array or ArrayBuffer to Uint8Array
if (Array.isArray(thing)) {
thing = Uint8Array.from(thing);
}

if (thing instanceof ArrayBuffer) {
thing = new Uint8Array(thing);
}

// Uint8Array to base64
if (thing instanceof Uint8Array) {
var str = "";
var len = thing.byteLength;

for (var i = 0; i < len; i++) {
str += String.fromCharCode(thing[i]);
}
thing = window.btoa(str);
}

if (typeof thing !== "string") {
throw new Error("could not coerce '" + name + "' to string");
}

// base64 to base64url
// NOTE: "=" at the end of challenge is optional, strip it off here
thing = thing.replace(/\+/g, "-").replace(/\//g, "_").replace(/=*$/g, "");

return thing;
}

export function coerceToArrayBuffer(thing, name) {
if (typeof thing === "string") {
// base64url to base64
thing = thing.replace(/-/g, "+").replace(/_/g, "/");

// base64 to Uint8Array
var str = window.atob(thing);
var bytes = new Uint8Array(str.length);
for (var i = 0; i < str.length; i++) {
bytes[i] = str.charCodeAt(i);
}
thing = bytes;
}

// Array to Uint8Array
if (Array.isArray(thing)) {
thing = new Uint8Array(thing);
}

// Uint8Array to ArrayBuffer
if (thing instanceof Uint8Array) {
thing = thing.buffer;
}

// error if none of the above worked
if (!(thing instanceof ArrayBuffer)) {
throw new TypeError("could not coerce '" + name + "' to ArrayBuffer");
}

return thing;
}
39 changes: 18 additions & 21 deletions internal/api/ui/login/static/resources/scripts/webauthn.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,28 @@
function checkWebauthnSupported(button, func) {
let support = document.getElementsByClassName("wa-support");
let noSupport = document.getElementsByClassName("wa-no-support");
if (!window.PublicKeyCredential) {
for (let item of noSupport) {
item.classList.remove('hidden');
}
for (let item of support) {
item.classList.add('hidden');
}
return;
let support = document.getElementsByClassName("wa-support");
let noSupport = document.getElementsByClassName("wa-no-support");
if (!window.PublicKeyCredential) {
for (let item of noSupport) {
item.classList.remove("hidden");
}
document.getElementById(button).addEventListener('click', func);
for (let item of support) {
item.classList.add("hidden");
}
return;
}
document.getElementById(button).addEventListener("click", func);
}

function webauthnError(error) {
let err = document.getElementById('wa-error');
err.getElementsByClassName('cause')[0].innerText = error.message;
err.classList.remove('hidden');
let err = document.getElementById("wa-error");
err.getElementsByClassName("cause")[0].innerText = error.message;
err.classList.remove("hidden");
}

function bufferDecode(value) {
return decode(value);
function bufferDecode(value, name) {
return coerceToArrayBuffer(value, name);
}

function bufferEncode(value) {
return encode(value)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
function bufferEncode(value, name) {
return coerceToBase64Url(value, name);
}
77 changes: 45 additions & 32 deletions internal/api/ui/login/static/resources/scripts/webauthn_login.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,54 @@
document.addEventListener('DOMContentLoaded', checkWebauthnSupported('btn-login', login));
document.addEventListener(
"DOMContentLoaded",
checkWebauthnSupported("btn-login", login)
);

function login() {
document.getElementById('wa-error').classList.add('hidden');
document.getElementById("wa-error").classList.add("hidden");

let makeAssertionOptions = JSON.parse(atob(document.getElementsByName('credentialAssertionData')[0].value));
makeAssertionOptions.publicKey.challenge = bufferDecode(makeAssertionOptions.publicKey.challenge);
makeAssertionOptions.publicKey.allowCredentials.forEach(function (listItem) {
listItem.id = bufferDecode(listItem.id)
});
navigator.credentials.get({
publicKey: makeAssertionOptions.publicKey
}).then(function (credential) {
verifyAssertion(credential);
}).catch(function (err) {
webauthnError(err);
let makeAssertionOptions = JSON.parse(
atob(document.getElementsByName("credentialAssertionData")[0].value)
);
makeAssertionOptions.publicKey.challenge = bufferDecode(
makeAssertionOptions.publicKey.challenge,
"publicKey.challenge"
);
makeAssertionOptions.publicKey.allowCredentials.forEach(function (listItem) {
listItem.id = bufferDecode(listItem.id, "publicKey.allowCredentials.id");
});
navigator.credentials
.get({
publicKey: makeAssertionOptions.publicKey,
})
.then(function (credential) {
verifyAssertion(credential);
})
.catch(function (err) {
webauthnError(err);
});
}

function verifyAssertion(assertedCredential) {
let authData = new Uint8Array(assertedCredential.response.authenticatorData);
let clientDataJSON = new Uint8Array(assertedCredential.response.clientDataJSON);
let rawId = new Uint8Array(assertedCredential.rawId);
let sig = new Uint8Array(assertedCredential.response.signature);
let userHandle = new Uint8Array(assertedCredential.response.userHandle);
let authData = new Uint8Array(assertedCredential.response.authenticatorData);
let clientDataJSON = new Uint8Array(
assertedCredential.response.clientDataJSON
);
let rawId = new Uint8Array(assertedCredential.rawId);
let sig = new Uint8Array(assertedCredential.response.signature);
let userHandle = new Uint8Array(assertedCredential.response.userHandle);

let data = JSON.stringify({
id: assertedCredential.id,
rawId: bufferEncode(rawId),
type: assertedCredential.type,
response: {
authenticatorData: bufferEncode(authData),
clientDataJSON: bufferEncode(clientDataJSON),
signature: bufferEncode(sig),
userHandle: bufferEncode(userHandle),
},
})
let data = JSON.stringify({
id: assertedCredential.id,
rawId: bufferEncode(rawId),
type: assertedCredential.type,
response: {
authenticatorData: bufferEncode(authData),
clientDataJSON: bufferEncode(clientDataJSON),
signature: bufferEncode(sig),
userHandle: bufferEncode(userHandle),
},
});

document.getElementsByName('credentialData')[0].value = btoa(data);
document.getElementsByTagName('form')[0].submit();
}
document.getElementsByName("credentialData")[0].value = btoa(data);
document.getElementsByTagName("form")[0].submit();
}
83 changes: 51 additions & 32 deletions internal/api/ui/login/static/resources/scripts/webauthn_register.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,61 @@
document.addEventListener('DOMContentLoaded', checkWebauthnSupported('btn-register', registerCredential));
document.addEventListener(
"DOMContentLoaded",
checkWebauthnSupported("btn-register", registerCredential)
);

function registerCredential() {
document.getElementById('wa-error').classList.add('hidden');
document.getElementById("wa-error").classList.add("hidden");

let opt = JSON.parse(atob(document.getElementsByName('credentialCreationData')[0].value));
opt.publicKey.challenge = bufferDecode(opt.publicKey.challenge);
opt.publicKey.user.id = bufferDecode(opt.publicKey.user.id);
if (opt.publicKey.excludeCredentials) {
for (let i = 0; i < opt.publicKey.excludeCredentials.length; i++) {
if (opt.publicKey.excludeCredentials[i].id !== null) {
opt.publicKey.excludeCredentials[i].id = bufferDecode(opt.publicKey.excludeCredentials[i].id);
}
}
let opt = JSON.parse(
atob(document.getElementsByName("credentialCreationData")[0].value)
);
opt.publicKey.challenge = bufferDecode(
opt.publicKey.challenge,
"publicKey.challenge"
);
opt.publicKey.user.id = bufferDecode(
opt.publicKey.user.id,
"publicKey.user.id"
);
if (opt.publicKey.excludeCredentials) {
for (let i = 0; i < opt.publicKey.excludeCredentials.length; i++) {
if (opt.publicKey.excludeCredentials[i].id !== null) {
opt.publicKey.excludeCredentials[i].id = bufferDecode(
opt.publicKey.excludeCredentials[i].id,
"publicKey.excludeCredentials"
);
}
}
navigator.credentials.create({
publicKey: opt.publicKey
}).then(function (credential) {
createCredential(credential);
}).catch(function (err) {
webauthnError(err);
}
navigator.credentials
.create({
publicKey: opt.publicKey,
})
.then(function (credential) {
createCredential(credential);
})
.catch(function (err) {
webauthnError(err);
});
}

function createCredential(newCredential) {
let attestationObject = new Uint8Array(newCredential.response.attestationObject);
let clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
let rawId = new Uint8Array(newCredential.rawId);
let attestationObject = new Uint8Array(
newCredential.response.attestationObject
);
let clientDataJSON = new Uint8Array(newCredential.response.clientDataJSON);
let rawId = new Uint8Array(newCredential.rawId);

let data = JSON.stringify({
id: newCredential.id,
rawId: bufferEncode(rawId),
type: newCredential.type,
response: {
attestationObject: bufferEncode(attestationObject),
clientDataJSON: bufferEncode(clientDataJSON),
},
});
let data = JSON.stringify({
id: newCredential.id,
rawId: bufferEncode(rawId),
type: newCredential.type,
response: {
attestationObject: bufferEncode(attestationObject),
clientDataJSON: bufferEncode(clientDataJSON),
},
});

document.getElementsByName('credentialData')[0].value = btoa(data);
document.getElementsByTagName('form')[0].submit();
}
document.getElementsByName("credentialData")[0].value = btoa(data);
document.getElementsByTagName("form")[0].submit();
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion internal/api/ui/login/static/templates/mfa_init_u2f.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ <h1>{{t "InitMFAU2F.Title"}}</h1>
</div>
</form>

<script src="{{ resourceUrl "scripts/base64.js" }}"></script>
<script src="{{ resourceUrl "scripts/utils.js" }}"></script>
<script src="{{ resourceUrl "scripts/webauthn.js" }}"></script>
<script src="{{ resourceUrl "scripts/webauthn_register.js" }}"></script>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ <h1>{{t "VerifyMFAU2F.Title"}}</h1>
{{ end }}
</form>

<script src="{{ resourceUrl "scripts/base64.js" }}"></script>
<script src="{{ resourceUrl "scripts/utils.js" }}"></script>
<script src="{{ resourceUrl "scripts/webauthn.js" }}"></script>
<script src="{{ resourceUrl "scripts/webauthn_login.js" }}"></script>

Expand Down
2 changes: 1 addition & 1 deletion internal/api/ui/login/static/templates/passwordless.html
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ <h1>{{t "Passwordless.Title"}}</h1>
</div>
</form>

<script src="{{ resourceUrl "scripts/base64.js" }}"></script>
<script src="{{ resourceUrl "scripts/utils.js" }}"></script>
<script src="{{ resourceUrl "scripts/webauthn.js" }}"></script>
<script src="{{ resourceUrl "scripts/webauthn_login.js" }}"></script>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ <h1>{{t "PasswordlessRegistration.Title"}}</h1>
</div>
</form>

<script src="{{ resourceUrl "scripts/base64.js" }}"></script>
<script src="{{ resourceUrl "scripts/utils.js" }}"></script>
<script src="{{ resourceUrl "scripts/webauthn.js" }}"></script>
<script src="{{ resourceUrl "scripts/webauthn_register.js" }}"></script>

Expand Down

0 comments on commit 058b706

Please sign in to comment.