[Author Prev][Author Next][Thread Prev][Thread Next][Author Index][Thread Index]

[tor-commits] [stem/master] Merge hsv3_crypto into hidden_service.py



commit 80def07ebfdbb2fe00b85a6721fd3cbb2b4220b2
Author: Damian Johnson <atagar@xxxxxxxxxxxxxx>
Date:   Sat Oct 5 18:41:45 2019 -0700

    Merge hsv3_crypto into hidden_service.py
---
 stem/client/datatype.py                   |   4 +-
 stem/descriptor/hidden_service.py         |  88 +++++++++++++++-----
 stem/descriptor/hsv3_crypto.py            | 128 ------------------------------
 test/unit/descriptor/hidden_service_v3.py |   8 +-
 4 files changed, 76 insertions(+), 152 deletions(-)

diff --git a/stem/client/datatype.py b/stem/client/datatype.py
index 1f49388b..7e33e353 100644
--- a/stem/client/datatype.py
+++ b/stem/client/datatype.py
@@ -628,7 +628,7 @@ class LinkByFingerprint(LinkSpecifier):
     if len(value) != 20:
       raise ValueError('Fingerprint link specifiers should be twenty bytes, but was %i instead: %s' % (len(value), binascii.hexlify(value)))
 
-    self.fingerprint = value
+    self.fingerprint = stem.util.str_tools._to_unicode(value)
 
 
 class LinkByEd25519(LinkSpecifier):
@@ -644,7 +644,7 @@ class LinkByEd25519(LinkSpecifier):
     if len(value) != 32:
       raise ValueError('Fingerprint link specifiers should be thirty two bytes, but was %i instead: %s' % (len(value), binascii.hexlify(value)))
 
-    self.fingerprint = value
+    self.fingerprint = stem.util.str_tools._to_unicode(value)
 
 
 class KDF(collections.namedtuple('KDF', ['key_hash', 'forward_digest', 'backward_digest', 'forward_key', 'backward_key'])):
diff --git a/stem/descriptor/hidden_service.py b/stem/descriptor/hidden_service.py
index e4ee13e3..601348d9 100644
--- a/stem/descriptor/hidden_service.py
+++ b/stem/descriptor/hidden_service.py
@@ -30,9 +30,9 @@ import binascii
 import collections
 import hashlib
 import io
+import struct
 
 import stem.client.datatype
-import stem.descriptor.hsv3_crypto
 import stem.prereq
 import stem.util.connection
 import stem.util.str_tools
@@ -105,6 +105,12 @@ BASIC_AUTH = 1
 STEALTH_AUTH = 2
 CHECKSUM_CONSTANT = b'.onion checksum'
 
+SALT_LEN = 16
+MAC_LEN = 32
+
+S_KEY_LEN = 32
+S_IV_LEN = 16
+
 
 class DecryptionFailure(Exception):
   """
@@ -132,6 +138,8 @@ class IntroductionPointV3(collections.namedtuple('IntroductionPointV3', ['link_s
   """
   Introduction point for a v3 hidden service.
 
+  .. versionadded:: 1.8.0
+
   :var list link_specifiers: :class:`~stem.client.datatype.LinkSpecifier` where this service is reachable
   :var str onion_key: ntor introduction point public key
   :var str auth_key: cross-certifier of the signing key
@@ -194,6 +202,46 @@ def _parse_file(descriptor_file, desc_type = None, validate = False, **kwargs):
       break  # done parsing file
 
 
+def _decrypt_layer(encrypted_block, constant, revision_counter, subcredential, blinded_key):
+  from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+  from cryptography.hazmat.backends import default_backend
+
+  def pack(val):
+    return struct.pack('>Q', val)
+
+  if encrypted_block.startswith('-----BEGIN MESSAGE-----\n') and encrypted_block.endswith('\n-----END MESSAGE-----'):
+    encrypted_block = encrypted_block[24:-22]
+
+  try:
+    encrypted = base64.b64decode(encrypted_block)
+  except:
+    raise ValueError('Unable to decode encrypted block as base64')
+
+  if len(encrypted) < SALT_LEN + MAC_LEN:
+    raise ValueError('Encrypted block malformed (only %i bytes)' % len(encrypted))
+
+  salt = encrypted[:SALT_LEN]
+  ciphertext = encrypted[SALT_LEN:-MAC_LEN]
+  expected_mac = encrypted[-MAC_LEN:]
+
+  kdf = hashlib.shake_256(blinded_key + subcredential + pack(revision_counter) + salt + constant)
+  keys = kdf.digest(S_KEY_LEN + S_IV_LEN + MAC_LEN)
+
+  secret_key = keys[:S_KEY_LEN]
+  secret_iv = keys[S_KEY_LEN:S_KEY_LEN + S_IV_LEN]
+  mac_key = keys[S_KEY_LEN + S_IV_LEN:]
+
+  mac = hashlib.sha3_256(pack(len(mac_key)) + mac_key + pack(len(salt)) + salt + ciphertext).digest()
+
+  if mac != expected_mac:
+    raise ValueError('Malformed mac (expected %s, but was %s)' % (expected_mac, mac))
+
+  cipher = Cipher(algorithms.AES(secret_key), modes.CTR(secret_iv), default_backend())
+  decryptor = cipher.decryptor()
+
+  return stem.util.str_tools._to_unicode(decryptor.update(ciphertext) + decryptor.finalize())
+
+
 def _parse_protocol_versions_line(descriptor, entries):
   value = _value('protocol-versions', entries)
 
