diff --git a/app/background/hip2/hip2.js b/app/background/hip2/hip2.js new file mode 100644 index 000000000..75fc218b2 --- /dev/null +++ b/app/background/hip2/hip2.js @@ -0,0 +1,95 @@ +// Based on: +// - https://github.com/Falci/well-known-wallets-hns/blob/master/lib.js +// - https://github.com/lukeburns/hip2-dane/blob/main/index.js + +import isValidAddress from '../../utils/verifyAddress'; + +const hdns = require('hdns'); +const https = require('https'); + +const MAX_LENGTH = 90; + +const verifyTLSA = async (cert, host) => { + try { + const tlsa = await hdns.resolveTLSA(host, 'tcp', 443); + const valid = hdns.verifyTLSA(tlsa[0], cert.raw); + + return valid; + } catch (e) { + console.error(e); + return false; + } +}; + +export async function getAddress(host, network) { + let certificate = undefined; + + return new Promise(async (resolve, reject) => { + const options = { + rejectUnauthorized: false, + lookup: hdns.legacy, + }; + + const req = https.get(`https://${host}/.well-known/wallets/HNS`, options, res => { + res.setEncoding('utf8'); + + let data = ''; + + res.on('data', chunk => { + // undefined = not yet stored + // null = socket destroyed + // object = may contain certificate + if (certificate === undefined) { + certificate = res.socket.getPeerCertificate(false); + } + + const newLine = chunk.indexOf('\n'); + if (newLine >= 0) { + req.destroy(); + chunk = chunk.slice(0, newLine); + } + + if (data.length + chunk.length > MAX_LENGTH) { + if (!req.destroyed) { + req.destroy(); + } + const error = new Error('response too large'); + error.code = 'ELARGE'; + return reject(error); + } + + data += chunk; + }) + + res.on('end', async () => { + const dane = await verifyTLSA(certificate, host); + if (!dane) { + const error = new Error('invalid DANE'); + error.code = 'EINSECURE'; + return reject(error); + } + + if (res.statusCode >= 400) { + const error = new Error(res.statusMessage); + error.code = res.statusCode; + return reject(error); + } + + const addr = data.trim(); + + if (!isValidAddress(addr, network)) { + const error = new Error('invalid address'); + error.code = 'EINVALID'; + return reject(error); + } + + return resolve(data.trim()); + }); + }); + + req.on('error', reject); + req.end(); + }); +} + +export const { setServers } = hdns; diff --git a/app/background/hip2/service.js b/app/background/hip2/service.js index 939c57d6d..e0840fd50 100644 --- a/app/background/hip2/service.js +++ b/app/background/hip2/service.js @@ -2,11 +2,12 @@ import { SET_HIP2_PORT } from '../../ducks/hip2Reducer'; import { get, put } from '../db/service'; import { dispatchToMainWindow } from '../../mainWindow'; import { service } from '../node/service'; -const { fetchAddress, setServers } = require('hip2-dane'); +import { setServers, getAddress } from './hip2'; +import isValidAddress from '../../utils/verifyAddress'; const HIP2_PORT = 'hip2/port'; -async function getPort () { +async function getPort() { const hip2Port = await get(HIP2_PORT); if (hip2Port !== null) { return hip2Port; @@ -15,7 +16,7 @@ async function getPort () { return 9892; } -async function setPort (port) { +async function setPort(port) { await put(HIP2_PORT, port); dispatchToMainWindow({ type: SET_HIP2_PORT, @@ -23,28 +24,28 @@ async function setPort (port) { }); } -const sName = 'Hip2'; +async function fetchAddress(host) { + const network = service.network.type; -const networkPrefix = { - simnet: 'ss1', - testnet: 'ts1', - main: 'hs1', - regtest: 'rs1' -}; + // Host should not be a Handshake address + if (isValidAddress(host, network)) { + const error = new Error('alias cannot be a valid address') + error.code = 'ECOLLISION' + throw error; + } + + return await getAddress(host, network); +} + +const sName = 'Hip2'; const methods = { getPort, setPort, - fetchAddress: async address => await fetchAddress(address, { - token: 'HNS', - maxLength: 90, - validate: addr => { - return typeof addr === 'string' && addr.slice(0, 3) === networkPrefix[service.network.type]; - } - }), - setServers + fetchAddress, + setServers, }; -export async function start (server) { +export async function start(server) { server.withService(sName, methods); } diff --git a/app/components/SendModal/index.js b/app/components/SendModal/index.js index 3215a723f..896ce10a4 100644 --- a/app/components/SendModal/index.js +++ b/app/components/SendModal/index.js @@ -105,7 +105,7 @@ class SendModal extends Component { if (input) { this.setState({ hip2Loading: true }) - // delay lookup by 120ms + // delay lookup by 120ms setTimeout(() => { // abort lookup if input has changed if (!(this.state.hip2Input && this.state.hip2To === input)) return @@ -124,7 +124,7 @@ class SendModal extends Component { hip2Error = this.context.t('hip2InvalidAddress') } else if (err.code === 'ECOLLISION') { hip2Error = this.context.t('hip2InvalidAlias') - } else if (err.code === -1) { + } else if (err.code === 'EINSECURE') { hip2Error = this.context.t('hip2InvalidTLSA') } else { hip2Error = this.context.t('hip2AddressNotFound') @@ -310,20 +310,20 @@ class SendModal extends Component {
{t('sendToLabel')}
- {hip2Input && + {hip2Input && {to ? ( - ) : ( hip2Loading ? - + ) : ( hip2Loading ? + : '@')} }