From be0e724b2394c3c704147ea61f402b4757f4e806 Mon Sep 17 00:00:00 2001
From: f97ada87 <f97ada87@cock.li>
Date: Sat, 30 Sep 2017 19:19:44 +1000
Subject: [PATCH] implement stealth ack objects

---
 src/api.py                       |  6 +++--
 src/bitmessagecurses/__init__.py |  7 ++++--
 src/bitmessageqt/__init__.py     |  6 +++--
 src/bitmessageqt/account.py      |  4 +++-
 src/class_objectProcessor.py     | 17 +++++---------
 src/class_singleWorker.py        | 19 +++++++++++----
 src/class_smtpServer.py          |  4 +++-
 src/helper_ackPayload.py         | 40 ++++++++++++++++++++++++++++++++
 8 files changed, 79 insertions(+), 24 deletions(-)
 create mode 100644 src/helper_ackPayload.py

diff --git a/src/api.py b/src/api.py
index e20854fc..edb9e23d 100644
--- a/src/api.py
+++ b/src/api.py
@@ -35,6 +35,7 @@ import network.stats
 
 # Classes
 from helper_sql import sqlQuery,sqlExecute,SqlBulkExecute,sqlStoredProcedure
+from helper_ackPayload import genAckPayload
 from debug import logger
 from inventory import Inventory
 from version import softwareVersion
@@ -679,7 +680,8 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
         if not fromAddressEnabled:
             raise APIError(14, 'Your fromAddress is disabled. Cannot send.')
 
-        ackdata = OpenSSL.rand(32)
+        stealthLevel = BMConfigParser().safeGetInt('bitmessagesettings', 'ackstealthlevel')
+        ackdata = genAckPayload(streamNumber, stealthLevel)
 
         t = ('', 
              toAddress, 
@@ -740,7 +742,7 @@ class MySimpleXMLRPCRequestHandler(SimpleXMLRPCRequestHandler):
                 fromAddress, 'enabled')
         except:
             raise APIError(13, 'could not find your fromAddress in the keys.dat file.')
-        ackdata = OpenSSL.rand(32)
+        ackdata = genAckPayload(streamNumber, 0)
         toAddress = '[Broadcast subscribers]'
         ripe = ''
 
diff --git a/src/bitmessagecurses/__init__.py b/src/bitmessagecurses/__init__.py
index 381d7c7a..fc1d74b2 100644
--- a/src/bitmessagecurses/__init__.py
+++ b/src/bitmessagecurses/__init__.py
@@ -20,6 +20,7 @@ import curses
 import dialog
 from dialog import Dialog
 from helper_sql import *
+from helper_ackPayload import genAckPayload
 
 from addresses import *
 import ConfigParser