@@ -693,14 +741,13 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
     else:
       self._entries = entries
 
-  def decrypt(self, onion_address, validate = False):
+  def decrypt(self, onion_address):
     """
     Decrypt this descriptor. Hidden serice descriptors contain two encryption
     layers (:class:`~stem.descriptor.hidden_service.OuterLayer` and
     :class:`~stem.descriptor.hidden_service.InnerLayer`).
 
     :param str onion_address: hidden service address this descriptor is from
-    :param bool validate: perform validation checks on decrypted content
 
     :returns: :class:`~stem.descriptor.hidden_service.InnerLayer` with our
       decrypted content
@@ -721,19 +768,15 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
       if not blinded_key:
         raise ValueError('No signing key is present')
 
-      identity_public_key = HiddenServiceDescriptorV3._public_key_from_address(onion_address)
-
       # credential = H('credential' | public-identity-key)
       # subcredential = H('subcredential' | credential | blinded-public-key)
 
+      identity_public_key = HiddenServiceDescriptorV3._public_key_from_address(onion_address)
       credential = hashlib.sha3_256(b'credential%s' % (identity_public_key)).digest()
       subcredential = hashlib.sha3_256(b'subcredential%s%s' % (credential, blinded_key)).digest()
 
-      outer_layer = OuterLayer(stem.descriptor.hsv3_crypto.decrypt_outter_layer(self.superencrypted, self.revision_counter, blinded_key, subcredential), validate)
-
-      inner_layer_plaintext = stem.descriptor.hsv3_crypto.decrypt_inner_layer(outer_layer.encrypted, self.revision_counter, blinded_key, subcredential)
-
-      self._inner_layer = InnerLayer(inner_layer_plaintext, validate, outer_layer)
+      outer_layer = OuterLayer._decrypt(self.superencrypted, self.revision_counter, subcredential, blinded_key)
+      self._inner_layer = InnerLayer._decrypt(outer_layer, self.revision_counter, subcredential, blinded_key)
 
     return self._inner_layer
 
@@ -753,16 +796,16 @@ class HiddenServiceDescriptorV3(BaseHiddenServiceDescriptor):
     decoded_address = base64.b32decode(onion_address.upper())
 
     pubkey = decoded_address[:32]
-    checksum = decoded_address[32:34]
+    expected_checksum = decoded_address[32:34]
     version = decoded_address[34:35]
 
-    # validate our address checksum
+    checksum = hashlib.sha3_256(CHECKSUM_CONSTANT + pubkey + version).digest()[:2]
 
-    my_checksum_body = b'%s%s%s' % (CHECKSUM_CONSTANT, pubkey, version)
-    my_checksum = hashlib.sha3_256(my_checksum_body).digest()[:2]
+    if expected_checksum != checksum:
+      checksum_str = stem.util.str_tools._to_unicode(binascii.hexlify(checksum))
+      expected_checksum_str = stem.util.str_tools._to_unicode(binascii.hexlify(expected_checksum))
 
-    if (checksum != my_checksum):
-      raise ValueError('Bad checksum (expected %s but was %s)' % (binascii.hexlify(checksum), binascii.hexlify(my_checksum)))
+      raise ValueError('Bad checksum (expected %s but was %s)' % (expected_checksum_str, checksum_str))
 
     return pubkey
 
@@ -798,8 +841,13 @@ class OuterLayer(Descriptor):
     'encrypted': _parse_v3_outer_encrypted,
   }
 
+  @staticmethod
+  def _decrypt(encrypted, revision_counter, subcredential, blinded_key):
+    plaintext = _decrypt_layer(encrypted, b'hsdir-superencrypted-data', revision_counter, subcredential, blinded_key)
+    return OuterLayer(plaintext)
+
   def __init__(self, content, validate = False):
-    content = content.rstrip(b'\x00')  # strip null byte padding
+    content = content.rstrip('\x00')  # strip null byte padding
 
     super(OuterLayer, self).__init__(content, lazy_load = not validate)
     entries = _descriptor_components(content, validate)
@@ -841,9 +889,13 @@ class InnerLayer(Descriptor):
     'single-onion-service': _parse_v3_inner_single_service,
   }
 
+  @staticmethod
+  def _decrypt(outer_layer, revision_counter, subcredential, blinded_key):
+    plaintext = _decrypt_layer(outer_layer.encrypted, b'hsdir-encrypted-data', revision_counter, subcredential, blinded_key)
+    return InnerLayer(plaintext, outer_layer = outer_layer)
+
   def __init__(self, content, validate = False, outer_layer = None):
     super(InnerLayer, self).__init__(content, lazy_load = not validate)
-
     self.outer = outer_layer
 
     # inner layer begins with a few header fields, followed by multiple any
diff --git a/stem/descriptor/hsv3_crypto.py b/stem/descriptor/hsv3_crypto.py
deleted file mode 100644
index 9acb5242..00000000
--- a/stem/descriptor/hsv3_crypto.py
+++ /dev/null
@@ -1,128 +0,0 @@
-import base64
-import hashlib
-import struct
-
-"""
-Onion addresses
-
-  onion_address = base32(PUBKEY | CHECKSUM | VERSION) + '.onion'
-  CHECKSUM = H('.onion checksum' | PUBKEY | VERSION)[:2]
-
-  - PUBKEY is the 32 bytes ed25519 master pubkey of the hidden service.
-  - VERSION is an one byte version field (default value '\x03')
-  - '.onion checksum' is a constant string
-  - CHECKSUM is truncated to two bytes before inserting it in onion_address
-"""
-
-
-"""
-Blinded key stuff
-
-  Now wrt SRVs, if a client is in the time segment between a new time period
-  and a new SRV (i.e. the segments drawn with '-') it uses the current SRV,
-  else if the client is in a time segment between a new SRV and a new time
-  period (i.e. the segments drawn with '='), it uses the previous SRV.
-"""
-
-pass
-
-"""
-Basic descriptor logic:
-
-  SALT = 16 bytes from H(random), changes each time we rebuld the
-         descriptor even if the content of the descriptor hasn't changed.
-         (So that we don't leak whether the intro point list etc. changed)
-
-  secret_input = SECRET_DATA | subcredential | INT_8(revision_counter)
-
-  keys = KDF(secret_input | salt | STRING_CONSTANT, S_KEY_LEN + S_IV_LEN + MAC_KEY_LEN)
-
-  SECRET_KEY = first S_KEY_LEN bytes of keys
-  SECRET_IV  = next S_IV_LEN bytes of keys
-  MAC_KEY    = last MAC_KEY_LEN bytes of keys
-
-
-Layer data:
-
-  2.5.1.1. First layer encryption logic
-    SECRET_DATA = blinded-public-key
-    STRING_CONSTANT = 'hsdir-superencrypted-data'
-
-  2.5.2.1. Second layer encryption keys
-    SECRET_DATA = blinded-public-key | descriptor_cookie
-    STRING_CONSTANT = 'hsdir-encrypted-data'
-"""
-
-SALT_LEN = 16
-MAC_LEN = 32
-
-S_KEY_LEN = 32
-S_IV_LEN = 16
-MAC_KEY_LEN = 32
-
-
-def _ciphertext_mac_is_valid(key, salt, ciphertext, mac):
-  """
-  Instantiate MAC(key=k, message=m) with H(k_len | k | m), where k_len is
-  htonll(len(k)).
-
-  XXX spec:   H(mac_key_len | mac_key | salt_len | salt | encrypted)
-  """
-
-  # Construct our own MAC first
-  key_len = struct.pack('>Q', len(key))
-  salt_len = struct.pack('>Q', len(salt))
-
-  my_mac_body = b'%s%s%s%s%s' % (key_len, key, salt_len, salt, ciphertext)
-  my_mac = hashlib.sha3_256(my_mac_body).digest()
-
-  # Compare the two MACs
-  return my_mac == mac
-
-
-def _decrypt_descriptor_layer(ciphertext_blob_b64, revision_counter, subcredential, secret_data, string_constant):
-  from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
-  from cryptography.hazmat.backends import default_backend
-
-  if ciphertext_blob_b64.startswith('-----BEGIN MESSAGE-----\n') and ciphertext_blob_b64.endswith('\n-----END MESSAGE-----'):
-    ciphertext_blob_b64 = ciphertext_blob_b64[24:-22]
-
-  # decode the thing
-  ciphertext_blob = base64.b64decode(ciphertext_blob_b64)
-
-  if (len(ciphertext_blob) < SALT_LEN + MAC_LEN):
-    raise ValueError('bad encrypted blob')
-
-  salt = ciphertext_blob[:16]
-  ciphertext = ciphertext_blob[16:-32]
-  mac = ciphertext_blob[-32:]
-
-  # INT_8(revision_counter)
-  rev_counter_int_8 = struct.pack('>Q', revision_counter)
-  secret_input = b'%s%s%s' % (secret_data, subcredential, rev_counter_int_8)
-
-  kdf = hashlib.shake_256(b'%s%s%s' % (secret_input, salt, string_constant))
-  keys = kdf.digest(S_KEY_LEN + S_IV_LEN + MAC_KEY_LEN)
-
-  secret_key = keys[:S_KEY_LEN]
-  secret_iv = keys[S_KEY_LEN:S_KEY_LEN + S_IV_LEN]
-  mac_key = keys[S_KEY_LEN + S_IV_LEN:]
-
-  # Now time to decrypt descriptor
-  cipher = Cipher(algorithms.AES(secret_key), modes.CTR(secret_iv), default_backend())
-  decryptor = cipher.decryptor()
-  decrypted = decryptor.update(ciphertext) + decryptor.finalize()
-
-  # validate mac (the mac validates the two fields before the mac)
-  if not _ciphertext_mac_is_valid(mac_key, salt, ciphertext, mac):
-    raise ValueError('Bad MAC!!!')
-
-  return decrypted
-
-
-def decrypt_outter_layer(superencrypted_blob_b64, revision_counter, blinded_key, subcredential):
-  return _decrypt_descriptor_layer(superencrypted_blob_b64, revision_counter, subcredential, blinded_key, b'hsdir-superencrypted-data')
-
-
-def decrypt_inner_layer(encrypted_blob_b64, revision_counter, blinded_key, subcredential):
-  return _decrypt_descriptor_layer(encrypted_blob_b64, revision_counter, subcredential, blinded_key, b'hsdir-encrypted-data')
diff --git a/test/unit/descriptor/hidden_service_v3.py b/test/unit/descriptor/hidden_service_v3.py
index 093d5cb6..37781b5f 100644
--- a/test/unit/descriptor/hidden_service_v3.py
+++ b/test/unit/descriptor/hidden_service_v3.py
@@ -35,13 +35,13 @@ BDwQZ8rhp05oCqhhY3oFHqG9KS7HGzv9g2v1/PrVJMbkfpwu1YK4b3zIZAk=
 -----END ED25519 CERT-----\
 """
 
-with open(get_resource('hidden_service_v3'), 'rb') as descriptor_file:
+with open(get_resource('hidden_service_v3')) as descriptor_file:
   HS_DESC_STR = descriptor_file.read()
 
-with open(get_resource('hidden_service_v3_outer_layer'), 'rb') as outer_layer_file:
+with open(get_resource('hidden_service_v3_outer_layer')) as outer_layer_file:
   OUTER_LAYER_STR = outer_layer_file.read()
 
-with open(get_resource('hidden_service_v3_inner_layer'), 'rb') as inner_layer_file:
+with open(get_resource('hidden_service_v3_inner_layer')) as inner_layer_file:
   INNER_LAYER_STR = inner_layer_file.read()
 
 
@@ -79,7 +79,7 @@ class TestHiddenServiceDescriptorV3(unittest.TestCase):
     inner_layer = desc.decrypt(HS_ADDRESS)
 
     self.assertEqual(INNER_LAYER_STR, str(inner_layer))
-    self.assertEqual(OUTER_LAYER_STR.rstrip(b'\x00'), str(inner_layer.outer))
+    self.assertEqual(OUTER_LAYER_STR.rstrip('\x00'), str(inner_layer.outer))
 
   def test_outer_layer(self):
     """



_______________________________________________
tor-commits mailing list
tor-commits@xxxxxxxxxxxxxxxxxxxx
https://lists.torproject.org/cgi-bin/mailman/listinfo/tor-commits