diff --git a/package.json b/package.json index e7883a99..d3a0b1fe 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "test:watch": "jest --no-cache --watch", "typecheck": "tsc", "lint": "eslint \"services/gunDB/**/*.js\"", - "format": "prettier --write \"./**/*.js\"" + "format": "prettier --write \"./**/*.js\"", + "test:gun": "ts-node src/__gun__tests__/*.ts && rimraf -rf GUN-TEST-*" }, "author": "", "license": "ISC", diff --git a/services/gunDB/Mediator/index.js b/services/gunDB/Mediator/index.js index e4aba797..e7914de8 100644 --- a/services/gunDB/Mediator/index.js +++ b/services/gunDB/Mediator/index.js @@ -26,23 +26,13 @@ const SEAx = require('gun/sea') /** @type {import('../contact-api/SimpleGUN').ISEA} */ const mySEA = {} -const $$__SHOCKWALLET__MSG__ = '$$__SHOCKWALLET__MSG__' +// Avoid this: https://github.com/amark/gun/issues/804 and any other issues const $$__SHOCKWALLET__ENCRYPTED__ = '$$_SHOCKWALLET__ENCRYPTED__' +const $$__SHOCKWALLET__MSG__ = '$$__SHOCKWALLET__MSG__' +const $$__SHOCKWALLET__NUMBER__ = '$$__SHOCKWALLET__NUMBER__' +const $$__SHOCKWALLET__BOOLEAN__ = '$$__SHOCKWALLET__BOOLEAN__' mySEA.encrypt = (msg, secret) => { - if (typeof msg !== 'string') { - throw new TypeError( - 'mySEA.encrypt() -> expected msg to be an string instead got: ' + - typeof msg - ) - } - - if (msg.length === 0) { - throw new TypeError( - 'mySEA.encrypt() -> expected msg to be a populated string' - ) - } - if (typeof secret !== 'string') { throw new TypeError( `mySEA.encrypt() -> expected secret to be a an string, args: |msg| -- ${JSON.stringify( @@ -57,15 +47,35 @@ mySEA.encrypt = (msg, secret) => { ) } - // Avoid this: https://github.com/amark/gun/issues/804 and any other issues - const sanitizedMsg = $$__SHOCKWALLET__MSG__ + msg + let strToEncode = '' - return SEAx.encrypt(sanitizedMsg, secret).then(encMsg => { + if (typeof msg === 'string') { + if (msg.length === 0) { + throw new TypeError( + 'mySEA.encrypt() -> expected msg to be a populated string' + ) + } + + strToEncode = $$__SHOCKWALLET__MSG__ + msg + } else if (typeof msg === 'boolean') { + strToEncode = $$__SHOCKWALLET__BOOLEAN__ + msg + } else if (typeof msg === 'number') { + strToEncode = $$__SHOCKWALLET__NUMBER__ + msg + } else { + throw new TypeError('mySea.encrypt() -> Not a valid msg type.') + } + + return SEAx.encrypt(strToEncode, secret).then(encMsg => { return $$__SHOCKWALLET__ENCRYPTED__ + encMsg }) } -mySEA.decrypt = (encMsg, secret) => { +/** + * @param {string} encMsg + * @param {string} secret + * @returns {Promise} + */ +const decryptBase = (encMsg, secret) => { if (typeof encMsg !== 'string') { throw new TypeError( 'mySEA.encrypt() -> expected encMsg to be an string instead got: ' + @@ -104,10 +114,41 @@ mySEA.decrypt = (encMsg, secret) => { throw new TypeError('Could not decrypt') } - return decodedMsg.slice($$__SHOCKWALLET__MSG__.length) + if (decodedMsg.startsWith($$__SHOCKWALLET__MSG__)) { + return decodedMsg.slice($$__SHOCKWALLET__MSG__.length) + } else if (decodedMsg.startsWith($$__SHOCKWALLET__BOOLEAN__)) { + const dec = decodedMsg.slice($$__SHOCKWALLET__BOOLEAN__.length) + if (dec === 'true') { + return true + } else if (dec === 'false') { + return false + } + throw new Error('Could not decrypt boolean value.') + } else if (decodedMsg.startsWith($$__SHOCKWALLET__NUMBER__)) { + return Number(decodedMsg.slice($$__SHOCKWALLET__NUMBER__.length)) + } + + throw new TypeError( + `mySea.encrypt() -> Unexpected type of prefix found inside decrypted value, first 20 characters: ${decodedMsg.slice( + 0, + 20 + )}` + ) }) } +mySEA.decrypt = (encMsg, secret) => { + return decryptBase(encMsg, secret) +} + +mySEA.decryptNumber = (encMsg, secret) => { + return decryptBase(encMsg, secret) +} + +mySEA.decryptBoolean = (encMsg, secret) => { + return decryptBase(encMsg, secret) +} + mySEA.secret = async (recipientOrSenderEpub, recipientOrSenderSEA) => { if (typeof recipientOrSenderEpub !== 'string') { throw new TypeError( diff --git a/services/gunDB/contact-api/SimpleGUN.ts b/services/gunDB/contact-api/SimpleGUN.ts index 6d04fdf7..fee7b196 100644 --- a/services/gunDB/contact-api/SimpleGUN.ts +++ b/services/gunDB/contact-api/SimpleGUN.ts @@ -120,8 +120,19 @@ export interface UserGUNNode extends GUNNode { } export interface ISEA { - encrypt(message: string, senderSecret: string): Promise + encrypt( + message: string | number | boolean, + senderSecret: string + ): Promise decrypt(encryptedMessage: string, recipientSecret: string): Promise + decryptNumber( + encryptedMessage: string, + recipientSecret: string + ): Promise + decryptBoolean( + encryptedMessage: string, + recipientSecret: string + ): Promise secret( recipientOrSenderEpub: string, recipientOrSenderUserPair: UserPair diff --git a/src/__gun__tests__/mySea.ts b/src/__gun__tests__/mySea.ts new file mode 100644 index 00000000..f2f2af21 --- /dev/null +++ b/src/__gun__tests__/mySea.ts @@ -0,0 +1,109 @@ +/** + * @format + */ +import Gun from 'gun' +import uuid from 'uuid/v1' + +import { mySEA } from '../../services/gunDB/Mediator' +import { UserGUNNode } from '../../services/gunDB/contact-api/SimpleGUN' + +const setupUser = async (): Promise<[UserGUNNode]> => { + const gun = Gun({ + file: 'GUN-TEST-' + uuid() + }) + + const user = (gun.user() as unknown) as UserGUNNode + + await new Promise((res, rej) => { + user.create('testAlias-' + uuid(), 'testPass', ack => { + if (typeof ack.err === 'string') { + rej(new Error(ack.err)) + } else { + res() + } + }) + }) + + return [user] +} + +const encryptsDecryptsStrings = async () => { + const [user] = await setupUser() + + const stringMessage = 'Lorem ipsum dolor' + + const sec = await mySEA.secret(user._.sea.epub, user._.sea) + const encrypted = await mySEA.encrypt(stringMessage, sec) + const decrypted = await mySEA.decrypt(encrypted, sec) + + if (decrypted !== stringMessage) { + throw new Error() + } +} + +const encryptsDecryptsBooleans = async () => { + const [user] = await setupUser() + + const truth = true + const lie = false + + const sec = await mySEA.secret(user._.sea.epub, user._.sea) + + const encryptedTruth = await mySEA.encrypt(truth, sec) + const decryptedTruth = await mySEA.decryptBoolean(encryptedTruth, sec) + + if (decryptedTruth !== truth) { + throw new Error() + } + + const encryptedLie = await mySEA.encrypt(lie, sec) + const decryptedLie = await mySEA.decryptBoolean(encryptedLie, sec) + + if (decryptedLie !== lie) { + throw new Error( + `Expected false got: ${decryptedLie} - ${typeof decryptedLie}` + ) + } +} + +const encryptsDecryptsNumbers = async () => { + const [user] = await setupUser() + + const number = Math.random() * 999999 + + const sec = await mySEA.secret(user._.sea.epub, user._.sea) + const encrypted = await mySEA.encrypt(number, sec) + const decrypted = await mySEA.decryptNumber(encrypted, sec) + + if (decrypted !== number) { + throw new Error() + } +} + +const encryptsDecryptsZero = async () => { + const [user] = await setupUser() + + const zero = 0 + + const sec = await mySEA.secret(user._.sea.epub, user._.sea) + const encrypted = await mySEA.encrypt(zero, sec) + const decrypted = await mySEA.decryptNumber(encrypted, sec) + + if (decrypted !== zero) { + throw new Error() + } +} + +const runAllTests = async () => { + await encryptsDecryptsStrings() + await encryptsDecryptsBooleans() + await encryptsDecryptsNumbers() + await encryptsDecryptsZero() + + console.log('\n--------------------------------') + console.log('All tests ran successfully') + console.log('--------------------------------\n') + process.exit(0) +} + +runAllTests()