+
+
+
+
+
+
+
+
Bitwarden Captcha Connector
+
+
+
+
+
+

+
Captcha Required
+
+
+
+
+
+
diff --git a/src/connectors/captcha-mobile.scss b/src/connectors/captcha-mobile.scss
new file mode 100644
index 00000000000..a4c7f9b25b7
--- /dev/null
+++ b/src/connectors/captcha-mobile.scss
@@ -0,0 +1 @@
+@import "../scss/styles.scss";
diff --git a/src/connectors/captcha.html b/src/connectors/captcha.html
index 16aa5bdb63a..56795ec9a6c 100644
--- a/src/connectors/captcha.html
+++ b/src/connectors/captcha.html
@@ -3,8 +3,10 @@
+
+
+
Bitwarden Captcha Connector
-
diff --git a/src/connectors/captcha.ts b/src/connectors/captcha.ts
index 810189fc420..b625e2d796b 100644
--- a/src/connectors/captcha.ts
+++ b/src/connectors/captcha.ts
@@ -1,9 +1,14 @@
-import { getQsParam } from './common';
+import { b64Decode, getQsParam } from './common';
declare var hcaptcha: any;
-// tslint:disable-next-line
-require('./captcha.scss');
+if (window.location.pathname.includes('mobile')) {
+ // tslint:disable-next-line
+ require('./captcha-mobile.scss');
+} else {
+ // tslint:disable-next-line
+ require('./captcha.scss');
+}
document.addEventListener('DOMContentLoaded', () => {
init();
@@ -14,15 +19,15 @@ document.addEventListener('DOMContentLoaded', () => {
let parentUrl: string = null;
let parentOrigin: string = null;
+let callbackUri: string = null;
let sentSuccess = false;
-function init() {
- start();
+async function init() {
+ await start();
onMessage();
- info('ready');
}
-function start() {
+async function start() {
sentSuccess = false;
const data = getQsParam('data');
@@ -40,15 +45,49 @@ function start() {
parentOrigin = new URL(parentUrl).origin;
}
- hcaptcha.render('captcha', {
- sitekey: 'bc38c8a2-5311-4e8c-9dfc-49e99f6df417',
- callback: 'captchaSuccess',
- 'error-callback': 'captchaError',
+ let decodedData: any;
+ try {
+ decodedData = JSON.parse(b64Decode(data));
+ }
+ catch (e) {
+ error('Cannot parse data.');
+ return;
+ }
+ callbackUri = decodedData.callbackUri;
+
+ let src = 'https://hcaptcha.com/1/api.js?render=explicit';
+
+ // Set language code
+ if (decodedData.locale) {
+ src += `&hl=${decodedData.locale ?? 'en'}`;
+ }
+
+ // Set captchaRequired subtitle for mobile
+ const subtitleEl = document.getElementById('captchaRequired');
+ if (decodedData.captchaRequiredText && subtitleEl) {
+ subtitleEl.textContent = decodedData.captchaRequiredText;
+ }
+
+ const script = document.createElement('script');
+ script.src = src;
+ script.async = true;
+ script.defer = true;
+ script.addEventListener('load', e => {
+ hcaptcha.render('captcha', {
+ sitekey: decodedData.siteKey,
+ callback: 'captchaSuccess',
+ 'error-callback': 'captchaError',
+ });
+ watchHeight();
});
+ document.head.appendChild(script);
}
function captchaSuccess(response: string) {
success(response);
+ if (callbackUri) {
+ document.location.replace(callbackUri + '?token=' + encodeURIComponent(response));
+ }
}
function captchaError() {
@@ -79,7 +118,24 @@ function success(data: string) {
sentSuccess = true;
}
-function info(message: string) {
- parent.postMessage('info|' + message, parentUrl);
+function info(message: string | object) {
+ parent.postMessage('info|' + JSON.stringify(message), parentUrl);
+}
+
+async function watchHeight() {
+ const imagesDiv = document.body.lastChild as HTMLElement;
+ while (true) {
+ info({
+ height: imagesDiv.style.visibility === 'hidden' ?
+ document.documentElement.offsetHeight :
+ document.documentElement.scrollHeight,
+ width: document.documentElement.scrollWidth,
+ });
+ await sleep(100);
+ }
+}
+
+async function sleep(ms: number) {
+ await new Promise(r => setTimeout(r, ms));
}
diff --git a/src/connectors/common-webauthn.ts b/src/connectors/common-webauthn.ts
index 907357fd539..93a6e3ccfbf 100644
--- a/src/connectors/common-webauthn.ts
+++ b/src/connectors/common-webauthn.ts
@@ -21,12 +21,6 @@ export function buildDataString(assertedCredential: PublicKeyCredential) {
return JSON.stringify(data);
}
-export function b64Decode(str: string) {
- return decodeURIComponent(Array.prototype.map.call(atob(str), (c: string) => {
- return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
- }).join(''));
-}
-
export function parseWebauthnJson(jsonString: string) {
const json = JSON.parse(jsonString);
diff --git a/src/connectors/common.ts b/src/connectors/common.ts
index 08538656959..10431c19433 100644
--- a/src/connectors/common.ts
+++ b/src/connectors/common.ts
@@ -13,3 +13,9 @@ export function getQsParam(name: string) {
return decodeURIComponent(results[2].replace(/\+/g, ' '));
}
+
+export function b64Decode(str: string) {
+ return decodeURIComponent(Array.prototype.map.call(atob(str), (c: string) => {
+ return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
+ }).join(''));
+}
diff --git a/src/connectors/webauthn-fallback.ts b/src/connectors/webauthn-fallback.ts
index 7bd19353281..65802e55906 100644
--- a/src/connectors/webauthn-fallback.ts
+++ b/src/connectors/webauthn-fallback.ts
@@ -1,18 +1,70 @@
-import { getQsParam } from './common';
-import { b64Decode, buildDataString, parseWebauthnJson } from './common-webauthn';
+import { b64Decode, getQsParam } from './common';
+import { buildDataString, parseWebauthnJson } from './common-webauthn';
// tslint:disable-next-line
require('./webauthn.scss');
+let parsed = false;
+let webauthnJson: any;
let parentUrl: string = null;
let parentOrigin: string = null;
let sentSuccess = false;
+let locale: string = 'en';
let locales: any = {};
+function parseParameters() {
+ if (parsed) {
+ return;
+ }
+
+ parentUrl = getQsParam('parent');
+ if (!parentUrl) {
+ error('No parent.');
+ return;
+ } else {
+ parentUrl = decodeURIComponent(parentUrl);
+ parentOrigin = new URL(parentUrl).origin;
+ }
+
+ locale = getQsParam('locale').replace('-', '_');
+
+ const version = getQsParam('v');
+
+ if (version === '1') {
+ parseParametersV1();
+ } else {
+ parseParametersV2();
+ }
+ parsed = true;
+}
+
+function parseParametersV1() {
+ const data = getQsParam('data');
+ if (!data) {
+ error('No data.');
+ return;
+ }
+
+ webauthnJson = b64Decode(data);
+}
+
+function parseParametersV2() {
+ let dataObj: { data: any, btnText: string; } = null;
+ try {
+ dataObj = JSON.parse(b64Decode(getQsParam('data')));
+ }
+ catch (e) {
+ error('Cannot parse data.');
+ return;
+ }
+
+ webauthnJson = dataObj.data;
+}
+
document.addEventListener('DOMContentLoaded', async () => {
- const locale = getQsParam('locale').replace('-', '_');
+ parseParameters();
try {
locales = await loadLocales(locale);
} catch {
@@ -34,8 +86,8 @@ document.addEventListener('DOMContentLoaded', async () => {
content.classList.remove('d-none');
});
-async function loadLocales(locale: string) {
- const filePath = `locales/${locale}/messages.json?cache=${process.env.CACHE_TAG}`;
+async function loadLocales(newLocale: string) {
+ const filePath = `locales/${newLocale}/messages.json?cache=${process.env.CACHE_TAG}`;
const localesResult = await fetch(filePath);
return await localesResult.json();
}
@@ -54,25 +106,15 @@ function start() {
return;
}
- const data = getQsParam('data');
- if (!data) {
+ parseParameters();
+ if (!webauthnJson) {
error('No data.');
return;
}
- parentUrl = getQsParam('parent');
- if (!parentUrl) {
- error('No parent.');
- return;
- } else {
- parentUrl = decodeURIComponent(parentUrl);
- parentOrigin = new URL(parentUrl).origin;
- }
-
let json: any;
try {
- const jsonString = b64Decode(data);
- json = parseWebauthnJson(jsonString);
+ json = parseWebauthnJson(webauthnJson);
}
catch (e) {
error('Cannot parse data.');
diff --git a/src/connectors/webauthn.ts b/src/connectors/webauthn.ts
index 79ad8c1687a..2c1af50d332 100644
--- a/src/connectors/webauthn.ts
+++ b/src/connectors/webauthn.ts
@@ -1,43 +1,37 @@
-import { getQsParam } from './common';
-import { b64Decode, buildDataString, parseWebauthnJson } from './common-webauthn';
+import { b64Decode, getQsParam } from './common';
+import { buildDataString, parseWebauthnJson } from './common-webauthn';
// tslint:disable-next-line
require('./webauthn.scss');
-document.addEventListener('DOMContentLoaded', () => {
- init();
-
- const text = getQsParam('btnText');
- if (text) {
- const button = document.getElementById('webauthn-button');
- button.innerText = decodeURI(text);
- button.onclick = executeWebAuthn;
- }
-});
-
+let parsed = false;
+let webauthnJson: any;
+let btnText: string = null;
let parentUrl: string = null;
let parentOrigin: string = null;
let stopWebAuthn = false;
let sentSuccess = false;
let obj: any = null;
+document.addEventListener('DOMContentLoaded', () => {
+ init();
+
+ parseParameters();
+ if (btnText) {
+ const button = document.getElementById('webauthn-button');
+ button.innerText = decodeURI(btnText);
+ button.onclick = executeWebAuthn;
+ }
+});
+
function init() {
start();
onMessage();
info('ready');
}
-function start() {
- sentSuccess = false;
-
- if (!('credentials' in navigator)) {
- error('WebAuthn is not supported in this browser.');
- return;
- }
-
- const data = getQsParam('data');
- if (!data) {
- error('No data.');
+function parseParameters() {
+ if (parsed) {
return;
}
@@ -50,15 +44,63 @@ function start() {
parentOrigin = new URL(parentUrl).origin;
}
+ const version = getQsParam('v');
+
+ if (version === '1') {
+ parseParametersV1();
+ } else {
+ parseParametersV2();
+ }
+ parsed = true;
+}
+
+function parseParametersV1() {
+ const data = getQsParam('data');
+ if (!data) {
+ error('No data.');
+ return;
+ }
+
+ webauthnJson = b64Decode(data);
+ btnText = getQsParam('btnText');
+}
+
+function parseParametersV2() {
+ let dataObj: { data: any, btnText: string; } = null;
try {
- const jsonString = b64Decode(data);
- obj = parseWebauthnJson(jsonString);
+ dataObj = JSON.parse(b64Decode(getQsParam('data')));
}
catch (e) {
error('Cannot parse data.');
return;
}
+ webauthnJson = dataObj.data;
+ btnText = dataObj.btnText;
+}
+
+function start() {
+ sentSuccess = false;
+
+ if (!('credentials' in navigator)) {
+ error('WebAuthn is not supported in this browser.');
+ return;
+ }
+
+ parseParameters();
+ if (!webauthnJson) {
+ error('No data.');
+ return;
+ }
+
+ try {
+ obj = parseWebauthnJson(webauthnJson);
+ }
+ catch (e) {
+ error('Cannot parse webauthn data.');
+ return;
+ }
+
stopWebAuthn = false;
if (navigator.userAgent.indexOf(' Safari/') !== -1 && navigator.userAgent.indexOf('Chrome') === -1) {
diff --git a/src/scss/styles.scss b/src/scss/styles.scss
index 598fea83b30..c71abd2bf7c 100644
--- a/src/scss/styles.scss
+++ b/src/scss/styles.scss
@@ -615,6 +615,12 @@ app-user-billing {
}
}
+#hcaptcha_iframe {
+ width: 100%;
+ border: none;
+ transition: height 0.25s linear;
+}
+
#bt-dropin-container {
background: url('../images/loading.svg') 0 0 no-repeat;
min-height: 50px;
diff --git a/webpack.config.js b/webpack.config.js
index c3899731337..718cd75eb87 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -112,6 +112,11 @@ const plugins = [
filename: 'captcha-connector.html',
chunks: ['connectors/captcha'],
}),
+ new HtmlWebpackPlugin({
+ template: './src/connectors/captcha-mobile.html',
+ filename: 'captcha-mobile-connector.html',
+ chunks: ['connectors/captcha'],
+ }),
new CopyWebpackPlugin({
patterns:[
{ from: './src/.nojekyll' },