diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7df52f5f..657544a6 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -116,6 +116,35 @@ build-docker-image: - /^v[0-9]*\.[0-9]*\.[0-9]*$/ +build-sbom: + except: + - schedules + stage: build + image: psono-docker.jfrog.io/ubuntu:22.04 + script: + - apt-get update && apt-get install -y curl + - sh ./var/prep-build.sh + - npx @cyclonedx/cyclonedx-npm > sbom.json + - > + if [ ! -z "$artifactory_credentials" ]; then + curl -fL https://getcli.jfrog.io | sh && + ./jfrog config add rt-server-1 --artifactory-url=https://psono.jfrog.io/psono --user=gitlab --password=$artifactory_credentials --interactive=false && + ./jfrog rt u --target-props="CI_COMMIT_REF_NAME=$CI_COMMIT_REF_NAME;CI_COMMIT_SHA=$CI_COMMIT_SHA;CI_COMMIT_URL=$CI_PROJECT_URL/commit/$CI_COMMIT_SHA;CI_PROJECT_ID=$CI_PROJECT_ID;CI_PROJECT_NAME=$CI_PROJECT_NAME;CI_PROJECT_NAMESPACE=$CI_PROJECT_NAMESPACE;CI_PROJECT_URL=$CI_PROJECT_URL;CI_PIPELINE_ID=$CI_PIPELINE_ID;CI_PIPELINE_URL=$CI_PROJECT_URL/pipelines/$CI_PIPELINE_ID;CI_COMMIT_REF_NAME=$CI_COMMIT_REF_NAME;CI_JOB_ID=$CI_JOB_ID;CI_JOB_URL=$CI_PROJECT_URL/-/jobs/$CI_JOB_ID;CI_JOB_NAME=$CI_JOB_NAME;CI_JOB_STAGE=$CI_JOB_STAGE;CI_RUNNER_ID=$CI_RUNNER_ID;GITLAB_USER_ID=$GITLAB_USER_ID;CI_SERVER_VERSION=$CI_SERVER_VERSION" /builds/psono/psono-client/sbom.json psono/client/$CI_COMMIT_REF_NAME/client-sbom.json && + ./jfrog rt sp "psono/client/$CI_COMMIT_REF_NAME/client-sbom.json" "CI_COMMIT_TAG=$CI_COMMIT_TAG" || true + fi + - mv /builds/psono/psono-client/sbom.json ../ + - rm -Rf * + - rm -Rf .* 2> /dev/null || true + - mv ../sbom.json ./ + artifacts: + name: "sbom_$CI_COMMIT_REF_NAME" + paths: + - ./* + only: + - branches@psono/psono-client + - /^v[0-9]*\.[0-9]*\.[0-9]*$/ + + build-firefox-extension: except: - schedules @@ -410,6 +439,7 @@ release-artifacts: - apt-get install -y curl - curl -fL https://getcli.jfrog.io | sh - ./jfrog config add rt-server-1 --artifactory-url=https://psono.jfrog.io/psono --user=gitlab --password=$artifactory_credentials --interactive=false + - ./jfrog rt cp --flat psono/client/$CI_COMMIT_REF_NAME/client-sbom.json psono/client/latest/ - ./jfrog rt cp --flat psono/client/$CI_COMMIT_REF_NAME/firefox-extension.zip psono/client/latest/ - ./jfrog rt cp --flat psono/client/$CI_COMMIT_REF_NAME/chrome-extension.zip psono/client/latest/ - ./jfrog rt cp --flat psono/client/$CI_COMMIT_REF_NAME/webclient.zip psono/client/latest/ @@ -584,6 +614,8 @@ deploy-nightlyartifacts: - schedules stage: release image: psono-docker.jfrog.io/ubuntu:22.04 + dependencies: + - build-sbom script: - sh ./var/deploy_nightlyartifacts.sh environment: @@ -598,6 +630,8 @@ deploy-releaseartifacts: - schedules stage: deploy image: psono-docker.jfrog.io/ubuntu:22.04 + dependencies: + - build-sbom script: - sh ./var/deploy_releaseartifacts.sh environment: diff --git a/src/common/data/js/extension/worker-content-script.js b/src/common/data/js/extension/worker-content-script.js index 6eb8b35b..a2a9e15e 100644 --- a/src/common/data/js/extension/worker-content-script.js +++ b/src/common/data/js/extension/worker-content-script.js @@ -299,7 +299,7 @@ const ClassWorkerContentScript = function (base, browser, setTimeout) { let buttonContent = otherButtons[i].tagName.toLowerCase() === "input" ? otherButtons[i].value : otherButtons[i].innerText; if (passwordSubmitButtonLabels.has(buttonContent.toLowerCase().trim())) { - submitButtons.append(otherButtons[i]); + submitButtons.push(otherButtons[i]); } } @@ -1044,6 +1044,108 @@ const ClassWorkerContentScript = function (base, browser, setTimeout) { } } + /** + * parses an URL and returns an object with all details separated + * + * @param {String} url The url to be parsed + * @returns {object} Returns the split up url + */ + function parseUrl(url) { + const empty = { + scheme: null, + authority: null, + authority_without_www: null, + base_url: null, + full_domain: null, + full_domain_without_www: null, + port: null, + path: null, + query: null, + fragment: null + }; + if (!url) { + return empty; + } + + if (!url.includes("://")) { + // Its supposed to be an url but doesn't include a schema so let's prefix it with http:// + url = 'http://' + url; + } + + let parsedUrl; + try { + parsedUrl = new URL(url); + } catch (e) { + return empty; + } + + return { + scheme: parsedUrl.protocol.slice(0,-1), + base_url: parsedUrl.protocol + '//' + parsedUrl.host, + authority: parsedUrl.host, + authority_without_www: parsedUrl.host ? parsedUrl.host.replace(/^(www\.)/, ""): parsedUrl.host, //remove leading www. + full_domain: parsedUrl.hostname, + full_domain_without_www: parsedUrl.hostname ? parsedUrl.hostname.replace(/^(www\.)/, "") : parsedUrl.hostname, + port: parsedUrl.port || null, + path: parsedUrl.pathname, + query: parsedUrl.search || null, + fragment: parsedUrl.hash ? parsedUrl.hash.substring(1) : null, + }; + } + + /** + * Checks if a provided urlfilter and authority fit together + * + * @param {string} authority The "authority" of the current website, e.g. www.example.com:80 + * @param {string} urlFilter The url filter, e.g. *.example.com or www.example.com + * + * @returns {boolean} Whether the string ends with the suffix or not + */ + function isUrlFilterMatch(authority, urlFilter) { + if (!authority || !urlFilter) { + return false + } + authority = authority.toLowerCase(); + urlFilter = urlFilter.toLowerCase(); + let directMatch = authority === urlFilter; + let wildcardMatch = urlFilter.startsWith('*.') && authority.endsWith(urlFilter.substring(1)); + + return directMatch || wildcardMatch + } + + /** + * Returns the function that returns whether a certain leaf entry should be considered a possible condidate + * for a provided url + * + * @param {string} url The url to match + * + * @returns {(function(*): (boolean|*))|*} + */ + const getSearchWebsitePasswordsByUrlfilter = function (url) { + const parsedUrl = parseUrl(url); + + const filter = function (leaf) { + + if (typeof leaf.website_password_url_filter === "undefined") { + return false; + } + + if (leaf.website_password_url_filter) { + const urlFilters = leaf.website_password_url_filter.split(/\s+|,|;/); + for (let i = 0; i < urlFilters.length; i++) { + if (!isUrlFilterMatch(parsedUrl.authority, urlFilters[i])) { + continue; + } + return parsedUrl.scheme === 'https' || (leaf.hasOwnProperty("allow_http") && leaf["allow_http"]); + } + } + + return false; + }; + + return filter; + }; + /** * handles password request answer * @@ -1051,36 +1153,61 @@ const ClassWorkerContentScript = function (base, browser, setTimeout) { * @param sender * @param sendResponse */ - function onReturnSecret(data, sender, sendResponse) { - for (let i = 0; i < myForms.length; i++) { - if ( - (myForms[i].username && myForms[i].username.isEqualNode(lastRequestElement)) || - (myForms[i].password && myForms[i].password.isEqualNode(lastRequestElement)) || - fillAll - ) { - fillFieldHelper(myForms[i].username, data.website_password_username); - fillFieldHelper(myForms[i].password, data.website_password_password); + async function onReturnSecret(data, sender, sendResponse) { - for (let ii = 0; ii < dropInstances.length; ii++) { - dropInstances[ii].close(); - } - dropInstances = []; - if (!fillAll) { - break; + const isIframe = window !== window.top; + + function autofill() { + for (let i = 0; i < myForms.length; i++) { + if ( + (myForms[i].username && myForms[i].username.isEqualNode(lastRequestElement)) || + (myForms[i].password && myForms[i].password.isEqualNode(lastRequestElement)) || + fillAll + ) { + fillFieldHelper(myForms[i].username, data.website_password_username); + fillFieldHelper(myForms[i].password, data.website_password_password); + + for (let ii = 0; ii < dropInstances.length; ii++) { + dropInstances[ii].close(); + } + dropInstances = []; + if (!fillAll) { + break; + } } } - } - if (data.hasOwnProperty('custom_fields') && data.custom_fields) { - for (let i = 0; i < data.custom_fields.length; i++) { - const field = findFieldByName(data.custom_fields[i].name); - if (field) { - fillFieldHelper(field, data.custom_fields[i].value); + if (data.hasOwnProperty('custom_fields') && data.custom_fields) { + for (let i = 0; i < data.custom_fields.length; i++) { + const field = findFieldByName(data.custom_fields[i].name); + if (field) { + fillFieldHelper(field, data.custom_fields[i].value); + } } } + + fillAll = false; } - fillAll=false; + if (isIframe) { + const filter = getSearchWebsitePasswordsByUrlfilter(window.location.origin); + if (!filter(data)) { + const parsedUrl = parseUrl(window.location.origin); + const autofillId = uuid.v4(); + await base.emit("approve-iframe-login", { + 'authority': parsedUrl.authority, + 'autofill_id': autofillId + }, (response) => { + if (response.data) { + autofill() + } + }); + } else { + autofill() + } + } else { + autofill() + } } /** diff --git a/src/common/data/translations/locale-en.json b/src/common/data/translations/locale-en.json index 22f37cd0..cbea32ad 100644 --- a/src/common/data/translations/locale-en.json +++ b/src/common/data/translations/locale-en.json @@ -1,4 +1,8 @@ { + "APPROVE_IFRAME_LOGIN": "Different origin detected", + "APPROVE_IFRAME_LOGIN_DESCRIPTION": "The form is hosted on an origin that is not in your url filter. Choose approve to autofill anyway or cancel to stop. To avoid this message from popping up again, open advanced and add {{origin}} to your url filters.", + "CONFIG_JSON_MALFORMED": "The json format of your the config.json seems to be malformed. Once fixed make sure to test in an private / incognito browser tab.", + "INVALID_TOTP_PARAMETERS": "Invalid TOTP parameters.", "ACCOUNT_DELETED_SUCCESSFULLY": "Your user account with all its data has been deleted successfully.", "REQUEST_TO_DELETE_USER_ACCOUNT_SUCCESSFUL": "The request to delete your user account was successful. Please check your inbox for an email with the verification link.", "TAGS": "Tags", diff --git a/src/common/data/translations/locale-pt-br.json b/src/common/data/translations/locale-pt-BR.json similarity index 100% rename from src/common/data/translations/locale-pt-br.json rename to src/common/data/translations/locale-pt-BR.json diff --git a/src/js/components/dialogs/add-totp.js b/src/js/components/dialogs/add-totp.js index 18d4c225..4466430b 100644 --- a/src/js/components/dialogs/add-totp.js +++ b/src/js/components/dialogs/add-totp.js @@ -13,6 +13,8 @@ import InputAdornment from "@mui/material/InputAdornment"; import IconButton from "@mui/material/IconButton"; import VisibilityOffIcon from "@mui/icons-material/VisibilityOff"; import SelectFieldTotpAlgorithm from "../select-field/totp-algorithm"; +import cryptoLibraryService from "../../services/crypto-library"; +import GridContainerErrors from "../grid-container-errors"; const useStyles = makeStyles((theme) => ({ textField: { @@ -24,6 +26,7 @@ const DialogAddTotp = (props) => { const { open, onClose } = props; const { t } = useTranslation(); const classes = useStyles(); + const [errors, setErrors] = useState([]); const [totpPeriod, setTotpPeriod] = useState(30); const [totpAlgorithm, setTotpAlgorithm] = useState("SHA1"); const [totpDigits, setTotpDigits] = useState(6); @@ -34,12 +37,23 @@ const DialogAddTotp = (props) => { setShowPassword(!showPassword); }; const onSave = (event) => { - onClose( - totpPeriod, - totpAlgorithm, - totpDigits, - totpCode, - ) + try { + cryptoLibraryService.getTotpToken( + totpCode, + totpPeriod, + totpAlgorithm, + totpDigits + ); + onClose( + totpPeriod, + totpAlgorithm, + totpDigits, + totpCode, + ) + } catch (e) { + console.log(e); + setErrors(["INVALID_TOTP_PARAMETERS"]); + } }; return ( @@ -156,6 +170,7 @@ const DialogAddTotp = (props) => { /> +