diff --git a/observer.py b/observer.py index 267e1a7..a18e12b 100644 --- a/observer.py +++ b/observer.py @@ -9,6 +9,7 @@ import statistics import string import requests import time +import base64 from base64 import b32encode, b16decode from werkzeug.routing import BaseConverter from pygments import highlight @@ -17,7 +18,9 @@ from pygments.formatters import HtmlFormatter import subprocess import qrcode from io import BytesIO - +import pysodium +import nacl.encoding +import nacl.hash import config import local_config from lmq import FutureJSON, lmq_connection @@ -414,6 +417,109 @@ def block_with_txs_req(lmq, oxend, hash_or_height, **kwargs): return FutureJSON(lmq, oxend, 'rpc.get_block', cache_key='single', args=args, **kwargs) +def ons_info(lmq, oxend, name,ons_type,**kwargs): + if ons_type == 2: + name=name+'.loki' + name_hash = nacl.hash.blake2b(name.encode(), encoder = nacl.encoding.Base64Encoder) + + return FutureJSON(lmq, oxend, 'rpc.ons_names_to_owners', args={ + "entries": [{'name_hash':name_hash.decode('ascii'),'types':[ons_type]}]}) + + +@app.route('/ons/') +@app.route('/ons//') +def show_ons(name, more_details=False): + name = name.lower() + lmq, oxend = lmq_connection() + info = FutureJSON(lmq, oxend, 'rpc.get_info', 1) + if len(name) > 64 or not all(c.isalnum() or c in '_-' for c in name): + return flask.render_template('not_found.html', + info=info.get(), + type='bad_search', + id=name, + ) + + ons_types = {'session':0,'wallet':1,'lokinet':2} + ons_data = {'name':name} + SESSION_ENCRYPTED_LENGTH = 146 # If the encrypted value is not of expected character + WALLET_ENCRYPTED_LENGTH = 210 # length it is of HF15 and before. + LOKINET_ENCRYPTED_LENGTH = 144 # The user must update their session mapping. + + + for ons_type in ons_types: + onsinfo = ons_info(lmq, oxend, name, ons_types[ons_type]).get() + if 'entries' in onsinfo: + onsinfo = onsinfo['entries'][0] + ons_data[ons_type] = onsinfo + + if len(onsinfo['encrypted_value']) in [SESSION_ENCRYPTED_LENGTH, WALLET_ENCRYPTED_LENGTH, LOKINET_ENCRYPTED_LENGTH]: + # RPC returns encrypted_value as ciphertext and nonce concatenated. + # The nonce is the last 48 characters of the encrypted value and the remainder of characters is the encrypted_value. + nonce_received = onsinfo['encrypted_value'][-48:] + nonce = bytes.fromhex(nonce_received) + + # The ciphertext is the encrypted_value with the nonce taken away. + ciphertext = bytes.fromhex(onsinfo['encrypted_value'][:-48]) + + # If ons type is lokinet we need to add .loki to the name before hashing. + if ons_types[ons_type] == 2: + name+='.loki' + + # Calculate the blake2b hash of the lower-case full name + name_hash = nacl.hash.blake2b(name.encode(),encoder = nacl.encoding.RawEncoder) + + # Decryption key: another blake2b hash, but this time a keyed blake2b hash where the first hash is the key + decryption_key = nacl.hash.blake2b(name.encode(), key=name_hash, encoder = nacl.encoding.RawEncoder) + + # XChaCha20+Poly1305 decryption + val = pysodium.crypto_aead_xchacha20poly1305_ietf_decrypt(ciphertext=ciphertext, ad=b'', nonce=nonce, key=decryption_key) + + # lokinet check + print(ons_types[ons_type]) + if ons_types[ons_type] == 2: + # val will currently be the raw lokinet ed25519 pubkey (32 bytes). We can convert it to the more + # common lokinet address (which is the same value but encoded in z-base-32) and convert the bytes to + # a string: + val = b32encode(val).decode() + + # Python's regular base32 uses a different alphabet, so translate from base32 to z-base-32: + val = val.translate(str.maketrans("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", + "ybndrfg8ejkmcpqxot1uwisza345h769")) + + # Base32 is also padded with '=', which isn't used in z-base-32: + val = val.rstrip('=') + + # Finally slap ".loki" on the end: + val += ".loki" + + ons_data[ons_type]['mapping'] = val + else: + ons_data[ons_type]['mapping'] = val.hex() + else: + # Encryption involves a much more expensive argon2-based calculation for HF15 registrations. + # Owners should be notified they should update to the new encryption format. + ons_data[ons_type] = ons_info(lmq, oxend, name,ons_types[ons_type]).get()['entries'][0] + ons_data[ons_type]['mapping'] = 'Owner needs to update their ID for mapping info.' + else: + # If returned with no data from the RPC + ons_data[ons_type] = True + + if more_details: + formatter = HtmlFormatter(cssclass="syntax-highlight", style="paraiso-dark") + more_details = { + 'details_css': formatter.get_style_defs('.syntax-highlight'), + 'details_html': highlight(json.dumps(ons_data, indent="\t"), JsonLexer(), formatter), + } + else: + more_details = {} + + + return flask.render_template('ons.html', + info=info.get(), + ons=ons_data, + **more_details, + ) + @app.route('/sn/') @app.route('/sn//') @@ -687,12 +793,12 @@ def show_quorums(): base32z_dict = 'ybndrfg8ejkmcpqxot1uwisza345h769' base32z_map = {base32z_dict[i]: i for i in range(len(base32z_dict))} + @app.route('/search') def search(): lmq, oxend = lmq_connection() info = FutureJSON(lmq, oxend, 'rpc.get_info', 1) val = (flask.request.args.get('value') or '').strip() - if val and len(val) < 10 and val.isdigit(): # Block height return flask.redirect(flask.url_for('show_block', height=val), code=301) @@ -704,34 +810,43 @@ def search(): v >>= 4 val = "{:64x}".format(v) + if len(val) == 64: + # Initiate all the lookups at once, then redirect to whichever one responds affirmatively + snreq = sn_req(lmq, oxend, val) + blreq = block_header_req(lmq, oxend, val, fail_okay=True) + txreq = tx_req(lmq, oxend, [val]) + + sn = snreq.get() + if sn and 'service_node_states' in sn and sn['service_node_states']: + return flask.redirect(flask.url_for('show_sn', pubkey=val), code=301) + + bl = blreq.get() + if bl and 'block_header' in bl and bl['block_header']: + return flask.redirect(flask.url_for('show_block', hash=val), code=301) + + tx = txreq.get() + if tx and 'txs' in tx and tx['txs']: + return flask.redirect(flask.url_for('show_tx', txid=val), code=301) + + # ONS can be of length 64 however with txids, and sn pubkey's being of length 64 + # I have removed it from the possible searches. + if len(val) < 64 and all(c.isalnum() or c in '_-' for c in val): + return flask.redirect(flask.url_for('show_ons', name=val), code=301) + elif not val or len(val) != 64 or any(c not in string.hexdigits for c in val): return flask.render_template('not_found.html', - info=info.get(), - type='bad_search', - id=val, - ) - - # Initiate all the lookups at once, then redirect to whichever one responds affirmatively - snreq = sn_req(lmq, oxend, val) - blreq = block_header_req(lmq, oxend, val, fail_okay=True) - txreq = tx_req(lmq, oxend, [val]) - - sn = snreq.get() - if sn and 'service_node_states' in sn and sn['service_node_states']: - return flask.redirect(flask.url_for('show_sn', pubkey=val), code=301) - bl = blreq.get() - if bl and 'block_header' in bl and bl['block_header']: - return flask.redirect(flask.url_for('show_block', hash=val), code=301) - tx = txreq.get() - if tx and 'txs' in tx and tx['txs']: - return flask.redirect(flask.url_for('show_tx', txid=val), code=301) + info=info.get(), + type='bad_search', + id=val, + ) return flask.render_template('not_found.html', info=info.get(), - type='search', + type='bad_search', id=val, ) + @app.route('/api/networkinfo') def api_networkinfo(): lmq, oxend = lmq_connection() diff --git a/templates/ons.html b/templates/ons.html new file mode 100644 index 0000000..6d0c339 --- /dev/null +++ b/templates/ons.html @@ -0,0 +1,69 @@ +{% extends "_basic.html" %} + +{% block content %} + + +
+
+
+

