diff --git a/web/cashtab/package-lock.json b/web/cashtab/package-lock.json
--- a/web/cashtab/package-lock.json
+++ b/web/cashtab/package-lock.json
@@ -16,6 +16,7 @@
"dotenv": "^8.2.0",
"dotenv-expand": "^5.1.0",
"ecashaddrjs": "^1.0.1",
+ "eccrypto-js": "^5.4.0",
"ethereum-blockies-base64": "^1.0.2",
"localforage": "^1.9.0",
"lodash.isempty": "^4.4.0",
@@ -3525,6 +3526,11 @@
"node": ">=8.9"
}
},
+ "node_modules/aes-js": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz",
+ "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ=="
+ },
"node_modules/aggregate-error": {
"version": "3.1.0",
"license": "MIT",
@@ -8135,6 +8141,20 @@
"safer-buffer": "^2.1.0"
}
},
+ "node_modules/eccrypto-js": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/eccrypto-js/-/eccrypto-js-5.4.0.tgz",
+ "integrity": "sha512-W4xBr0UANgpkSIu2FBHG5wjhvOR4L19HwAUm9xiA4c3bzB9gGbH8+E9hSMTNF+QbmEi+TDvt373JHKA6NnW38A==",
+ "dependencies": {
+ "aes-js": "3.1.2",
+ "enc-utils": "2.1.0",
+ "hash.js": "1.1.7",
+ "js-sha3": "0.8.0",
+ "pbkdf2": "^3.0.17",
+ "randombytes": "2.1.0",
+ "secp256k1": "3.8.0"
+ }
+ },
"node_modules/ecurve": {
"version": "1.0.6",
"license": "MIT",
@@ -8201,6 +8221,21 @@
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
},
+ "node_modules/enc-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/enc-utils/-/enc-utils-2.1.0.tgz",
+ "integrity": "sha512-VD0eunGDyzhojePzkORWDnW88gi6tIeGb5Z6QVHugux6mMAPiXyw94fb/7WdDQEWhKMSoYRyzFFUebCqeH20PA==",
+ "dependencies": {
+ "bn.js": "4.11.8",
+ "is-typedarray": "1.0.0",
+ "typedarray-to-buffer": "3.1.5"
+ }
+ },
+ "node_modules/enc-utils/node_modules/bn.js": {
+ "version": "4.11.8",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
+ "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
+ },
"node_modules/encodeurl": {
"version": "1.0.2",
"dev": true,
@@ -14520,6 +14555,11 @@
"version": "0.9.0",
"license": "MIT"
},
+ "node_modules/js-sha3": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
+ "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"license": "MIT"
@@ -30950,6 +30990,11 @@
"regex-parser": "^2.2.11"
}
},
+ "aes-js": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz",
+ "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ=="
+ },
"aggregate-error": {
"version": "3.1.0",
"requires": {
@@ -34210,6 +34255,20 @@
"safer-buffer": "^2.1.0"
}
},
+ "eccrypto-js": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/eccrypto-js/-/eccrypto-js-5.4.0.tgz",
+ "integrity": "sha512-W4xBr0UANgpkSIu2FBHG5wjhvOR4L19HwAUm9xiA4c3bzB9gGbH8+E9hSMTNF+QbmEi+TDvt373JHKA6NnW38A==",
+ "requires": {
+ "aes-js": "3.1.2",
+ "enc-utils": "2.1.0",
+ "hash.js": "1.1.7",
+ "js-sha3": "0.8.0",
+ "pbkdf2": "^3.0.17",
+ "randombytes": "2.1.0",
+ "secp256k1": "3.8.0"
+ }
+ },
"ecurve": {
"version": "1.0.6",
"requires": {
@@ -34256,6 +34315,23 @@
"resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
"integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="
},
+ "enc-utils": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/enc-utils/-/enc-utils-2.1.0.tgz",
+ "integrity": "sha512-VD0eunGDyzhojePzkORWDnW88gi6tIeGb5Z6QVHugux6mMAPiXyw94fb/7WdDQEWhKMSoYRyzFFUebCqeH20PA==",
+ "requires": {
+ "bn.js": "4.11.8",
+ "is-typedarray": "1.0.0",
+ "typedarray-to-buffer": "3.1.5"
+ },
+ "dependencies": {
+ "bn.js": {
+ "version": "4.11.8",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
+ "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
+ }
+ }
+ },
"encodeurl": {
"version": "1.0.2",
"dev": true
@@ -38476,6 +38552,11 @@
"js-sha256": {
"version": "0.9.0"
},
+ "js-sha3": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
+ "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
+ },
"js-tokens": {
"version": "4.0.0"
},
diff --git a/web/cashtab/package.json b/web/cashtab/package.json
--- a/web/cashtab/package.json
+++ b/web/cashtab/package.json
@@ -12,6 +12,7 @@
"dotenv": "^8.2.0",
"dotenv-expand": "^5.1.0",
"ecashaddrjs": "^1.0.1",
+ "eccrypto-js": "^5.4.0",
"ethereum-blockies-base64": "^1.0.2",
"localforage": "^1.9.0",
"lodash.isempty": "^4.4.0",
diff --git a/web/cashtab/src/components/Common/Ticker.js b/web/cashtab/src/components/Common/Ticker.js
--- a/web/cashtab/src/components/Common/Ticker.js
+++ b/web/cashtab/src/components/Common/Ticker.js
@@ -34,10 +34,12 @@
opReturnPrefixHex: '6a',
opReturnPushDataHex: '04',
opReturnAppPrefixLengthHex: '04',
+ opPushDataOne: '4c',
appPrefixesHex: {
eToken: '534c5000',
cashtab: '00746162',
},
+ encryptionPrefix: '656e6372797074', // 'encrypt'
},
settingsValidation: {
fiatCurrency: [
@@ -114,6 +116,13 @@
return hexStr.startsWith(getCashtabEncodingSubstring());
}
+export function isMessageEncrypted(hexStr) {
+ if (!hexStr || typeof hexStr !== 'string') {
+ return false;
+ }
+ return hexStr.startsWith(currency.opReturn.encryptionPrefix);
+}
+
export function isEtokenOutput(hexStr) {
if (!hexStr || typeof hexStr !== 'string') {
return false;
@@ -121,21 +130,64 @@
return hexStr.startsWith(getETokenEncodingSubstring());
}
+export function isLongMessage(hexStr) {
+ if (!hexStr || typeof hexStr !== 'string') {
+ return false;
+ }
+ return hexStr.startsWith(currency.opReturn.opPushDataOne);
+}
+
+export function removeIncomingBytePrefix(hexSubstring) {
+ if (!hexSubstring || typeof hexSubstring !== 'string') {
+ return '';
+ }
+ if (isLongMessage(hexSubstring)) {
+ // this is a long message with 4 prefix characters preceding the message
+ hexSubstring = hexSubstring.slice(4);
+ } else {
+ // this is a short message with 2 prefix characters preceding the message
+ hexSubstring = hexSubstring.slice(2);
+ }
+ return hexSubstring;
+}
+
export function extractCashtabMessage(hexSubstring) {
if (!hexSubstring || typeof hexSubstring !== 'string') {
return '';
}
- let substring = hexSubstring.replace(getCashtabEncodingSubstring(), ''); // remove the cashtab encoding
- substring = substring.slice(2); // remove the 2 bytes indicating the size of the next element on the stack e.g. a0 -> 160 bytes
- return substring;
+ // remove the cashtab encoding
+ hexSubstring = hexSubstring.replace(getCashtabEncodingSubstring(), '');
+
+ // remove the incoming byte prefix preceding the message
+ hexSubstring = removeIncomingBytePrefix(hexSubstring);
+
+ return hexSubstring;
}
export function extractExternalMessage(hexSubstring) {
if (!hexSubstring || typeof hexSubstring !== 'string') {
return '';
}
- let substring = hexSubstring.slice(4); // remove the preceding OP_RETURN prefixes
- return substring;
+ // remove the OP_RETURN encoding
+ hexSubstring = hexSubstring.replace(currency.opReturn.encryptionPrefix, '');
+
+ // remove the incoming byte prefix preceding the message
+ hexSubstring = removeIncomingBytePrefix(hexSubstring);
+
+ return hexSubstring;
+}
+
+export function extractEncryptedMessage(hexSubstring) {
+ if (!hexSubstring || typeof hexSubstring !== 'string') {
+ return '';
+ }
+
+ hexSubstring = hexSubstring.replace(currency.opReturn.encryptionPrefix, '');
+
+ // remove the incoming byte prefix preceding the message
+ hexSubstring = removeIncomingBytePrefix(hexSubstring);
+
+ return hexSubstring;
}
export function isValidCashPrefix(addressString) {
diff --git a/web/cashtab/src/components/Send/Send.js b/web/cashtab/src/components/Send/Send.js
--- a/web/cashtab/src/components/Send/Send.js
+++ b/web/cashtab/src/components/Send/Send.js
@@ -245,6 +245,9 @@
bchValue = fiatToCrypto(value, fiatPrice);
}
+ let encryptionFlag;
+ encryptionFlag = true; // for testing only
+
try {
const link = await sendBch(
BCH,
@@ -254,6 +257,7 @@
bchValue,
currency.defaultFee,
optionalOpReturnMsg,
+ encryptionFlag,
);
sendXecNotification(link);
} catch (e) {
diff --git a/web/cashtab/src/components/Wallet/Tx.js b/web/cashtab/src/components/Wallet/Tx.js
--- a/web/cashtab/src/components/Wallet/Tx.js
+++ b/web/cashtab/src/components/Wallet/Tx.js
@@ -41,6 +41,15 @@
word-break: break-word;
padding-left: 13px;
padding-right: 30px;
+ /* invisible scrollbar */
+ overflow: hidden;
+ height: 100%;
+ margin-right: -50px; /* Maximum width of scrollbar */
+ padding-right: 50px; /* Maximum width of scrollbar */
+ overflow-y: scroll;
+ ::-webkit-scrollbar {
+ display: none;
+ }
`;
const SentLabel = styled.span`
font-weight: bold;
@@ -56,6 +65,12 @@
color: ${props => props.theme.primary} !important;
white-space: nowrap;
`;
+const EncryptionMessageLabel = styled.span`
+ text-align: left;
+ font-weight: bold;
+ color: red;
+ white-space: nowrap;
+`;
const MessageLabel = styled.span`
text-align: left;
font-weight: bold;
@@ -403,6 +418,13 @@
External Message
)}
+ {data.isEncryptedMessage ? (
+
+ - Encrypted
+
+ ) : (
+ ''
+ )}
{data.opReturnMessage
? Buffer.from(
diff --git a/web/cashtab/src/hooks/useBCH.js b/web/cashtab/src/hooks/useBCH.js
--- a/web/cashtab/src/hooks/useBCH.js
+++ b/web/cashtab/src/hooks/useBCH.js
@@ -5,6 +5,8 @@
isEtokenOutput,
extractCashtabMessage,
extractExternalMessage,
+ isMessageEncrypted,
+ extractEncryptedMessage,
} from '@components/Common/Ticker';
import { isValidTokenStats } from '@utils/validation';
import SlpWallet from 'minimal-slp-wallet';
@@ -18,6 +20,7 @@
confirmNonEtokenUtxos,
} from '@utils/cashMethods';
import cashaddr from 'ecashaddrjs';
+const eccrypto = require('eccrypto-js');
export default function useBCH() {
const SEND_BCH_ERRORS = {
@@ -78,7 +81,7 @@
return flatTxHistory.splice(0, txCount);
};
- const parseTxData = txData => {
+ const parseTxData = async (txData, wallet) => {
/*
Desired output
[
@@ -97,7 +100,6 @@
}
]
*/
-
const parsedTxHistory = [];
for (let i = 0; i < txData.length; i += 1) {
const tx = txData[i];
@@ -124,6 +126,7 @@
let amountReceived = 0;
let opReturnMessage = '';
let isCashtabMessage = false;
+ let isEncryptedMessage = false;
// Assume an incoming transaction
let outgoingTx = false;
let tokenTx = false;
@@ -152,26 +155,72 @@
!Object.keys(thisOutput.scriptPubKey).includes('addresses')
) {
let hex = thisOutput.scriptPubKey.hex;
-
+ console.log('Full hex is: ' + hex);
if (isEtokenOutput(hex)) {
// this is an eToken transaction
tokenTx = true;
} else if (isCashtabOutput(hex)) {
// this is a cashtab.com generated message
- try {
- substring = extractCashtabMessage(hex);
- opReturnMessage = Buffer.from(substring, 'hex');
- isCashtabMessage = true;
- } catch (err) {
- // soft error if an unexpected or invalid cashtab hex is encountered
- opReturnMessage = '';
+ console.log('Cashtab message found');
+ substring = extractCashtabMessage(hex);
+
+ if (isMessageEncrypted(substring)) {
+ // this is an encrypted Cashtab message
+ console.log('encrypted message found');
+
+ substring = extractEncryptedMessage(substring);
console.log(
- 'useBCH.parsedTxHistory() error: invalid cashtab msg hex: ' +
- substring,
+ 'extracted encryption string is: ' + substring,
);
+
+ let fundingWif;
+ if (
+ wallet &&
+ wallet.state &&
+ wallet.state.slpBalancesAndUtxos
+ ) {
+ fundingWif =
+ wallet.state.slpBalancesAndUtxos
+ .nonSlpUtxos[0].wif;
+ } else {
+ break;
+ }
+
+ console.log('WIF for decryption: ' + fundingWif);
+
+ // Convert the hex encoded message to a buffer
+ const msgBuf = Buffer.from(substring, 'hex');
+ let structData;
+ let decryptedMessage;
+ try {
+ // Convert the bufer into a structured object.
+ structData = convertToEncryptStruct(msgBuf);
+ decryptedMessage = await eccrypto.decrypt(
+ fundingWif,
+ structData,
+ );
+ console.log(
+ 'decryption successful, message: ' +
+ decryptedMessage,
+ );
+ } catch (err) {
+ console.log('parseTx decryption error: ' + err);
+ decryptedMessage = 'Encrypted Message';
+ }
+
+ console.log(
+ 'parseTx.Decrypted message:' + decryptedMessage,
+ );
+ opReturnMessage = decryptedMessage;
+ isEncryptedMessage = true;
+ } else {
+ // this is an un-encrypted Cashtab message
+ opReturnMessage = substring;
}
+ isCashtabMessage = true;
} else {
// this is an externally generated message
+ console.log('Un-encrypted message found');
try {
substring = extractExternalMessage(hex);
opReturnMessage = Buffer.from(substring, 'hex');
@@ -210,11 +259,50 @@
parsedTx.destinationAddress = destinationAddress;
parsedTx.opReturnMessage = opReturnMessage;
parsedTx.isCashtabMessage = isCashtabMessage;
+ parsedTx.isEncryptedMessage = isEncryptedMessage;
parsedTxHistory.push(parsedTx);
}
return parsedTxHistory;
};
+ // Converts a serialized buffer containing encrypted data into an object
+ // that can interpreted by the eccryptoJS library.
+ const convertToEncryptStruct = encbuf => {
+ try {
+ let offset = 0;
+ const tagLength = 32;
+ let pub;
+ switch (encbuf[0]) {
+ case 4:
+ pub = encbuf.slice(0, 65);
+ break;
+ case 3:
+ case 2:
+ pub = encbuf.slice(0, 33);
+ break;
+ default:
+ throw new Error(`Invalid type: ${encbuf[0]}`);
+ }
+ offset += pub.length;
+
+ const c = encbuf.slice(offset, encbuf.length - tagLength);
+ const ivbuf = c.slice(0, 128 / 8);
+ const ctbuf = c.slice(128 / 8);
+
+ const d = encbuf.slice(encbuf.length - tagLength, encbuf.length);
+
+ return {
+ iv: ivbuf,
+ ephemPublicKey: pub,
+ ciphertext: ctbuf,
+ mac: d,
+ };
+ } catch (err) {
+ console.error(`Error in convertToEncryptStruct()`);
+ throw err;
+ }
+ };
+
const getTxHistory = async (BCH, addresses) => {
let txHistoryResponse;
try {
@@ -257,7 +345,7 @@
return txDataWithPassThrough;
};
- const getTxData = async (BCH, txHistory) => {
+ const getTxData = async (BCH, txHistory, wallet) => {
// Flatten tx history
let flatTxs = flattenTransactions(txHistory);
@@ -276,7 +364,7 @@
try {
txDataPromiseResponse = await Promise.all(txDataPromises);
- const parsed = parseTxData(txDataPromiseResponse);
+ const parsed = parseTxData(txDataPromiseResponse, wallet);
return parsed;
} catch (err) {
@@ -967,6 +1055,7 @@
sendAmount,
feeInSatsPerByte,
optionalOpReturnMsg,
+ encryptionFlag,
) => {
try {
if (!sendAmount) {
@@ -1005,20 +1094,62 @@
throw error;
}
+ let script;
// Start of building the OP_RETURN output.
// only build the OP_RETURN output if the user supplied it
if (
typeof optionalOpReturnMsg !== 'undefined' &&
optionalOpReturnMsg.trim() !== ''
) {
- const script = [
- BCH.Script.opcodes.OP_RETURN, // 6a
- Buffer.from(
- currency.opReturn.appPrefixesHex.cashtab,
- 'hex',
- ), // 00746162
- Buffer.from(optionalOpReturnMsg),
- ];
+ if (encryptionFlag) {
+ //if the user has opted to encrypt this message
+ let recipientPubKey = await getPublicKey(
+ BCH,
+ destinationAddress,
+ );
+ console.log('PUBLIC KEY IS: ' + recipientPubKey);
+
+ const pubKeyBuf = Buffer.from(recipientPubKey, 'hex');
+ const bufferedFile = Buffer.from(optionalOpReturnMsg);
+ const structuredEj = await eccrypto.encrypt(
+ pubKeyBuf,
+ bufferedFile,
+ );
+
+ // Serialize the encrypted data object
+ const encryptedEj = Buffer.concat([
+ structuredEj.ephemPublicKey,
+ structuredEj.iv,
+ structuredEj.ciphertext,
+ structuredEj.mac,
+ ]);
+
+ const encryptedMessage = encryptedEj.toString('hex');
+
+ console.log('Original message: ' + optionalOpReturnMsg);
+ console.log('Encrypted message: ' + encryptedMessage);
+
+ // build the OP_RETURN script with the encryption prefix
+ script = [
+ BCH.Script.opcodes.OP_RETURN, // 6a
+ Buffer.from(
+ currency.opReturn.appPrefixesHex.cashtab,
+ 'hex',
+ ), // 00746162
+ Buffer.from(currency.opReturn.encryptionPrefix, 'hex'), // 656e6372797074 a.k.a 'encrypt'
+ Buffer.from(encryptedEj),
+ ];
+ } else {
+ // this is an un-encrypted message
+ script = [
+ BCH.Script.opcodes.OP_RETURN, // 6a
+ Buffer.from(
+ currency.opReturn.appPrefixesHex.cashtab,
+ 'hex',
+ ), // 00746162
+ Buffer.from(optionalOpReturnMsg),
+ ];
+ }
const data = BCH.Script.encode(script);
transactionBuilder.addOutput(data, 0);
}
@@ -1131,6 +1262,16 @@
}
};
+ const getPublicKey = async (BCH, address) => {
+ try {
+ const publicKey = await BCH.encryption.getPubKey(address);
+ return publicKey.publicKey;
+ } catch (err) {
+ console.log(`useBCH.getPublicKey() error: `, err);
+ throw err;
+ }
+ };
+
const getBCH = (apiIndex = 0) => {
let ConstructedSlpWallet;
diff --git a/web/cashtab/src/hooks/useWallet.js b/web/cashtab/src/hooks/useWallet.js
--- a/web/cashtab/src/hooks/useWallet.js
+++ b/web/cashtab/src/hooks/useWallet.js
@@ -240,7 +240,7 @@
hydratedUtxoDetails,
);
const txHistory = await getTxHistory(BCH, cashAddresses);
- const parsedTxHistory = await getTxData(BCH, txHistory);
+ const parsedTxHistory = await getTxData(BCH, txHistory, wallet);
const parsedWithTokens = await addTokenTxData(BCH, parsedTxHistory);