2022-04-01 17:18:18 +02:00
# MIT License
#
2023-09-29 10:31:20 +02:00
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
2022-04-01 17:18:18 +02:00
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
2018-03-16 11:40:37 +01:00
import math
2018-03-19 18:11:50 +01:00
import os
2018-04-04 14:14:22 +02:00
import RNS
2018-03-19 20:51:26 +01:00
import time
import atexit
2022-06-07 12:51:41 +02:00
import hashlib
2020-04-22 12:07:13 +02:00
from . vendor import umsgpack as umsgpack
2022-06-08 12:29:51 +02:00
2022-06-08 19:47:09 +02:00
from RNS . Cryptography import X25519PrivateKey , X25519PublicKey , Ed25519PrivateKey , Ed25519PublicKey
2022-06-08 12:29:51 +02:00
from RNS . Cryptography import Fernet
2018-03-16 10:50:37 +01:00
2022-03-08 00:38:51 +01:00
2018-03-16 10:50:37 +01:00
class Identity :
2021-05-16 21:58:50 +02:00
"""
This class is used to manage identities in Reticulum . It provides methods
for encryption , decryption , signatures and verification , and is the basis
for all encrypted communication over Reticulum networks .
2021-05-20 15:31:38 +02:00
: param create_keys : Specifies whether new encryption and signing keys should be generated .
2021-05-16 21:58:50 +02:00
"""
2021-05-20 15:31:38 +02:00
CURVE = " Curve25519 "
2021-05-16 21:58:50 +02:00
"""
2021-05-20 15:31:38 +02:00
The curve used for Elliptic Curve DH key exchanges
2021-05-16 21:58:50 +02:00
"""
2020-08-13 12:15:56 +02:00
2021-05-20 15:31:38 +02:00
KEYSIZE = 256 * 2
"""
X25519 key size in bits . A complete key is the concatenation of a 256 bit encryption key , and a 256 bit signing key .
"""
2020-08-13 12:15:56 +02:00
2021-05-20 15:31:38 +02:00
# Non-configurable constants
2022-06-08 12:52:42 +02:00
FERNET_OVERHEAD = RNS . Cryptography . Fernet . FERNET_OVERHEAD
2022-04-27 13:21:53 +02:00
AES128_BLOCKSIZE = 16 # In bytes
HASHLENGTH = 256 # In bits
SIGLENGTH = KEYSIZE # In bits
2020-08-13 12:15:56 +02:00
2022-10-06 23:14:32 +02:00
NAME_HASH_LENGTH = 80
2021-09-02 18:00:03 +02:00
TRUNCATED_HASHLENGTH = RNS . Reticulum . TRUNCATED_HASHLENGTH
2021-05-16 21:58:50 +02:00
"""
Constant specifying the truncated hash length ( in bits ) used by Reticulum
2021-05-20 15:31:38 +02:00
for addressable hashes and other purposes . Non - configurable .
2021-05-16 21:58:50 +02:00
"""
2020-08-13 12:15:56 +02:00
# Storage
known_destinations = { }
@staticmethod
def remember ( packet_hash , destination_hash , public_key , app_data = None ) :
2021-05-20 22:30:54 +02:00
if len ( public_key ) != Identity . KEYSIZE / / 8 :
raise TypeError ( " Can ' t remember " + RNS . prettyhexrep ( destination_hash ) + " , the public key size of " + str ( len ( public_key ) ) + " is not valid. " , RNS . LOG_ERROR )
else :
Identity . known_destinations [ destination_hash ] = [ time . time ( ) , packet_hash , public_key , app_data ]
2020-08-13 12:15:56 +02:00
@staticmethod
def recall ( destination_hash ) :
2021-05-16 21:58:50 +02:00
"""
Recall identity for a destination hash .
: param destination_hash : Destination hash as * bytes * .
: returns : An : ref : ` RNS . Identity < api - identity > ` instance that can be used to create an outgoing : ref : ` RNS . Destination < api - destination > ` , or * None * if the destination is unknown .
"""
2020-08-13 12:15:56 +02:00
if destination_hash in Identity . known_destinations :
identity_data = Identity . known_destinations [ destination_hash ]
2021-05-20 15:31:38 +02:00
identity = Identity ( create_keys = False )
2021-05-16 16:15:57 +02:00
identity . load_public_key ( identity_data [ 2 ] )
2021-05-13 16:41:23 +02:00
identity . app_data = identity_data [ 3 ]
2020-08-13 12:15:56 +02:00
return identity
else :
2022-10-13 20:43:38 +02:00
for registered_destination in RNS . Transport . destinations :
if destination_hash == registered_destination . hash :
identity = Identity ( create_keys = False )
identity . load_public_key ( registered_destination . identity . get_public_key ( ) )
identity . app_data = None
return identity
2020-08-13 12:15:56 +02:00
return None
2021-05-13 16:41:23 +02:00
@staticmethod
def recall_app_data ( destination_hash ) :
2021-05-16 21:58:50 +02:00
"""
Recall last heard app_data for a destination hash .
: param destination_hash : Destination hash as * bytes * .
: returns : * Bytes * containing app_data , or * None * if the destination is unknown .
"""
2021-05-13 16:41:23 +02:00
if destination_hash in Identity . known_destinations :
app_data = Identity . known_destinations [ destination_hash ] [ 3 ]
return app_data
else :
return None
2020-08-13 12:15:56 +02:00
@staticmethod
2021-05-16 16:15:57 +02:00
def save_known_destinations ( ) :
2022-09-06 19:43:46 +02:00
# TODO: Improve the storage method so we don't have to
# deserialize and serialize the entire table on every
2022-09-13 22:32:00 +02:00
# save, but the only changes. It might be possible to
# simply overwrite on exit now that every local client
# disconnect triggers a data persist.
2022-09-06 19:43:46 +02:00
2021-10-08 08:52:50 +02:00
try :
2022-09-06 19:43:46 +02:00
if hasattr ( Identity , " saving_known_destinations " ) :
wait_interval = 0.2
wait_timeout = 5
wait_start = time . time ( )
while Identity . saving_known_destinations :
time . sleep ( wait_interval )
if time . time ( ) > wait_start + wait_timeout :
RNS . log ( " Could not save known destinations to storage, waiting for previous save operation timed out. " , RNS . LOG_ERROR )
return False
Identity . saving_known_destinations = True
save_start = time . time ( )
2021-10-08 08:52:50 +02:00
storage_known_destinations = { }
if os . path . isfile ( RNS . Reticulum . storagepath + " /known_destinations " ) :
try :
file = open ( RNS . Reticulum . storagepath + " /known_destinations " , " rb " )
storage_known_destinations = umsgpack . load ( file )
file . close ( )
except :
pass
for destination_hash in storage_known_destinations :
if not destination_hash in Identity . known_destinations :
Identity . known_destinations [ destination_hash ] = storage_known_destinations [ destination_hash ]
2022-09-24 12:23:59 +02:00
RNS . log ( " Saving " + str ( len ( Identity . known_destinations ) ) + " known destinations to storage... " , RNS . LOG_DEBUG )
2021-10-08 08:52:50 +02:00
file = open ( RNS . Reticulum . storagepath + " /known_destinations " , " wb " )
umsgpack . dump ( Identity . known_destinations , file )
file . close ( )
2022-09-06 19:43:46 +02:00
save_time = time . time ( ) - save_start
if save_time < 1 :
time_str = str ( round ( save_time * 1000 , 2 ) ) + " ms "
else :
time_str = str ( round ( save_time , 2 ) ) + " s "
2022-09-24 12:23:59 +02:00
RNS . log ( " Saved known destinations to storage in " + time_str , RNS . LOG_DEBUG )
2022-09-06 19:43:46 +02:00
2021-10-08 08:52:50 +02:00
except Exception as e :
RNS . log ( " Error while saving known destinations to disk, the contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2020-08-13 12:15:56 +02:00
2022-09-06 19:43:46 +02:00
Identity . saving_known_destinations = False
2020-08-13 12:15:56 +02:00
@staticmethod
2021-05-16 16:15:57 +02:00
def load_known_destinations ( ) :
2020-08-13 12:15:56 +02:00
if os . path . isfile ( RNS . Reticulum . storagepath + " /known_destinations " ) :
try :
file = open ( RNS . Reticulum . storagepath + " /known_destinations " , " rb " )
2022-07-02 15:15:47 +02:00
loaded_known_destinations = umsgpack . load ( file )
2020-08-13 12:15:56 +02:00
file . close ( )
2022-07-02 15:15:47 +02:00
Identity . known_destinations = { }
for known_destination in loaded_known_destinations :
if len ( known_destination ) == RNS . Reticulum . TRUNCATED_HASHLENGTH / / 8 :
Identity . known_destinations [ known_destination ] = loaded_known_destinations [ known_destination ]
2020-08-13 12:15:56 +02:00
RNS . log ( " Loaded " + str ( len ( Identity . known_destinations ) ) + " known destination from storage " , RNS . LOG_VERBOSE )
except :
RNS . log ( " Error loading known destinations from disk, file will be recreated on exit " , RNS . LOG_ERROR )
else :
2021-10-08 08:52:50 +02:00
RNS . log ( " Destinations file does not exist, no known destinations loaded " , RNS . LOG_VERBOSE )
2020-08-13 12:15:56 +02:00
@staticmethod
2021-05-16 16:15:57 +02:00
def full_hash ( data ) :
2021-05-16 21:58:50 +02:00
"""
Get a SHA - 256 hash of passed data .
: param data : Data to be hashed as * bytes * .
: returns : SHA - 256 hash as * bytes *
"""
2022-06-07 15:48:23 +02:00
return RNS . Cryptography . sha256 ( data )
2020-08-13 12:15:56 +02:00
@staticmethod
2021-05-16 16:15:57 +02:00
def truncated_hash ( data ) :
2021-05-16 21:58:50 +02:00
"""
Get a truncated SHA - 256 hash of passed data .
: param data : Data to be hashed as * bytes * .
: returns : Truncated SHA - 256 hash as * bytes *
"""
2021-05-16 16:15:57 +02:00
return Identity . full_hash ( data ) [ : ( Identity . TRUNCATED_HASHLENGTH / / 8 ) ]
2020-08-13 12:15:56 +02:00
@staticmethod
2021-05-16 16:15:57 +02:00
def get_random_hash ( ) :
2021-05-16 21:58:50 +02:00
"""
Get a random SHA - 256 hash .
: param data : Data to be hashed as * bytes * .
: returns : Truncated SHA - 256 hash of random data as * bytes *
"""
2022-04-20 13:08:21 +02:00
return Identity . truncated_hash ( os . urandom ( Identity . TRUNCATED_HASHLENGTH / / 8 ) )
2020-08-13 12:15:56 +02:00
@staticmethod
2021-05-16 16:15:57 +02:00
def validate_announce ( packet ) :
2022-04-20 09:04:12 +02:00
try :
if packet . packet_type == RNS . Packet . ANNOUNCE :
destination_hash = packet . destination_hash
public_key = packet . data [ : Identity . KEYSIZE / / 8 ]
2022-10-06 23:14:32 +02:00
name_hash = packet . data [ Identity . KEYSIZE / / 8 : Identity . KEYSIZE / / 8 + Identity . NAME_HASH_LENGTH / / 8 ]
random_hash = packet . data [ Identity . KEYSIZE / / 8 + Identity . NAME_HASH_LENGTH / / 8 : Identity . KEYSIZE / / 8 + Identity . NAME_HASH_LENGTH / / 8 + 10 ]
signature = packet . data [ Identity . KEYSIZE / / 8 + Identity . NAME_HASH_LENGTH / / 8 + 10 : Identity . KEYSIZE / / 8 + Identity . NAME_HASH_LENGTH / / 8 + 10 + Identity . SIGLENGTH / / 8 ]
2022-04-20 09:04:12 +02:00
app_data = b " "
2022-10-06 23:14:32 +02:00
if len ( packet . data ) > Identity . KEYSIZE / / 8 + Identity . NAME_HASH_LENGTH / / 8 + 10 + Identity . SIGLENGTH / / 8 :
app_data = packet . data [ Identity . KEYSIZE / / 8 + Identity . NAME_HASH_LENGTH / / 8 + 10 + Identity . SIGLENGTH / / 8 : ]
2022-04-20 09:04:12 +02:00
2022-10-04 06:59:33 +02:00
signed_data = destination_hash + public_key + name_hash + random_hash + app_data
2022-04-20 09:04:12 +02:00
2022-10-06 23:14:32 +02:00
if not len ( packet . data ) > Identity . KEYSIZE / / 8 + Identity . NAME_HASH_LENGTH / / 8 + 10 + Identity . SIGLENGTH / / 8 :
2022-04-20 09:04:12 +02:00
app_data = None
announced_identity = Identity ( create_keys = False )
announced_identity . load_public_key ( public_key )
if announced_identity . pub != None and announced_identity . validate ( signature , signed_data ) :
2022-10-04 06:59:33 +02:00
hash_material = name_hash + announced_identity . hash
expected_hash = RNS . Identity . full_hash ( hash_material ) [ : RNS . Reticulum . TRUNCATED_HASHLENGTH / / 8 ]
2022-04-20 09:04:12 +02:00
2022-10-04 06:59:33 +02:00
if destination_hash == expected_hash :
2022-10-04 22:42:59 +02:00
# Check if we already have a public key for this destination
# and make sure the public key is not different.
if destination_hash in Identity . known_destinations :
if public_key != Identity . known_destinations [ destination_hash ] [ 2 ] :
# In reality, this should never occur, but in the odd case
# that someone manages a hash collision, we reject the announce.
RNS . log ( " Received announce with valid signature and destination hash, but announced public key does not match already known public key. " , RNS . LOG_CRITICAL )
RNS . log ( " This may indicate an attempt to modify network paths, or a random hash collision. The announce was rejected. " , RNS . LOG_CRITICAL )
return False
2022-10-04 06:59:33 +02:00
RNS . Identity . remember ( packet . get_hash ( ) , destination_hash , public_key , app_data )
del announced_identity
2022-12-22 11:26:59 +01:00
if packet . rssi != None or packet . snr != None :
signal_str = " [ "
if packet . rssi != None :
signal_str + = " RSSI " + str ( packet . rssi ) + " dBm "
if packet . snr != None :
signal_str + = " , "
if packet . snr != None :
signal_str + = " SNR " + str ( packet . snr ) + " dB "
signal_str + = " ] "
else :
signal_str = " "
2022-10-04 06:59:33 +02:00
if hasattr ( packet , " transport_id " ) and packet . transport_id != None :
2022-12-22 11:26:59 +01:00
RNS . log ( " Valid announce for " + RNS . prettyhexrep ( destination_hash ) + " " + str ( packet . hops ) + " hops away, received via " + RNS . prettyhexrep ( packet . transport_id ) + " on " + str ( packet . receiving_interface ) + signal_str , RNS . LOG_EXTREME )
2022-10-04 06:59:33 +02:00
else :
2022-12-22 11:26:59 +01:00
RNS . log ( " Valid announce for " + RNS . prettyhexrep ( destination_hash ) + " " + str ( packet . hops ) + " hops away, received on " + str ( packet . receiving_interface ) + signal_str , RNS . LOG_EXTREME )
2022-04-20 09:04:12 +02:00
2022-10-04 06:59:33 +02:00
return True
else :
2022-10-04 22:42:59 +02:00
RNS . log ( " Received invalid announce for " + RNS . prettyhexrep ( destination_hash ) + " : Destination mismatch. " , RNS . LOG_DEBUG )
2022-10-04 06:59:33 +02:00
return False
2022-04-20 19:29:25 +02:00
2022-04-20 09:04:12 +02:00
else :
2022-10-04 22:42:59 +02:00
RNS . log ( " Received invalid announce for " + RNS . prettyhexrep ( destination_hash ) + " : Invalid signature. " , RNS . LOG_DEBUG )
2022-04-20 09:04:12 +02:00
del announced_identity
return False
except Exception as e :
RNS . log ( " Error occurred while validating announce. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
return False
2020-08-13 12:15:56 +02:00
2022-09-13 20:17:25 +02:00
@staticmethod
def persist_data ( ) :
if not RNS . Transport . owner . is_connected_to_shared_instance :
Identity . save_known_destinations ( )
2020-08-13 12:15:56 +02:00
@staticmethod
2021-05-16 13:02:46 +02:00
def exit_handler ( ) :
2022-09-13 20:17:25 +02:00
Identity . persist_data ( )
2020-08-13 12:15:56 +02:00
2021-09-02 20:35:42 +02:00
@staticmethod
def from_bytes ( prv_bytes ) :
"""
Create a new : ref : ` RNS . Identity < api - identity > ` instance from * bytes * of private key .
Can be used to load previously created and saved identities into Reticulum .
: param prv_bytes : The * bytes * of private a saved private key . * * HAZARD ! * * Never use this to generate a new key by feeding random data in prv_bytes .
: returns : A : ref : ` RNS . Identity < api - identity > ` instance , or * None * if the * bytes * data was invalid .
"""
identity = Identity ( create_keys = False )
if identity . load_private_key ( prv_bytes ) :
return identity
else :
return None
2020-08-13 12:15:56 +02:00
@staticmethod
def from_file ( path ) :
2021-05-16 21:58:50 +02:00
"""
Create a new : ref : ` RNS . Identity < api - identity > ` instance from a file .
Can be used to load previously created and saved identities into Reticulum .
: param path : The full path to the saved : ref : ` RNS . Identity < api - identity > ` data
: returns : A : ref : ` RNS . Identity < api - identity > ` instance , or * None * if the loaded data was invalid .
"""
2021-05-20 15:31:38 +02:00
identity = Identity ( create_keys = False )
2020-08-13 12:15:56 +02:00
if identity . load ( path ) :
return identity
else :
return None
2021-08-29 01:24:21 +02:00
def to_file ( self , path ) :
"""
Saves the identity to a file . This will write the private key to disk ,
and anyone with access to this file will be able to decrypt all
communication for the identity . Be very careful with this method .
: param path : The full path specifying where to save the identity .
: returns : True if the file was saved , otherwise False .
"""
try :
with open ( path , " wb " ) as key_file :
key_file . write ( self . get_private_key ( ) )
return True
return False
except Exception as e :
RNS . log ( " Error while saving identity to " + str ( path ) , RNS . LOG_ERROR )
RNS . log ( " The contained exception was: " + str ( e ) )
2021-05-20 15:31:38 +02:00
def __init__ ( self , create_keys = True ) :
2020-08-13 12:15:56 +02:00
# Initialize keys to none
2021-05-20 15:31:38 +02:00
self . prv = None
self . prv_bytes = None
self . sig_prv = None
self . sig_prv_bytes = None
self . pub = None
self . pub_bytes = None
self . sig_pub = None
self . sig_pub_bytes = None
self . hash = None
self . hexhash = None
if create_keys :
2021-05-16 16:15:57 +02:00
self . create_keys ( )
2020-08-13 12:15:56 +02:00
2021-05-16 16:15:57 +02:00
def create_keys ( self ) :
2021-05-20 15:31:38 +02:00
self . prv = X25519PrivateKey . generate ( )
2022-06-08 13:36:23 +02:00
self . prv_bytes = self . prv . private_bytes ( )
2021-05-20 15:31:38 +02:00
self . sig_prv = Ed25519PrivateKey . generate ( )
2022-06-08 19:47:09 +02:00
self . sig_prv_bytes = self . sig_prv . private_bytes ( )
2021-05-20 15:31:38 +02:00
self . pub = self . prv . public_key ( )
2022-06-08 13:36:23 +02:00
self . pub_bytes = self . pub . public_bytes ( )
2021-05-20 15:31:38 +02:00
self . sig_pub = self . sig_prv . public_key ( )
2022-06-08 19:47:09 +02:00
self . sig_pub_bytes = self . sig_pub . public_bytes ( )
2020-08-13 12:15:56 +02:00
2021-05-16 16:15:57 +02:00
self . update_hashes ( )
2020-08-13 12:15:56 +02:00
RNS . log ( " Identity keys created for " + RNS . prettyhexrep ( self . hash ) , RNS . LOG_VERBOSE )
2021-05-16 16:15:57 +02:00
def get_private_key ( self ) :
2021-05-16 21:58:50 +02:00
"""
: returns : The private key as * bytes *
"""
2021-05-20 15:31:38 +02:00
return self . prv_bytes + self . sig_prv_bytes
2020-08-13 12:15:56 +02:00
2021-05-16 16:15:57 +02:00
def get_public_key ( self ) :
2021-05-16 21:58:50 +02:00
"""
: returns : The public key as * bytes *
"""
2021-05-20 15:31:38 +02:00
return self . pub_bytes + self . sig_pub_bytes
2020-08-13 12:15:56 +02:00
2021-05-16 16:15:57 +02:00
def load_private_key ( self , prv_bytes ) :
2021-05-16 21:58:50 +02:00
"""
Load a private key into the instance .
: param prv_bytes : The private key as * bytes * .
: returns : True if the key was loaded , otherwise False .
"""
2020-08-13 12:15:56 +02:00
try :
2021-05-20 15:31:38 +02:00
self . prv_bytes = prv_bytes [ : Identity . KEYSIZE / / 8 / / 2 ]
self . prv = X25519PrivateKey . from_private_bytes ( self . prv_bytes )
self . sig_prv_bytes = prv_bytes [ Identity . KEYSIZE / / 8 / / 2 : ]
self . sig_prv = Ed25519PrivateKey . from_private_bytes ( self . sig_prv_bytes )
self . pub = self . prv . public_key ( )
2022-06-08 13:36:23 +02:00
self . pub_bytes = self . pub . public_bytes ( )
2021-05-20 15:31:38 +02:00
self . sig_pub = self . sig_prv . public_key ( )
2022-06-08 19:47:09 +02:00
self . sig_pub_bytes = self . sig_pub . public_bytes ( )
2021-05-20 15:31:38 +02:00
2021-05-16 16:15:57 +02:00
self . update_hashes ( )
2020-08-13 12:15:56 +02:00
return True
except Exception as e :
2021-05-20 15:31:38 +02:00
raise e
2020-08-13 12:15:56 +02:00
RNS . log ( " Failed to load identity key " , RNS . LOG_ERROR )
2021-05-03 20:24:44 +02:00
RNS . log ( " The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2020-08-13 12:15:56 +02:00
return False
2021-05-20 15:31:38 +02:00
def load_public_key ( self , pub_bytes ) :
2021-05-16 21:58:50 +02:00
"""
Load a public key into the instance .
2021-05-20 15:31:38 +02:00
: param pub_bytes : The public key as * bytes * .
2021-05-16 21:58:50 +02:00
: returns : True if the key was loaded , otherwise False .
"""
2020-08-13 12:15:56 +02:00
try :
2021-05-20 15:31:38 +02:00
self . pub_bytes = pub_bytes [ : Identity . KEYSIZE / / 8 / / 2 ]
self . sig_pub_bytes = pub_bytes [ Identity . KEYSIZE / / 8 / / 2 : ]
self . pub = X25519PublicKey . from_public_bytes ( self . pub_bytes )
self . sig_pub = Ed25519PublicKey . from_public_bytes ( self . sig_pub_bytes )
2021-05-16 16:15:57 +02:00
self . update_hashes ( )
2020-08-13 12:15:56 +02:00
except Exception as e :
RNS . log ( " Error while loading public key, the contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2021-05-16 16:15:57 +02:00
def update_hashes ( self ) :
2021-05-20 15:31:38 +02:00
self . hash = Identity . truncated_hash ( self . get_public_key ( ) )
2020-08-13 12:15:56 +02:00
self . hexhash = self . hash . hex ( )
def load ( self , path ) :
try :
with open ( path , " rb " ) as key_file :
prv_bytes = key_file . read ( )
2021-05-16 16:15:57 +02:00
return self . load_private_key ( prv_bytes )
2020-08-13 12:15:56 +02:00
return False
except Exception as e :
RNS . log ( " Error while loading identity from " + str ( path ) , RNS . LOG_ERROR )
2023-05-31 15:39:55 +02:00
RNS . log ( " The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2020-08-13 12:15:56 +02:00
2021-05-20 15:31:38 +02:00
def get_salt ( self ) :
return self . hash
def get_context ( self ) :
return None
2020-08-13 12:15:56 +02:00
def encrypt ( self , plaintext ) :
2021-05-16 21:58:50 +02:00
"""
Encrypts information for the identity .
: param plaintext : The plaintext to be encrypted as * bytes * .
2021-05-20 15:31:38 +02:00
: returns : Ciphertext token as * bytes * .
: raises : * KeyError * if the instance does not hold a public key .
2021-05-16 21:58:50 +02:00
"""
2020-08-13 12:15:56 +02:00
if self . pub != None :
2021-05-20 15:31:38 +02:00
ephemeral_key = X25519PrivateKey . generate ( )
2022-06-08 13:36:23 +02:00
ephemeral_pub_bytes = ephemeral_key . public_key ( ) . public_bytes ( )
2021-05-20 15:31:38 +02:00
shared_key = ephemeral_key . exchange ( self . pub )
2022-03-08 00:38:51 +01:00
2022-06-07 15:48:23 +02:00
derived_key = RNS . Cryptography . hkdf (
2021-05-20 15:31:38 +02:00
length = 32 ,
2022-06-07 15:48:23 +02:00
derive_from = shared_key ,
2021-05-20 15:31:38 +02:00
salt = self . get_salt ( ) ,
2022-06-07 15:48:23 +02:00
context = self . get_context ( ) ,
)
2021-05-20 15:31:38 +02:00
2022-06-08 12:29:51 +02:00
fernet = Fernet ( derived_key )
ciphertext = fernet . encrypt ( plaintext )
2021-05-20 15:31:38 +02:00
token = ephemeral_pub_bytes + ciphertext
return token
2020-08-13 12:15:56 +02:00
else :
raise KeyError ( " Encryption failed because identity does not hold a public key " )
2021-05-20 15:31:38 +02:00
def decrypt ( self , ciphertext_token ) :
2021-05-16 21:58:50 +02:00
"""
Decrypts information for the identity .
: param ciphertext : The ciphertext to be decrypted as * bytes * .
: returns : Plaintext as * bytes * , or * None * if decryption fails .
2021-05-20 15:31:38 +02:00
: raises : * KeyError * if the instance does not hold a private key .
2021-05-16 21:58:50 +02:00
"""
2020-08-13 12:15:56 +02:00
if self . prv != None :
2021-05-20 15:31:38 +02:00
if len ( ciphertext_token ) > Identity . KEYSIZE / / 8 / / 2 :
plaintext = None
try :
peer_pub_bytes = ciphertext_token [ : Identity . KEYSIZE / / 8 / / 2 ]
peer_pub = X25519PublicKey . from_public_bytes ( peer_pub_bytes )
shared_key = self . prv . exchange ( peer_pub )
2022-03-08 00:38:51 +01:00
2022-06-07 15:48:23 +02:00
derived_key = RNS . Cryptography . hkdf (
2021-05-20 15:31:38 +02:00
length = 32 ,
2022-06-07 15:48:23 +02:00
derive_from = shared_key ,
2021-05-20 15:31:38 +02:00
salt = self . get_salt ( ) ,
2022-06-07 15:48:23 +02:00
context = self . get_context ( ) ,
)
2021-05-20 15:31:38 +02:00
2022-06-08 12:29:51 +02:00
fernet = Fernet ( derived_key )
2021-05-20 15:31:38 +02:00
ciphertext = ciphertext_token [ Identity . KEYSIZE / / 8 / / 2 : ]
2022-06-08 12:29:51 +02:00
plaintext = fernet . decrypt ( ciphertext )
2021-05-20 15:31:38 +02:00
except Exception as e :
RNS . log ( " Decryption by " + RNS . prettyhexrep ( self . hash ) + " failed: " + str ( e ) , RNS . LOG_DEBUG )
return plaintext ;
else :
RNS . log ( " Decryption failed because the token size was invalid. " , RNS . LOG_DEBUG )
return None
2020-08-13 12:15:56 +02:00
else :
raise KeyError ( " Decryption failed because identity does not hold a private key " )
def sign ( self , message ) :
2021-05-16 21:58:50 +02:00
"""
Signs information by the identity .
: param message : The message to be signed as * bytes * .
: returns : Signature as * bytes * .
2021-05-20 15:31:38 +02:00
: raises : * KeyError * if the instance does not hold a private key .
2021-05-16 21:58:50 +02:00
"""
2021-05-20 15:31:38 +02:00
if self . sig_prv != None :
try :
return self . sig_prv . sign ( message )
except Exception as e :
RNS . log ( " The identity " + str ( self ) + " could not sign the requested message. The contained exception was: " + str ( e ) , RNS . LOG_ERROR )
2022-04-28 14:17:12 +02:00
raise e
2020-08-13 12:15:56 +02:00
else :
raise KeyError ( " Signing failed because identity does not hold a private key " )
def validate ( self , signature , message ) :
2021-05-16 21:58:50 +02:00
"""
Validates the signature of a signed message .
: param signature : The signature to be validated as * bytes * .
: param message : The message to be validated as * bytes * .
: returns : True if the signature is valid , otherwise False .
2021-05-20 15:31:38 +02:00
: raises : * KeyError * if the instance does not hold a public key .
2021-05-16 21:58:50 +02:00
"""
2020-08-13 12:15:56 +02:00
if self . pub != None :
try :
2021-05-20 15:31:38 +02:00
self . sig_pub . verify ( signature , message )
2020-08-13 12:15:56 +02:00
return True
except Exception as e :
return False
else :
raise KeyError ( " Signature validation failed because identity does not hold a public key " )
def prove ( self , packet , destination = None ) :
signature = self . sign ( packet . packet_hash )
if RNS . Reticulum . should_use_implicit_proof ( ) :
proof_data = signature
else :
proof_data = packet . packet_hash + signature
if destination == None :
2021-05-16 16:42:07 +02:00
destination = packet . generate_proof_destination ( )
2020-08-13 12:15:56 +02:00
proof = RNS . Packet ( destination , proof_data , RNS . Packet . PROOF , attached_interface = packet . receiving_interface )
proof . send ( )
def __str__ ( self ) :
return RNS . prettyhexrep ( self . hash )