@@ -778,7 +779,8 @@ def sendMessage(sender="", recv="", broadcast=None, subject="", body="", reply=F
                     if len(shared.connectedHostsList) == 0:
                         set_background_title(d, "Not connected warning")
                         scrollbox(d, unicode("Because you are not currently connected to the network, "))
-                    ackdata = OpenSSL.rand(32)
+                    stealthLevel = BMConfigParser().safeGetInt('bitmessagesettings', 'ackstealthlevel')
+                    ackdata = genAckPayload(streamNumber, stealthLevel)
                     sqlExecute(
                         "INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
                         "",
@@ -802,7 +804,8 @@ def sendMessage(sender="", recv="", broadcast=None, subject="", body="", reply=F
             set_background_title(d, "Empty sender error")
             scrollbox(d, unicode("You must specify an address to send the message from."))
         else:
-            ackdata = OpenSSL.rand(32)
+            # dummy ackdata, no need for stealth
+            ackdata = genAckPayload(streamNumber, 0)
             recv = BROADCAST_STR
             ripe = ""
             sqlExecute(
diff --git a/src/bitmessageqt/__init__.py b/src/bitmessageqt/__init__.py
index bb90bb47..09669616 100644
--- a/src/bitmessageqt/__init__.py
+++ b/src/bitmessageqt/__init__.py
@@ -52,6 +52,7 @@ import random
 import string
 from datetime import datetime, timedelta
 from helper_sql import *
+from helper_ackPayload import genAckPayload
 import helper_search
 import l10n
 import openclpow
@@ -1879,7 +1880,8 @@ class MyForm(settingsmixin.SMainWindow):
                         if shared.statusIconColor == 'red':
                             self.statusBar().showMessage(_translate(
                                 "MainWindow", "Warning: You are currently not connected. Bitmessage will do the work necessary to send the message but it won\'t send until you connect."))
-                        ackdata = OpenSSL.rand(32)
+                        stealthLevel = BMConfigParser().safeGetInt('bitmessagesettings', 'ackstealthlevel')
+                        ackdata = genAckPayload(streamNumber, stealthLevel)
                         t = ()
                         sqlExecute(
                             '''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',
@@ -1933,7 +1935,7 @@ class MyForm(settingsmixin.SMainWindow):
                 # We don't actually need the ackdata for acknowledgement since
                 # this is a broadcast message, but we can use it to update the
                 # user interface when the POW is done generating.
-                ackdata = OpenSSL.rand(32)
+                ackdata = genAckPayload(streamNumber, 0)
                 toAddress = str_broadcast_subscribers
                 ripe = ''
                 t = ('', # msgid. We don't know what this will be until the POW is done. 
diff --git a/src/bitmessageqt/account.py b/src/bitmessageqt/account.py
index eee6c7b4..92d497f8 100644
--- a/src/bitmessageqt/account.py
+++ b/src/bitmessageqt/account.py
@@ -5,6 +5,7 @@ import re
 import sys
 import inspect
 from helper_sql import *
+from helper_ackPayload import genAckPayload
 from addresses import decodeAddress
 from bmconfigparser import BMConfigParser
 from foldertree import AccountMixin
@@ -166,7 +167,8 @@ class GatewayAccount(BMAccount):
         
     def send(self):
         status, addressVersionNumber, streamNumber, ripe = decodeAddress(self.toAddress)
-        ackdata = OpenSSL.rand(32)
+        stealthLevel = BMConfigParser().safeGetInt('bitmessagesettings', 'ackstealthlevel')
+        ackdata = genAckPayload(streamNumber, stealthLevel)
         t = ()
         sqlExecute(
             '''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',
diff --git a/src/class_objectProcessor.py b/src/class_objectProcessor.py
index e59645bc..876eff62 100644
--- a/src/class_objectProcessor.py
+++ b/src/class_objectProcessor.py
@@ -21,6 +21,7 @@ import helper_inbox
 import helper_msgcoding
 import helper_sent
 from helper_sql import *
+from helper_ackPayload import genAckPayload
 import protocol
 import queues
 import state
@@ -97,15 +98,9 @@ class objectProcessor(threading.Thread):
         # Let's check whether this is a message acknowledgement bound for us.
         if len(data) < 32:
             return
-        readPosition = 20  # bypass the nonce, time, and object type
-        # chomp version number
-        versionNumber, varIntLength = decodeVarint(
-            data[readPosition:readPosition + 10])
-        readPosition += varIntLength
-        # chomp stream number
-        streamNumber, varIntLength = decodeVarint(
-            data[readPosition:readPosition + 10])
-        readPosition += varIntLength
+
+        # bypass nonce and time, retain object type/version/stream + body
+        readPosition = 16
 
         if data[readPosition:] in shared.ackdataForWhichImWatching:
             logger.info('This object is an acknowledgement bound for me.')
@@ -558,8 +553,8 @@ class objectProcessor(threading.Thread):
                 message = time.strftime("%a, %Y-%m-%d %H:%M:%S UTC", time.gmtime(
                 )) + '   Message ostensibly from ' + fromAddress + ':\n\n' + body
                 fromAddress = toAddress  # The fromAddress for the broadcast that we are about to send is the toAddress (my address) for the msg message we are currently processing.
-                ackdataForBroadcast = OpenSSL.rand(
-                    32)  # We don't actually need the ackdataForBroadcast for acknowledgement since this is a broadcast message but we can use it to update the user interface when the POW is done generating.
+                # We don't actually need the ackdataForBroadcast for acknowledgement since this is a broadcast message but we can use it to update the user interface when the POW is done generating.
+                ackdata = genAckPayload(streamNumber, 0)
                 toAddress = '[Broadcast subscribers]'
                 ripe = ''
 
diff --git a/src/class_singleWorker.py b/src/class_singleWorker.py
index 58eb33c6..322bb20e 100644
--- a/src/class_singleWorker.py
+++ b/src/class_singleWorker.py
@@ -81,6 +81,16 @@ class singleWorker(threading.Thread, StoppableThread):
             logger.info('Watching for ackdata ' + hexlify(ackdata))
             shared.ackdataForWhichImWatching[ackdata] = 0
 
+        # Fix legacy (headerless) watched ackdata to include header
+        for oldack in shared.ackdataForWhichImWatching.keys():
+            if (len(oldack)==32):
+                # attach legacy header, always constant (msg/1/1)
+                newack = '\x00\x00\x00\x02\x01\x01' + oldack
+                shared.ackdataForWhichImWatching[newack] = 0
+                sqlExecute('UPDATE sent SET ackdata=? WHERE ackdata=?',
+                       newack, oldack )
+                del shared.ackdataForWhichImWatching[oldack]
+
         self.stop.wait(
             10)  # give some time for the GUI to start before we start on existing POW tasks.
 
@@ -967,11 +977,10 @@ class singleWorker(threading.Thread, StoppableThread):
             TTL = 28*24*60*60 # 4 weeks
         TTL = int(TTL + random.randrange(-300, 300)) # Add some randomness to the TTL
         embeddedTime = int(time.time() + TTL)
-        payload = pack('>Q', (embeddedTime))
-        payload += '\x00\x00\x00\x02' # object type: msg
-        payload += encodeVarint(1) # msg version
-        payload += encodeVarint(toStreamNumber) + ackdata
-        
+
+        # type/version/stream already included 
+        payload = pack('>Q', (embeddedTime)) + ackdata
+
         target = 2 ** 64 / (defaults.networkDefaultProofOfWorkNonceTrialsPerByte*(len(payload) + 8 + defaults.networkDefaultPayloadLengthExtraBytes + ((TTL*(len(payload)+8+defaults.networkDefaultPayloadLengthExtraBytes))/(2 ** 16))))
         logger.info('(For ack message) Doing proof of work. TTL set to ' + str(TTL))
 
diff --git a/src/class_smtpServer.py b/src/class_smtpServer.py
index 3bc81a61..b62a7130 100644
--- a/src/class_smtpServer.py
+++ b/src/class_smtpServer.py
@@ -14,6 +14,7 @@ from addresses import decodeAddress
 from bmconfigparser import BMConfigParser
 from debug import logger
 from helper_sql import sqlExecute
+from helper_ackPayload import genAckPayload
 from helper_threading import StoppableThread
 from pyelliptic.openssl import OpenSSL
 import queues
@@ -65,7 +66,8 @@ class smtpServerPyBitmessage(smtpd.SMTPServer):
 
     def send(self, fromAddress, toAddress, subject, message):
         status, addressVersionNumber, streamNumber, ripe = decodeAddress(toAddress)
-        ackdata = OpenSSL.rand(32)
+        stealthLevel = BMConfigParser().safeGetInt('bitmessagesettings', 'ackstealthlevel')
+        ackdata = genAckPayload(streamNumber, stealthLevel)
         t = ()
         sqlExecute(
             '''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''',
diff --git a/src/helper_ackPayload.py b/src/helper_ackPayload.py
new file mode 100644
index 00000000..ef99ec2a
--- /dev/null
+++ b/src/helper_ackPayload.py
@@ -0,0 +1,40 @@
+import hashlib
+import highlevelcrypto
+import random
+import helper_random
+from binascii import hexlify, unhexlify
+from struct import pack, unpack
+from addresses import encodeVarint
+
+# This function generates payload objects for message acknowledgements
+# Several stealth levels are available depending on the privacy needs; 
+# a higher level means better stealth, but also higher cost (size+POW)
+#   - level 0: a random 32-byte sequence with a message header appended
+#   - level 1: a getpubkey request for a (random) dummy key hash
+#   - level 2: a standard message, encrypted to a random pubkey
+
+def genAckPayload(streamNumber=1, stealthLevel=0):
+    if (stealthLevel==2):      # Generate privacy-enhanced payload
+        # Generate a dummy privkey and derive the pubkey
+        dummyPubKeyHex = highlevelcrypto.privToPub(hexlify(helper_random.randomBytes(32)))
+        # Generate a dummy message of random length
+        # (the smallest possible standard-formatted message is 234 bytes)
+        dummyMessage = helper_random.randomBytes(random.randint(234, 800))
+        # Encrypt the message using standard BM encryption (ECIES)
+        ackdata = highlevelcrypto.encrypt(dummyMessage, dummyPubKeyHex)
+        acktype = 2  # message
+        version = 1
+
+    elif (stealthLevel==1):    # Basic privacy payload (random getpubkey)
+        ackdata = helper_random.randomBytes(32)
+        acktype = 0  # getpubkey
+        version = 4
+
+    else:            # Minimum viable payload (non stealth)
+        ackdata = helper_random.randomBytes(32)
+        acktype = 2  # message
+        version = 1
+
+    ackobject = pack('>I', acktype) + encodeVarint(version) + encodeVarint(streamNumber) + ackdata
+
+    return ackobject
-- 
2.45.1