diff --git a/package.json b/package.json index 18d13a8..50c6912 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,9 @@ "is-html": "^2.0.0", "js-status-chat-name": "git+https://github.com/status-im/js-status-chat-name.git#v0.1.2", "morgan": "^1.9.1", + "multibase": "^1.0.1", "qrcode": "^1.3.0", + "secp256k1": "^4.0.1", "univeil": "^0.1.14" }, "devDependencies": { diff --git a/routes/index.js b/routes/index.js index ea377ea..a718845 100644 --- a/routes/index.js +++ b/routes/index.js @@ -68,12 +68,18 @@ const handleSite = (req, res) => { /* Open User Profile from Chat Key in Status */ const handleChatKey = (req, res) => { - /* We accept upper case for chat keys */ - const chatKey = req.params[0].toLowerCase() + let chatKey = req.params[0] + let uncompressedKey = chatKey + try { - chatName = StatusIm.chatKeyToChatName(chatKey) + if (!chatKey.startsWith('0x')) { /* decompress/deserialize key */ + uncompressedKey = utils.decompressKey(chatKey) + } else { /* We accept upper case for hexadecimal public keys */ + chatKey = chatKey.toLowerCase() + } + chatName = StatusIm.chatKeyToChatName(uncompressedKey) } catch(error) { - console.error(`Failed to parse: "${req.params[0]}", Error:`, error.message) + console.error(`Failed to parse: "${uncompressedKey}", Error:`, error.message) res.render('index', { title: 'Invalid chat key format!', error }) return } @@ -130,9 +136,14 @@ router.get('/health', (req, res) => res.send('OK')) router.get('/b/:url(*)', handleSite) router.get('/browse/:url(*)', handleSite) /* Legacy */ +router.get(/^\/u\/(z[0-9a-zA-Z]{46,49})$/, handleChatKey) +router.get(/^\/u\/(z[0-9a-zA-Z]+)$/, handleError('Incorrect length of chat key')) +router.get(/^\/u\/(fe701[0-9a-fA-F]{66})$/, handleChatKey) +router.get(/^\/u\/(fe701[0-9a-fA-F]+)$/, handleError('Incorrect length of chat key')) +router.get(/^\/u\/(f[0-9a-fA-F]{66})$/, handleChatKey) +router.get(/^\/u\/(f[0-9a-fA-F]+)$/, handleError('Incorrect length of chat key')) router.get(/^\/u\/(0[xX]04[0-9a-fA-F]{128})$/, handleChatKey) -router.get(/^\/u\/(0[xX]04[0-9a-fA-F]{1,127})$/, handleError('Incorrect length of chat key')) -router.get(/^\/u\/(0[xX]04[0-9a-fA-F]{129,})$/, handleError('Incorrect length of chat key')) +router.get(/^\/u\/(0[xX]04[0-9a-fA-F]+)$/, handleError('Incorrect length of chat key')) router.get(/^\/user\/(0[xX]04[0-9a-fA-F]{128})$/, handleChatKey) /* Legacy */ router.get(/^\/u\/([^><]*[A-Z]+[^><]*)$/, handleRedirect) diff --git a/tests/main.js b/tests/main.js index 4491ad3..1f58a41 100644 --- a/tests/main.js +++ b/tests/main.js @@ -8,6 +8,8 @@ import appleSiteAssociation from '../resources/apple-app-site-association.json' const host = 'join.status.im' const chatKey = 'e139115a1acc72510388fcf7e1cf492784c9a839888b25271465f4f1baa38c2d3997f8fd78828eb8628bc3bb55ababd884c6002d18330d59c404cc9ce3e4fb35' +const multibaseKey = 'fe70103e139115a1acc72510388fcf7e1cf492784c9a839888b25271465f4f1baa38c2d' +const compressedKey = 'zQ3shuoHL7WZEfKdexM6EyDRDhXBgcKz5SVw79stVMpmeyUvG' const chatName = 'Lavender Trivial Goral' const srv = request(app) @@ -103,6 +105,40 @@ test('test chat key routes', t => { }) }) +test('test multibase chat key routes', t => { + t.test(`/u/${multibaseKey.substr(0,12)}... - VALID`, async t => { + const res = await get(`/u/${multibaseKey}`) + t.eq(res.statusCode, 200, 'returns 200') + t.eq(meta(res, 'al:ios:url'), `status-im://u/${multibaseKey}`, 'contains ios url') + t.eq(meta(res, 'al:android:url'), `status-im://u/${multibaseKey}`, 'contains android url') + t.eq(html(res, 'div.info'), `Chat and transact with ${multibaseKey} in Status.`, 'contains prompt') + t.eq(html(res, '#header'), chatName, 'contains chat name') + }) + + t.test(`/u/${multibaseKey.substr(0,12)}... - TOO SHORT`, async t => { /* error on too short chat key */ + const res = await get(`/u/${multibaseKey.substr(0,46)}`) + t.eq(res.statusCode, 400, 'returns 400') + t.eq(html(res, 'code#error'), 'Incorrect length of chat key', 'contains error') + }) +}) + +test('test compressed chat key routes', t => { + t.test(`/u/${compressedKey.substr(0,12)}... - VALID`, async t => { + const res = await get(`/u/${compressedKey}`) + t.eq(res.statusCode, 200, 'returns 200') + t.eq(meta(res, 'al:ios:url'), `status-im://u/${compressedKey}`, 'contains ios url') + t.eq(meta(res, 'al:android:url'), `status-im://u/${compressedKey}`, 'contains android url') + t.eq(html(res, 'div.info'), `Chat and transact with ${compressedKey} in Status.`, 'contains prompt') + t.eq(html(res, '#header'), chatName, 'contains chat name') + }) + + t.test(`/u/${compressedKey.substr(0,12)}... - TOO SHORT`, async t => { /* error on too short chat key */ + const res = await get(`/u/${compressedKey.substr(0,46)}`) + t.eq(res.statusCode, 400, 'returns 400') + t.eq(html(res, 'code#error'), 'Incorrect length of chat key', 'contains error') + }) +}) + test('test public channel routes', t => { t.test('/status-test - VALID', async t => { const res = await get('/status-test') diff --git a/utils/index.js b/utils/index.js index d549dca..1706b5c 100644 --- a/utils/index.js +++ b/utils/index.js @@ -2,6 +2,9 @@ const QRCode = require('qrcode') const uts46 = require('idna-uts46-hx') const isHtml = require('is-html') const univeil = require('univeil') +const { Buffer } = require('buffer') +const multibase = require('multibase') +const secp256k1 = require('secp256k1') const isAndroid = (req) => ( req.headers['user-agent'].toLowerCase().indexOf("android") > -1 @@ -42,6 +45,23 @@ const normalizeEns = (name) => ( const showSpecialChars = (str) => univeil(str) +/* check for multiformat variant encoding of the + * multicodec secp256k1 key identifier e7 */ +const isMultiFormatSecp256k1 = (bytes) => ( + Buffer.from([231, 1]).compare(bytes.slice(0, 2)) == 0 +) + +/* decodes base58btc encoding and decompresses a serialized secp256k1 */ +const decompressKey = (key) => { + let cBytes = multibase.decode(key) + if (isMultiFormatSecp256k1(cBytes)) { + cBytes = cBytes.slice(2) + } + let pubKey = secp256k1.publicKeyConvert(cBytes, compressed=false) + let multibaseHex = multibase.encode('base16', pubKey).toString() + return '0x' + multibaseHex.substr(1) +} + module.exports = { isAndroid, isIOS, @@ -49,4 +69,5 @@ module.exports = { isValidUrl, normalizeEns, showSpecialChars, + decompressKey, } diff --git a/yarn.lock b/yarn.lock index 410bd6f..b7371ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -102,6 +102,13 @@ balanced-match@^1.0.0: resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= +base-x@^3.0.8: + version "3.0.8" + resolved "https://registry.yarnpkg.com/base-x/-/base-x-3.0.8.tgz#1e1106c2537f0162e8b52474a557ebb09000018d" + integrity sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA== + dependencies: + safe-buffer "^5.0.1" + base64-js@^1.0.2: version "1.3.1" resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1" @@ -139,6 +146,11 @@ bindings@^1.5.0: dependencies: file-uri-to-path "1.0.0" +bn.js@^4.4.0: + version "4.11.9" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" + integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== + body-parser@1.18.3: version "1.18.3" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" @@ -197,6 +209,11 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" +brorand@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8= + buffer-alloc-unsafe@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0" @@ -228,6 +245,14 @@ buffer@^5.4.3: base64-js "^1.0.2" ieee754 "^1.1.4" +buffer@^5.5.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.6.0.tgz#a31749dc7d81d84db08abf937b6b8c4033f62786" + integrity sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw== + dependencies: + base64-js "^1.0.2" + ieee754 "^1.1.4" + byline@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" @@ -619,6 +644,19 @@ ejs@~2.5.7: resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.9.tgz#7ba254582a560d267437109a68354112475b0ce5" integrity sha512-GJCAeDBKfREgkBtgrYSf9hQy9kTb3helv0zGdzqhM7iAkW8FA/ZF97VQDbwFiwIT8MQLLOe5VlPZOEvZAqtUAQ== +elliptic@^6.5.2: + version "6.5.3" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.3.tgz#cb59eb2efdaf73a0bd78ccd7015a62ad6e0f93d6" + integrity sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw== + dependencies: + bn.js "^4.4.0" + brorand "^1.0.1" + hash.js "^1.0.0" + hmac-drbg "^1.0.0" + inherits "^2.0.1" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.0" + emoji-regex@^7.0.1: version "7.0.3" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" @@ -922,6 +960,23 @@ has-values@^1.0.0: is-number "^3.0.0" kind-of "^4.0.0" +hash.js@^1.0.0, hash.js@^1.0.3: + version "1.1.7" + resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" + integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== + dependencies: + inherits "^2.0.3" + minimalistic-assert "^1.0.1" + +hmac-drbg@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE= + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + html-tags@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.1.0.tgz#7b5e6f7e665e9fb41f30007ed9e0d41e97fb2140" @@ -1338,6 +1393,16 @@ mime@^1.4.1: resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== +minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo= + minimatch@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" @@ -1379,6 +1444,14 @@ ms@^2.1.1: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== +multibase@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/multibase/-/multibase-1.0.1.tgz#4adbe1de0be8a1ab0274328b653c3f1903476724" + integrity sha512-KcCxpBVY8fdVKu4dJMAahq4F/2Z/9xqEjIiR7PiMe7LRGeorFn2NLmicN6nLBCqQvft6MG2Lc9X5P0IdyvnxEw== + dependencies: + base-x "^3.0.8" + buffer "^5.5.0" + nan@^2.12.1: version "2.14.0" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.0.tgz#7818f722027b2459a86f0295d434d1fc2336c52c" @@ -1406,6 +1479,16 @@ negotiator@0.6.2: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb" integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== +node-addon-api@^2.0.0: + version "2.0.2" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" + integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== + +node-gyp-build@^4.2.0: + version "4.2.2" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.2.tgz#3f44b65adaafd42fb6c3d81afd630e45c847eb66" + integrity sha512-Lqh7mrByWCM8Cf9UPqpeoVBBo5Ugx+RKu885GAzmLBVYjeywScxHXPGLa4JfYNZmcNGwzR0Glu5/9GaQZMFqyA== + nodemon@^1.17.5: version "1.19.4" resolved "https://registry.yarnpkg.com/nodemon/-/nodemon-1.19.4.tgz#56db5c607408e0fdf8920d2b444819af1aae0971" @@ -1783,6 +1866,15 @@ safe-regex@^1.1.0: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +secp256k1@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/secp256k1/-/secp256k1-4.0.1.tgz#b9570ca26ace9e74c3171512bba253da9c0b6d60" + integrity sha512-iGRjbGAKfXMqhtdkkuNxsgJQfJO8Oo78Rm7DAvsG3XKngq+nJIOGqrCSXcQqIVsmCj0wFanE5uTKFxV3T9j2wg== + dependencies: + elliptic "^6.5.2" + node-addon-api "^2.0.0" + node-gyp-build "^4.2.0" + semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"