Oxen Name Server Lookup "{{ons.name}}"

+
+

+
+ {%if ons.session == True%} +

Yes

+ {%else%} +

No

+

{{ons.session.owner}}

+

{{ons.session.txid}}

+ +

{{ons.session.update_height}}

+

{{ons.session.name_hash}}

+

{{ons.session.mapping}}

+ {%endif%} + +

+
+ {%if ons.wallet == True%} + {%if ons.session == True%} +

Yes

+ {%else%} +

Only available for above Session ID Owner

+ {%endif%} + {%else%} +

No

+

{{ons.wallet.owner}}

+

{{ons.wallet.txid}}

+

{{ons.wallet.update_height}}

+

{{ons.wallet.name_hash}}

+

{{ons.wallet.mapping}}

+ {%endif%} + +

+
+ {%if ons.lokinet == True%} +

Yes

+ {%else%} +

No

+

{{ons.lokinet.owner}}

+

{{ons.lokinet.txid}}

+

{{ons.lokinet.update_height}}

+

{{ons.lokinet.expiration_height}}

+

{{ons.lokinet.name_hash}}

+

{{ons.lokinet.mapping}}

+ {%endif%} +
+
+ {%if details_html%} + +
+ {{details_html | safe}} + {%else%} +
+ Show raw details +
+ {%endif%} +
+{% endblock %} \ No newline at end of file