/ RNS / Cryptography / X25519.py
X25519.py
  1  # By Nicko van Someren, 2021. This code is released into the public domain.
  2  # Small modifications for use in Reticulum, and constant time key exchange
  3  # added by Mark Qvist in 2022.
  4  
  5  # WARNING! Only the X25519PrivateKey.exchange() method attempts to hide execution time.
  6  # In the context of Reticulum, this is sufficient, but it may not be in other systems. If
  7  # this code is to be used to provide cryptographic security in an environment where the
  8  # start and end times of the execution can be guessed, inferred or measured then it is
  9  # critical that steps are taken to hide the execution time, for instance by adding a
 10  # delay so that encrypted packets are not sent until a fixed time after the _start_ of
 11  # execution.
 12  
 13  
 14  import os
 15  import time
 16  
 17  P = 2 ** 255 - 19
 18  _A = 486662
 19  
 20  
 21  def _point_add(point_n, point_m, point_diff):
 22      """Given the projection of two points and their difference, return their sum"""
 23      (xn, zn) = point_n
 24      (xm, zm) = point_m
 25      (x_diff, z_diff) = point_diff
 26      x = (z_diff << 2) * (xm * xn - zm * zn) ** 2
 27      z = (x_diff << 2) * (xm * zn - zm * xn) ** 2
 28      return x % P, z % P
 29  
 30  
 31  def _point_double(point_n):
 32      """Double a point provided in projective coordinates"""
 33      (xn, zn) = point_n
 34      xn2 = xn ** 2
 35      zn2 = zn ** 2
 36      x = (xn2 - zn2) ** 2
 37      xzn = xn * zn
 38      z = 4 * xzn * (xn2 + _A * xzn + zn2)
 39      return x % P, z % P
 40  
 41  
 42  def _const_time_swap(a, b, swap):
 43      """Swap two values in constant time"""
 44      index = int(swap) * 2
 45      temp = (a, b, b, a)
 46      return temp[index:index+2]
 47  
 48  
 49  def _raw_curve25519(base, n):
 50      """Raise the point base to the power n"""
 51      zero = (1, 0)
 52      one = (base, 1)
 53      mP, m1P = zero, one
 54  
 55      for i in reversed(range(256)):
 56          bit = bool(n & (1 << i))
 57          mP, m1P = _const_time_swap(mP, m1P, bit)
 58          mP, m1P = _point_double(mP), _point_add(mP, m1P, one)
 59          mP, m1P = _const_time_swap(mP, m1P, bit)
 60  
 61      x, z = mP
 62      inv_z = pow(z, P - 2, P)
 63      return (x * inv_z) % P
 64  
 65  
 66  def _unpack_number(s):
 67      """Unpack 32 bytes to a 256 bit value"""
 68      if len(s) != 32:
 69          raise ValueError('Curve25519 values must be 32 bytes')
 70      return int.from_bytes(s, "little")
 71  
 72  
 73  def _pack_number(n):
 74      """Pack a value into 32 bytes"""
 75      return n.to_bytes(32, "little")
 76  
 77  
 78  def _fix_secret(n):
 79      """Mask a value to be an acceptable exponent"""
 80      n &= ~7
 81      n &= ~(128 << 8 * 31)
 82      n |= 64 << 8 * 31
 83      return n
 84  
 85  def _fix_base_point(n):
 86      n &= ~(2**255)
 87      return n
 88  
 89  def curve25519(base_point_raw, secret_raw):
 90      """Raise the base point to a given power"""
 91      base_point = _fix_base_point(_unpack_number(base_point_raw))
 92      secret = _fix_secret(_unpack_number(secret_raw))
 93      return _pack_number(_raw_curve25519(base_point, secret))
 94  
 95  
 96  def curve25519_base(secret_raw):
 97      """Raise the generator point to a given power"""
 98      secret = _fix_secret(_unpack_number(secret_raw))
 99      return _pack_number(_raw_curve25519(9, secret))
100  
101  
102  class X25519PublicKey:
103      def __init__(self, x):
104          self.x = x
105  
106      @classmethod
107      def from_public_bytes(cls, data):
108          return cls(_unpack_number(data))
109  
110      def public_bytes(self):
111          return _pack_number(self.x)
112  
113  
114  class X25519PrivateKey:
115      MIN_EXEC_TIME = 0.002
116      MAX_EXEC_TIME = 0.5
117      DELAY_WINDOW = 10
118  
119      T_CLEAR = None
120      T_MAX = 0
121  
122      def __init__(self, a):
123          self.a = a
124  
125      @classmethod
126      def generate(cls):
127          return cls.from_private_bytes(os.urandom(32))
128  
129      @classmethod
130      def from_private_bytes(cls, data):
131          return cls(_fix_secret(_unpack_number(data)))
132  
133      def private_bytes(self):
134          return _pack_number(self.a)
135  
136      def public_key(self):
137          return X25519PublicKey.from_public_bytes(_pack_number(_raw_curve25519(9, self.a)))
138  
139      def exchange(self, peer_public_key):
140          if isinstance(peer_public_key, bytes):
141              peer_public_key = X25519PublicKey.from_public_bytes(peer_public_key)
142  
143          start = time.time()
144          
145          shared = _pack_number(_raw_curve25519(peer_public_key.x, self.a))
146          
147          end = time.time()
148          duration = end-start
149  
150          if X25519PrivateKey.T_CLEAR == None:
151              X25519PrivateKey.T_CLEAR = end + X25519PrivateKey.DELAY_WINDOW
152  
153          if end > X25519PrivateKey.T_CLEAR:
154              X25519PrivateKey.T_CLEAR = end + X25519PrivateKey.DELAY_WINDOW
155              X25519PrivateKey.T_MAX = 0
156          
157          if duration < X25519PrivateKey.T_MAX or duration < X25519PrivateKey.MIN_EXEC_TIME:
158              target = start+X25519PrivateKey.T_MAX
159  
160              if target > start+X25519PrivateKey.MAX_EXEC_TIME:
161                  target = start+X25519PrivateKey.MAX_EXEC_TIME
162  
163              if target < start+X25519PrivateKey.MIN_EXEC_TIME:
164                  target = start+X25519PrivateKey.MIN_EXEC_TIME
165  
166              try:
167                  time.sleep(target-time.time())
168              except Exception as e:
169                  pass
170  
171          elif duration > X25519PrivateKey.T_MAX:
172              X25519PrivateKey.T_MAX = duration
173  
174          return shared