diff --git a/javascript/src/iframe/viamapi-iframe.js b/javascript/src/iframe/viamapi-iframe.js index ba3b561ad082517a325cee6c3d5fabd837b4038e..2fbf2c28cb1f006a6bc9cb732d7e6694ebf25f73 100644 --- a/javascript/src/iframe/viamapi-iframe.js +++ b/javascript/src/iframe/viamapi-iframe.js @@ -47,6 +47,7 @@ import { checkRecoveryKeyCombine, encryptShare } from "../utilities/secrets"; +import { generateNonce, signRSA } from "../utilities/cryptoUtils"; const penpalMethods = require("../../temp/penpal-methods").default; const WopiAPI = require("./wopiapi-iframe"); @@ -129,6 +130,23 @@ function setIdentityInLocalStorage(identityToStore, extendKey = true) { ); } +async function setCurrentlyLoadedIdentity(identity) { + if (identity) { + const nonce = generateNonce(); + const privateKey = identity.authentication.privateKey; + const nonceSignature = await signRSA(privateKey, nonce); + window.viamApi.setNonce(Buffer.from(nonce).toString("base64")); + window.viamApi.setNonceSignature( + Buffer.from(nonceSignature).toString("base64") + ); + } else { + window.viamApi.setNonce(""); + window.viamApi.setNonceSignature(""); + } + + window.currentlyLoadedIdentity = identity; +} + function getProfileData(identity) { return new Penpal.Promise(executeResultUpper => { executeRestfulFunction( @@ -260,7 +278,7 @@ const destroyIdentity = () => { const { publicKey } = window.currentlyLoadedIdentity.authentication; delete window.loadedIdentities[publicKey]; - window.currentlyLoadedIdentity = null; + setCurrentlyLoadedIdentity(null); destroyIdentityFromLocalStorage(publicKey); } }; @@ -390,10 +408,17 @@ function loadIdentityInternal(identityKey, pinCode) { "Please restore or authorize your account via another device." }); } + if (!loadedIdentity.authentication.privateKey) { + result({ + data: "", + code: "400", + status: "No privateKey" + }); + } localStorage.removeItem("attempt"); window.loadedIdentities[identityKey] = loadedIdentity; - window.currentlyLoadedIdentity = loadedIdentity; + await setCurrentlyLoadedIdentity(loadedIdentity); if (identityKey === localStorage.getItem("authenticatedIdentity")) { window.currentlyAuthenticatedIdentity = loadedIdentity; @@ -479,10 +504,10 @@ function getCertificateForPassport(passportUUID, internal) { getProfileData(passportIdentity).then(executeResult1 => { setIdentityInLocalStorage(passportIdentity) - .then(() => { + .then(async () => { window.currentlyAuthenticatedIdentity = passportIdentity; window.lastTimeGetProfile = 0; - window.currentlyLoadedIdentity = passportIdentity; + await setCurrentlyLoadedIdentity(passportIdentity); const copyOfCryptoData = JSON.parse( JSON.stringify(cryptoData) ); @@ -529,7 +554,7 @@ function getCertificateForPassport(passportUUID, internal) { const connection = Penpal.connectToParent({ // Methods child is exposing to parent methods: { - initialize: (apiUrl, wopiUrl, collaboraUrl) => { + initialize: async (apiUrl, wopiUrl, collaboraUrl) => { if (!apiUrl) { apiUrl = `${window.location.origin}/api/`; console.warn(`API host URL not specified. Fall back to ${apiUrl}`); // eslint-disable-line no-console @@ -555,11 +580,40 @@ const connection = Penpal.connectToParent({ collaboraUrl.charAt(collaboraUrl.length - 1) === "/" ? collaboraUrl : collaboraUrl + "/"; + + const { + code, + data: { domains: permittedDomains } + } = await penpalMethods.identityGetPermittedDomains(); + + if (code !== "200") { + throw new Error("Unable to retrieve a list of permitted domains."); + } + + const iframeOrigin = document.referrer; + if ( + iframeOrigin && // Empty iframe origins are allowed. This is the case for Roundcube plugin + permittedDomains && + permittedDomains.length + ) { + let iframeOriginIsPermitted = false; + + for (const domain of permittedDomains) { + if (iframeOrigin.includes(domain)) { + iframeOriginIsPermitted = true; + break; + } + } + + if (!iframeOriginIsPermitted) { + throw new Error(`Iframe origin "${iframeOrigin}" is not permitted.`); + } + } }, ...penpalMethods, createIdentity(pinCode) { return new Penpal.Promise(result => { - createPassportCertificate(makeid()).then(function (keys) { + createPassportCertificate(makeid()).then(async function (keys) { const newIdentity = new Identity(); const cryptoData = new CryptoData(); cryptoData.setPublicKey(keys["publicKeyPEM"]); @@ -568,7 +622,7 @@ const connection = Penpal.connectToParent({ newIdentity.setAuthentication(cryptoData); newIdentity.setPinCode(pinCode); - window.currentlyLoadedIdentity = newIdentity; + await setCurrentlyLoadedIdentity(newIdentity); localStorage.setItem( "currentlyLoadedIdentity", JSON.stringify(newIdentity) @@ -629,7 +683,7 @@ const connection = Penpal.connectToParent({ identity.pinCode = newPinCode; await setIdentityInLocalStorage(identity); window.currentlyAuthenticatedIdentity = identity; - window.currentlyLoadedIdentity = identity; + await setCurrentlyLoadedIdentity(identity); return encodeResponse("200", null, "Successfully changed pincode"); } else { @@ -758,7 +812,7 @@ const connection = Penpal.connectToParent({ let sequence = Promise.resolve(); if (executeResult.code === "200") { sequence = sequence.then(() => { - setIdentityInLocalStorage(registerIdentity); + setIdentityInLocalStorage(window.currentlyLoadedIdentity); }); } sequence @@ -791,6 +845,7 @@ const connection = Penpal.connectToParent({ }); }, login: async (loginIdentity, mode, requestCode, requestActionID) => { + const localStorageIdentity = getIdentityFromLocalStorage(); if (!window.loadedIdentities[loginIdentity.authentication.publicKey]) { return { data: "", @@ -995,7 +1050,7 @@ const connection = Penpal.connectToParent({ "currentlyLoadedIdentity" ); const identity = new Identity(currentlyLoadedIdentity); - window.currentlyLoadedIdentity = identity; + await setCurrentlyLoadedIdentity(identity); const { publicKey } = identity.authentication; window.loadedIdentities[publicKey] = identity; window.viamAnonymousApi.setIdentity(publicKey); @@ -1577,7 +1632,7 @@ const connection = Penpal.connectToParent({ return encodeResponse("200", "enabled", "OK"); }, /** - * + * * @param passportUUID * @param documentUUID * @param documentContentType @@ -1609,7 +1664,7 @@ message SignatureData { uint32 pageNumber = 3; Size pageSize = 4; } - * + * */ signDocumentJava: async ( passportUUID, @@ -2489,7 +2544,7 @@ connection.promise.then(parent => { if (event.key === "authenticatedIdentity" && event.newValue === null) { const publicKey = window.currentlyAuthenticatedIdentity.authentication.publicKey; - window.currentlyLoadedIdentity = null; + setCurrentlyLoadedIdentity(null); window.currentlyAuthenticatedIdentity = null; const event = createEvent("LogoutFromAnotherTab", "Logout", [publicKey]); parent.onEvent(event); @@ -2572,7 +2627,8 @@ connection.promise.then(parent => { false ); - window.currentlyLoadedIdentity = identity; + !window.currentlyLoadedIdentity && + (await setCurrentlyLoadedIdentity(identity)); if (!identityAuthenticatedEvent && identity) { const event = createEvent("IdentityAuthenticated", "Authenticated", [ @@ -2604,7 +2660,7 @@ connection.promise.then(parent => { } identityAuthenticatedEvent = false; - window.currentlyLoadedIdentity = null; + setCurrentlyLoadedIdentity(null); } localStorage.removeItem("currentlyLoadedIdentity"); @@ -2711,7 +2767,7 @@ connection.promise.then(parent => { localStorage.removeItem("token"); localStorage.removeItem("authenticatedIdentity"); delete window.loadedIdentities[authenticationPublicKey]; - window.currentlyLoadedIdentity = null; + setCurrentlyLoadedIdentity(null); window.currentlyAuthenticatedIdentity = null; window.lastTimeGetProfile = 0; diff --git a/javascript/src/utilities/cryptoUtils.js b/javascript/src/utilities/cryptoUtils.js new file mode 100644 index 0000000000000000000000000000000000000000..cee822966828071966dd6a7c5c66f2acb17b8561 --- /dev/null +++ b/javascript/src/utilities/cryptoUtils.js @@ -0,0 +1,45 @@ +const webcryptoLiner = require("webcrypto-liner/build/index"); + +export const generateNonce = () => { + return webcryptoLiner.crypto.getRandomValues(new Buffer(12)); +} + +const convertPemToBinary = (pem) => { + const lines = pem.split("\n"); + let encoded = ""; + for (let i = 0; i < lines.length; i++) { + if ( + lines[i].trim().length > 0 && + lines[i].indexOf("-BEGIN PRIVATE KEY-") < 0 && + lines[i].indexOf("-BEGIN PUBLIC KEY-") < 0 && + lines[i].indexOf("-END PRIVATE KEY-") < 0 && + lines[i].indexOf("-END PUBLIC KEY-") < 0 + ) { + encoded += lines[i].trim(); + } + } + return Buffer.from(encoded, "base64") +}; + +export const signRSA = async (privateKeyPEM, data) => { + const privateKey = await webcryptoLiner.crypto.subtle.importKey( + "pkcs8", + convertPemToBinary(privateKeyPEM), + { + name: "RSA-PSS", + hash: "SHA-256" + }, + true, + ["sign"] + ); + + return await webcryptoLiner.crypto.subtle.sign( + { + name: "RSA-PSS", + // hash: "SHA-256", + saltLength: 32 + }, + privateKey, + data + ); +}; diff --git a/main.go b/main.go index 6d931629ef40a58c49882b40be4f203a03e4dd4f..a3c4b5aef0f2eaddf481043d4119fbed096dd86b 100644 --- a/main.go +++ b/main.go @@ -76,12 +76,15 @@ func buildViamAPI() string { keysLen := len(keys) result += "function ViamAPI() {\n" + + " this.privateKey = \"\";\n" + " this.config = {\n" + " headers: {\n" + " 'publicKey': '',\n" + " 'uuid': '',\n" + " 'deviceHash': '',\n" + - " 'token': ''\n" + + " 'token': '',\n" + + " 'nonce': '',\n" + + " 'nonceSignature': ''\n" + " }\n" + " }\n" + "}\n\n" @@ -99,10 +102,24 @@ func buildViamAPI() string { " this.config.headers.publicKey = window.btoa(authenticationPublicKey);\n" + "};\n\n" - result += "ViamAPI.prototype.getConfig = function() {\n" + - " return this.config;\n" + + result += "ViamAPI.prototype.setNonceSignature = function(nonceSignature) {\n" + + " this.config.headers.nonceSignature = nonceSignature;\n" + "};\n\n" + result += "ViamAPI.prototype.setNonce = function(nonce) {\n" + + " this.config.headers.nonce = nonce;\n" + + "};\n\n" + result += "ViamAPI.prototype.getNonce = function() {\n" + + " return this.config.headers.nonce;\n" + + "};\n\n" + result += "ViamAPI.prototype.setPrivateKey = function(privateKey) {\n" + + " this.privateKey = privateKey;\n" + + "};\n\n" + + result += "ViamAPI.prototype.getConfig = function() {\n" + + " return this.config;\n" + + "};\n\n" + for i := 0; i < keysLen; i++ { url := keys[i] if endPoints[url].Form != nil {