diff --git a/qortal-ui-core/language/us.json b/qortal-ui-core/language/us.json index e9fae851..b998e557 100644 --- a/qortal-ui-core/language/us.json +++ b/qortal-ui-core/language/us.json @@ -644,7 +644,8 @@ "bchange41": "Do you give this application permission to access this list?", "bchange42": "Items", "bchange43": "Do you give this application permission to add to this list?", - "bchange44": "Do you give this application permission to delete from this list?" + "bchange44": "Do you give this application permission to delete from this list?", + "bchange45": "Encrypt" }, "datapage": { "dchange1": "Data Management", diff --git a/qortal-ui-plugins/plugins/core/components/qdn-action-encryption.js b/qortal-ui-plugins/plugins/core/components/qdn-action-encryption.js new file mode 100644 index 00000000..98d69a09 --- /dev/null +++ b/qortal-ui-plugins/plugins/core/components/qdn-action-encryption.js @@ -0,0 +1,84 @@ +import nacl from '../../../../qortal-ui-crypto/api/deps/nacl-fast.js' +import ed2curve from '../../../../qortal-ui-crypto/api/deps/ed2curve.js' + + + +export function uint8ArrayToBase64(uint8Array) { + const length = uint8Array.length; + let base64String = ''; + const chunkSize = 1024 * 1024; // Process 1MB at a time + + for (let i = 0; i < length; i += chunkSize) { + const chunkEnd = Math.min(i + chunkSize, length); + const chunk = uint8Array.subarray(i, chunkEnd); + const binaryString = chunk.reduce((acc, byte) => acc + String.fromCharCode(byte), ''); + base64String += btoa(binaryString); + } + + return base64String; +} + +export function base64ToUint8Array(base64) { + const binaryString = atob(base64) + const len = binaryString.length + const bytes = new Uint8Array(len) + + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i) + } + + return bytes +} + + +export const encryptData = ({ data64, recipientPublicKey }) => { + + + const Uint8ArrayData = base64ToUint8Array(data64) + const uint8Array = Uint8ArrayData + + if (!(uint8Array instanceof Uint8Array)) { + + throw new Error("The Uint8ArrayData you've submitted is invalid") + } + try { + const privateKey = window.parent.reduxStore.getState().app.selectedAddress.keyPair.privateKey + if (!privateKey) { + + throw new Error("Unable to retrieve keys") + } + const publicKeyUnit8Array = window.parent.Base58.decode(recipientPublicKey) + + const convertedPrivateKey = ed2curve.convertSecretKey(privateKey) + const convertedPublicKey = ed2curve.convertPublicKey(publicKeyUnit8Array) + const sharedSecret = new Uint8Array(32) + nacl.lowlevel.crypto_scalarmult(sharedSecret, convertedPrivateKey, convertedPublicKey) + + const chatEncryptionSeed = new window.parent.Sha256().process(sharedSecret).finish().result + + const nonce = new Uint8Array(24); + window.crypto.getRandomValues(nonce); + const encryptedData = nacl.secretbox(uint8Array, nonce, chatEncryptionSeed) + + const str = "qortalEncryptedData"; + const strEncoder = new TextEncoder(); + const strUint8Array = strEncoder.encode(str); + + const combinedData = new Uint8Array(strUint8Array.length + nonce.length + encryptedData.length); + + combinedData.set(strUint8Array); + + combinedData.set(nonce, strUint8Array.length); + combinedData.set(encryptedData, strUint8Array.length + nonce.length); + + const uint8arrayToData64 = uint8ArrayToBase64(combinedData) + + return { + encryptedData: uint8arrayToData64, + recipientPublicKey + } + } catch (error) { + console.log({ error }) + throw new Error("Error in encrypting data") + } +} \ No newline at end of file diff --git a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js index 1782f60a..5b8ee326 100644 --- a/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js +++ b/qortal-ui-plugins/plugins/core/qdn/browser/browser.src.js @@ -24,6 +24,7 @@ import { QORT_DECIMALS } from 'qortal-ui-crypto/api/constants'; import nacl from '../../../../../qortal-ui-crypto/api/deps/nacl-fast.js' import ed2curve from '../../../../../qortal-ui-crypto/api/deps/ed2curve.js' import { mimeToExtensionMap } from '../../components/qdn-action-constants'; +import { encryptData } from '../../components/qdn-action-encryption'; const parentEpml = new Epml({ type: 'WINDOW', source: window.parent }); class WebBrowser extends LitElement { @@ -908,6 +909,7 @@ class WebBrowser extends LitElement { return; case actions.PUBLISH_QDN_RESOURCE: { + // optional fields: encrypt:boolean recipientPublicKey:string const requiredFields = ['service', 'name', 'data64']; const missingFields = []; @@ -929,7 +931,7 @@ class WebBrowser extends LitElement { const service = data.service; const name = data.name; let identifier = data.identifier; - const data64 = data.data64; + let data64 = data.data64; const filename = data.filename; const title = data.title; const description = data.description; @@ -942,12 +944,39 @@ class WebBrowser extends LitElement { if (data.identifier == null) { identifier = 'default'; } + + if (data.encrypt && !data.recipientPublicKey) { + let data = {}; + data['error'] = "Encrypting data requires the recipient's public key"; + response = JSON.stringify(data); + break + } + + if (data.encrypt) { + try { + const encryptDataResponse = encryptData({ + data64, recipientPublicKey: data.recipientPublicKey + }) + if (encryptDataResponse.encryptedData) { + data64 = encryptDataResponse.encryptedData + } + + } catch (error) { + const obj = {}; + const errorMsg = error.message || 'Upload failed due to failed encryption'; + obj['error'] = errorMsg; + response = JSON.stringify(obj); + break + } + + } const res2 = await showModalAndWait( actions.PUBLISH_QDN_RESOURCE, { name, identifier, - service + service, + encrypt: data.encrypt } ); if (res2.action === 'accept') { @@ -1034,6 +1063,7 @@ class WebBrowser extends LitElement { actions.PUBLISH_MULTIPLE_QDN_RESOURCES, { resources, + encrypt: data.encrypt } ); @@ -2852,6 +2882,7 @@ async function showModalAndWait(type, data) { ${type === actions.PUBLISH_MULTIPLE_QDN_RESOURCES ? `