diff --git a/.github/ISSUE_TEMPLATE/🐛-bug-report.md b/.github/ISSUE_TEMPLATE/🐛-bug-report.md index 62c9e58..82a4968 100644 --- a/.github/ISSUE_TEMPLATE/🐛-bug-report.md +++ b/.github/ISSUE_TEMPLATE/🐛-bug-report.md @@ -12,7 +12,6 @@ Before creating a bug report on this issue tracker, you **must** read the [Contr - The issue tracker is used by developers of this project. **Do not use it to ask general questions, or for support requests**. - Ideas and feature requests can be made on the [Discussions](https://github.com/markqvist/Reticulum/discussions). **Only** feature requests accepted by maintainers and developers are tracked and included on the issue tracker. **Do not post feature requests here**. -- Do not submit code written using large language models (LLMs) or other generative 'AI' programs (see the [Generative AI Policy](/Contributing.md#generative-ai-policy) for details). - After reading the [Contribution Guidelines](https://github.com/markqvist/Reticulum/blob/master/Contributing.md), **delete this section only** (*"Read the Contribution Guidelines"*) from your bug report, **and fill in all the other sections**. **Describe the Bug** diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f80ab6d..9cf0727 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,9 +29,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: 3.x - - run: | - python -m pip install -q cryptography - make test + - run: make test package: needs: test diff --git a/Changelog.md b/Changelog.md index 0104132..47e0c90 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,88 +1,3 @@ -### 2025-05-15: RNS β 0.9.6 - -This release activates AES-256 as the default encryption mode for all communication. It is the last release that will support the old AES-128 based modes, which will be entirely phased out in the next release. - -This release also includes a number of API and resource consumption improvements, and fixes a bug. - -**Changes** -- Enabled AES-256 as default encryption mode for all traffic -- Added dynamic link keepalive and timeout calculation -- Added ability to efficiently transfer files as responses in the `Request` API -- Added ability to include metadata on `Resource` transfers -- Added option to specify `Resource` auto-compression limits -- Added option to specify `Request` response auto-compression limits -- Added `Resource` transfer example -- Added allow overwrite option to `rncp` -- Improved hardware MTU auto-configuration -- Improved handling of file transfers using the `Resource` API -- Improved `Resource` transfer memory consumption -- Improved memory consumption of applications connected to a shared instance -- Improved `rncp` memory consumption for large files -- Fixed announce handlers not triggering after shared instance disappearance - -**Release Hashes** -``` -a23c64a04c1e83fd0ab449f564ac904da7fd4f61c0faf68a063f486cc48b44bd rns-0.9.6-py3-none-any.whl -4544882dea902b18b00d8a04c9ab93201974573b7b63c3db06cb310b0acec240 rnspure-0.9.6-py3-none-any.whl -``` - -### 2025-05-09: RNS β 0.9.5 - -This release initiates migration of Reticulum from AES-128 to AES-256 as the default link and packet cipher mode. It is a compatibility/migration release, that while supporting AES-256 doesn't use it by default. It will work with both the old AES-128 based modes, and the new AES-256 based modes. There's a very slight penalty in performance to support both the old and new modes at the same time, but only for single packet APIs (not links), and it really shouldn't be noticeable in any everyday use. - -In the next release, version `0.9.6`, Reticulum will transition fully to AES-256 and use it by default for all communications. That means that both single packets and links will use AES-256 by default. The old AES-128 link mode may or may not be available for a few releases, but will ultimately be phased out entirely. - -The update requires no intervention, configuration changes or anything similar from a users or developers perspective. Everything should simply work. This goes both for the `0.9.5` update, and the next `0.9.6` update that transitions fully to AES-256. - -**Changes** -- Added support for AES-256 mode to links and packets -- Added dynamic link mode support -- Added temporary backwards compatibility for AES-128 link and packet modes -- Added `get_mode()` method to link API -- Added tests for all enabled link modes -- Added `instance_name` option and description to default config file -- Improved ratchet persist reliability if Reticulum is force killed while persisting ratchets -- Fixed interface string representation for some interfaces -- Fixed instance name config option being overwritten if option was not last in section -- Fixed unhandled potential exception on fast-flapping `BackboneInterface` connections - -**Release Hashes** -``` -ae6587c86c98cae0df73567af093cc92fe204e71bb01f2506da9aec626a27e97 rns-0.9.5-py3-none-any.whl -96208c1d1234e3e4b1c18ca986bad5d4693aeb431453efd7ade33b87f35600e1 rnspure-0.9.5-py3-none-any.whl -``` - -### 2025-04-15: RNS β 0.9.4 - -This release significantly improves memory utilisation and performance. It also includes a few new features and general improvements to the included utilities and programs. - -**Changes** -- Significantly improved memory utilisation, thread count and performance on nodes with many interfaces or clients -- Switched local instance communication to run over abstract domain sockets on Linux and Android -- Switched instance IPC to run over abstract domain sockets on Linux and Android -- Added kernel event based I/O backend on Linux and Android -- Added fast `BackboneInterface` type -- Added support for XIAO-ESP32S3 to `rnodeconf` -- Added interactive shell option to `rnsd` -- Added API option to search for identity by identity hash -- Added option to run TCP and Backbone interfaces in AP mode -- Improved `RNodeMultiInterface` host communications specification -- Improved `rncp` statistics output -- Improved link and reverse-table culling -- Fixed an occasional I/O thread hang on instance shutdown, that would result in an error printed to the console -- Fixed various minor interface logging inconsistencies -- Fixed various minor interface checking inconsistencies -- Updated internal `configobj` implementation -- Refactored various parts of the transport core code -- Swicthed to using internal `netinfo` implementation instead of including full `ifaddr` library -- Cleaned out unneeded dependencies - -**Release Hashes** -``` -737294f29e013f9fa9c8c1326006d0547497607156828fee3dc2a0d3ddd754e7 rns-0.9.4-py3-none-any.whl -0bd8a908af115c27733484853d779574d6383ebc1d78160e5a72c14ed9692a13 rnspure-0.9.4-py3-none-any.whl -``` - ### 2025-03-13: RNS β 0.9.3 This maintenance release improves performance and fixes a number of bugs. diff --git a/Contributing.md b/Contributing.md index 099fa5c..fe818d8 100644 --- a/Contributing.md +++ b/Contributing.md @@ -40,12 +40,4 @@ Pull requests have a high chance of being accepted if they are: Even new ideas and proposals that have not been approved by a maintainer, or fall outside the established roadmap, are *occasionally* accepted - if they possess the remaining of the above qualities. If not, they will be closed and removed without comments or explanation. -## Generative AI Policy - -Contributions written using large language models (LLMs) or other generative 'AI' programs are prohibited. LLMs produce errors so frequently and in a way that is so unlike human error that issues will regularly remain undetected and slip through, even with stringent review. This is not a worthwhile tradeoff for Reticulum, especially considering the limited time maintainers have to correct these issues, and we ask that you refrain from using any such output in your contributions. - -This applies to all official Reticlulm projects and documentation as well as all submitted issues and discussion in official channels, except in cases where language translation and/or speech recogntion technologies are required for communication. We also ask that you avoid using LLMs for troubleshooting, as results can be misleading, and instead request help in one of our [various communities](https://reticulum.network/start.html). - -## Contributor License Agreement - -By contributing code to this project, you agree that copyright for the code is transferred to the Reticulum maintainers and that the code is irrevocably placed under the [Reticulum License](./LICENSE). +By contributing code to this project, you agree that copyright for the code is transferred to the Reticulum maintainers and that the code is irrevocably placed under the [MIT license](./LICENSE). diff --git a/Examples/ExampleInterface.py b/Examples/ExampleInterface.py index 2ddfa1e..6786e7c 100644 --- a/Examples/ExampleInterface.py +++ b/Examples/ExampleInterface.py @@ -1,3 +1,5 @@ +# MIT License - Copyright (c) 2024 Mark Qvist / unsigned.io + # This example illustrates creating a custom interface # definition, that can be loaded and used by Reticulum at # runtime. Any number of custom interfaces can be created diff --git a/Examples/Request.py b/Examples/Request.py index 8011666..7a56d72 100644 --- a/Examples/Request.py +++ b/Examples/Request.py @@ -1,6 +1,6 @@ ########################################################## -# This RNS example demonstrates how to perform requests # -# and receive responses over a link. # +# This RNS example demonstrates how to set perform # +# requests and receive responses over a link. # ########################################################## import os diff --git a/Examples/Resource.py b/Examples/Resource.py deleted file mode 100644 index f52681c..0000000 --- a/Examples/Resource.py +++ /dev/null @@ -1,294 +0,0 @@ -########################################################## -# This RNS example demonstrates how to transfer a # -# resource over an established link # -########################################################## - -import os -import sys -import time -import random -import argparse -import RNS - -# Let's define an app name. We'll use this for all -# destinations we create. Since this echo example -# is part of a range of example utilities, we'll put -# them all within the app namespace "example_utilities" -APP_NAME = "example_utilities" - -########################################################## -#### Server Part ######################################### -########################################################## - -# A reference to the latest client link that connected -latest_client_link = None - -# This initialisation is executed when the users chooses -# to run as a server -def server(configpath): - # We must first initialise Reticulum - reticulum = RNS.Reticulum(configpath) - - # Randomly create a new identity for our link example - server_identity = RNS.Identity() - - # We create a destination that clients can connect to. We - # want clients to create links to this destination, so we - # need to create a "single" destination type. - server_destination = RNS.Destination( - server_identity, - RNS.Destination.IN, - RNS.Destination.SINGLE, - APP_NAME, - "resourceexample" - ) - - # We configure a function that will get called every time - # a new client creates a link to this destination. - server_destination.set_link_established_callback(client_connected) - - # Everything's ready! - # Let's Wait for client resources or user input - server_loop(server_destination) - -def server_loop(destination): - # Let the user know that everything is ready - RNS.log( - "Resource example "+ - RNS.prettyhexrep(destination.hash)+ - " running, waiting for a connection." - ) - - RNS.log("Hit enter to manually send an announce (Ctrl-C to quit)") - - # We enter a loop that runs until the users exits. - # If the user hits enter, we will announce our server - # destination on the network, which will let clients - # know how to create messages directed towards it. - while True: - entered = input() - destination.announce() - RNS.log("Sent announce from "+RNS.prettyhexrep(destination.hash)) - -# When a client establishes a link to our server -# destination, this function will be called with -# a reference to the link. -def client_connected(link): - global latest_client_link - RNS.log("Client connected") - - # We configure the link to accept all resources - # and set a callback for completed resources - link.set_resource_strategy(RNS.Link.ACCEPT_ALL) - link.set_resource_concluded_callback(resource_concluded) - - link.set_link_closed_callback(client_disconnected) - latest_client_link = link - -def client_disconnected(link): - RNS.log("Client disconnected") - -def resource_concluded(resource): - if resource.status == RNS.Resource.COMPLETE: - RNS.log(f"Resource {resource} received") - RNS.log(f"Metadata: {resource.metadata}") - RNS.log(f"Data length: {os.stat(resource.data.name).st_size}") - RNS.log(f"Data can be read directly from: {resource.data}") - RNS.log(f"Data can be moved or copied from: {resource.data.name}") - RNS.log(f"First 32 bytes of data: {RNS.hexrep(resource.data.read(32))}") - else: - RNS.log(f"Receiving resource {resource} failed") - - - -########################################################## -#### Client Part ######################################### -########################################################## - -# A reference to the server link -server_link = None - -def random_text_generator(): - texts = ["They looked up", "On each full moon", "Becky was upset", "I’ll stay away from it", "The pet shop stocks everything"] - return texts[random.randint(0, len(texts)-1)] - -# This initialisation is executed when the users chooses -# to run as a client -def client(destination_hexhash, configpath): - # We need a binary representation of the destination - # hash that was entered on the command line - try: - dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH//8)*2 - if len(destination_hexhash) != dest_len: - raise ValueError( - "Destination length is invalid, must be {hex} hexadecimal characters ({byte} bytes).".format(hex=dest_len, byte=dest_len//2) - ) - - destination_hash = bytes.fromhex(destination_hexhash) - except: - RNS.log("Invalid destination entered. Check your input!\n") - sys.exit(0) - - # We must first initialise Reticulum - reticulum = RNS.Reticulum(configpath) - - # Check if we know a path to the destination - if not RNS.Transport.has_path(destination_hash): - RNS.log("Destination is not yet known. Requesting path and waiting for announce to arrive...") - RNS.Transport.request_path(destination_hash) - while not RNS.Transport.has_path(destination_hash): - time.sleep(0.1) - - # Recall the server identity - server_identity = RNS.Identity.recall(destination_hash) - - # Inform the user that we'll begin connecting - RNS.log("Establishing link with server...") - - # When the server identity is known, we set - # up a destination - server_destination = RNS.Destination( - server_identity, - RNS.Destination.OUT, - RNS.Destination.SINGLE, - APP_NAME, - "resourceexample" - ) - - # And create a link - link = RNS.Link(server_destination) - - # We'll set up functions to inform the - # user when the link is established or closed - link.set_link_established_callback(link_established) - link.set_link_closed_callback(link_closed) - - # Everything is set up, so let's enter a loop - # for the user to interact with the example - client_loop() - -def client_loop(): - global server_link - - # Wait for the link to become active - while not server_link: - time.sleep(0.1) - - should_quit = False - while not should_quit: - try: - print("> ", end=" ") - text = input() - - # Check if we should quit the example - if text == "quit" or text == "q" or text == "exit": - should_quit = True - server_link.teardown() - - else: - # Generate 32 megabytes of random data - data = os.urandom(32*1024*1024) - RNS.log(f"Data length: {len(data)}") - RNS.log(f"First 32 bytes of data: {RNS.hexrep(data[:32])}") - - # Generate some metadata - metadata = {"text": random_text_generator(), "numbers": [1,2,3,4], "blob": os.urandom(16)} - - # Send the resource - resource = RNS.Resource(data, server_link, metadata=metadata, callback=resource_concluded_sending, auto_compress=False) - - # Alternatively, you can stream data - # directly from an open file descriptor - - # with open("/path/to/file", "rb") as data_file: - # resource = RNS.Resource(data_file, server_link, metadata=metadata, callback=resource_concluded_sending, auto_compress=False) - - except Exception as e: - RNS.log("Error while sending resource over the link: "+str(e)) - should_quit = True - server_link.teardown() - -def resource_concluded_sending(resource): - if resource.status == RNS.Resource.COMPLETE: RNS.log(f"The resource {resource} was sent successfully") - else: RNS.log(f"Sending the resource {resource} failed") - -# This function is called when a link -# has been established with the server -def link_established(link): - # We store a reference to the link - # instance for later use - global server_link - server_link = link - - # Inform the user that the server is - # connected - RNS.log("Link established with server, hit enter to sand a resource, or type in \"quit\" to quit") - -# When a link is closed, we'll inform the -# user, and exit the program -def link_closed(link): - if link.teardown_reason == RNS.Link.TIMEOUT: - RNS.log("The link timed out, exiting now") - elif link.teardown_reason == RNS.Link.DESTINATION_CLOSED: - RNS.log("The link was closed by the server, exiting now") - else: - RNS.log("Link closed, exiting now") - - time.sleep(1.5) - sys.exit(0) - - -########################################################## -#### Program Startup ##################################### -########################################################## - -# This part of the program runs at startup, -# and parses input of from the user, and then -# starts up the desired program mode. -if __name__ == "__main__": - try: - parser = argparse.ArgumentParser(description="Simple resource example") - - parser.add_argument( - "-s", - "--server", - action="store_true", - help="wait for incoming resources from clients" - ) - - parser.add_argument( - "--config", - action="store", - default=None, - help="path to alternative Reticulum config directory", - type=str - ) - - parser.add_argument( - "destination", - nargs="?", - default=None, - help="hexadecimal hash of the server destination", - type=str - ) - - args = parser.parse_args() - - if args.config: - configarg = args.config - else: - configarg = None - - if args.server: - server(configarg) - else: - if (args.destination == None): - print("") - parser.print_help() - print("") - else: - client(args.destination, configarg) - - except KeyboardInterrupt: - print("") - sys.exit(0) \ No newline at end of file diff --git a/FUNDING.yml b/FUNDING.yml index d125d55..3755d31 100644 --- a/FUNDING.yml +++ b/FUNDING.yml @@ -1,3 +1,2 @@ -liberapay: Reticulum ko_fi: markqvist -custom: "https://unsigned.io/donate" +custom: "https://unsigned.io/donate" \ No newline at end of file diff --git a/LICENSE b/LICENSE index 9285f0f..92e0464 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ -Reticulum License +MIT License, unless otherwise noted -Copyright (c) 2016-2025 Mark Qvist +Copyright (c) 2016-2024 Mark Qvist / unsigned.io Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ 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 Software shall not be used in any kind of system which includes amongst - its functions the ability to purposefully do harm to human beings. - -- The Software shall not be used, directly or indirectly, in the creation of - an artificial intelligence, machine learning or language model training - dataset, including but not limited to any use that contributes to the - training or development of such a model or algorithm. - -- The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. +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, diff --git a/README.md b/README.md index a6f0aa1..3b0ac70 100755 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ For more info, see [reticulum.network](https://reticulum.network/) and [the FAQ - Forward Secrecy is available for all communication types, both for single packets and over links - Reticulum uses the following format for encrypted tokens: - Ephemeral per-packet and link keys and derived from an ECDH key exchange on Curve25519 - - AES-256 in CBC mode with PKCS7 padding + - AES-128 in CBC mode with PKCS7 padding - HMAC using SHA256 for authentication - IVs are generated through os.urandom() - Unforgeable packet delivery confirmations @@ -62,7 +62,7 @@ For more info, see [reticulum.network](https://reticulum.network/) and [the FAQ - Easily create your own custom interfaces for communicating over anything - Authentication and virtual network segmentation on all supported interface types - An intuitive and easy-to-use API - - Simpler and easier to use than sockets APIs, but more powerful + - Simpler and easier to use than sockets APIs and simpler, but more powerful - Makes building distributed and decentralised applications much simpler - Reliable and efficient transfer of arbitrary amounts of data - Reticulum can handle a few bytes of data or files of many gigabytes @@ -86,10 +86,9 @@ following resources. - You can use the [rnsh](https://github.com/acehoss/rnsh) program to establish remote shell sessions over Reticulum. - [LXMF](https://github.com/markqvist/lxmf) is a distributed, delay and disruption tolerant message transfer protocol built on Reticulum -- The [LXST](https://github.com/markqvist/lxst) protocol and framework provides real-time audio and signals transport over Reticulum. It includes primitives and utilities for building voice-based applications and hardware devices, such as the `rnphone` program, that can be used to build hardware telephones. - For an off-grid, encrypted and resilient mesh communications platform, see [Nomad Network](https://github.com/markqvist/NomadNet) -- The Android, Linux, macOS and Windows app [Sideband](https://github.com/markqvist/Sideband) has a graphical interface and many advanced features, such as file transfers, image and voice messages, real-time voice calls, a distributed telemetry system, mapping capabilities and full plugin extensibility. -- [MeshChat](https://github.com/liamcottle/reticulum-meshchat) is a user-friendly LXMF client with a web-based interface, that also supports image and voice messages, as well as file transfers. It also includes a built-in page browser for browsing Nomad Network nodes. +- The Android, Linux, macOS and Windows app [Sideband](https://github.com/markqvist/Sideband) has a graphical interface and focuses on ease of use. +- [MeshChat](https://github.com/liamcottle/reticulum-meshchat) is a user-friendly LXMF client, that also supports voice calls. ## Where can Reticulum be used? Over practically any medium that can support at least a half-duplex channel @@ -297,30 +296,23 @@ You can help support the continued development of open, free and private communi ``` 84FpY1QbxHcgdseePYNmhTHcrgMX4nFfBYtz2GKYToqHVVhJp8Eaw1Z1EedRnKD19b3B8NiLCGVxzKV17UMmmeEsCrPyA5w ``` -- Bitcoin - ``` - bc1p4a6axuvl7n9hpapfj8sv5reqj8kz6uxa67d5en70vzrttj0fmcusgxsfk5 - ``` - Ethereum ``` - 0xae89F3B94fC4AD6563F0864a55F9a697a90261ff + 0xFDabC71AC4c0C78C95aDDDe3B4FA19d6273c5E73 + ``` +- Bitcoin + ``` + 35G9uWVzrpJJibzUwpNUQGQNFzLirhrYAH ``` -- Liberapay: https://liberapay.com/Reticulum/ - - Ko-Fi: https://ko-fi.com/markqvist +Are certain features in the development roadmap are important to you or your +organisation? Make them a reality quickly by sponsoring their implementation. + ## Cryptographic Primitives Reticulum uses a simple suite of efficient, strong and well-tested cryptographic primitives, with widely available implementations that can be used both on -general-purpose CPUs and on microcontrollers. - -One of the primary considerations for choosing this particular set of primitives is -that they can be implemented *safely* with relatively few pitfalls, on practically -all current computing platforms. - -The primitives listed here **are authoritative**. Anything claiming to be Reticulum, -but not using these exact primitives **is not** Reticulum, and possibly an -intentionally compromised or weakened clone. The utilised primitives are: +general-purpose CPUs and on microcontrollers. The utilised primitives are: - Reticulum Identity Keys are 512-bit Curve25519 keysets - A 256-bit Ed25519 key for signatures @@ -328,15 +320,15 @@ intentionally compromised or weakened clone. The utilised primitives are: - HKDF for key derivation - Encrypted tokens are based on the [Fernet spec](https://github.com/fernet/spec/) - Ephemeral keys derived from an ECDH key exchange on Curve25519 + - AES-128 in CBC mode with PKCS7 padding - HMAC using SHA256 for message authentication - - IVs must be generated through `os.urandom()` or better - - AES-256 in CBC mode with PKCS7 padding + - IVs are generated through os.urandom() - No Fernet version and timestamp metadata fields - SHA-256 - SHA-512 -In the default installation configuration, the `X25519`, `Ed25519`, -and `AES-256-CBC` primitives are provided by [OpenSSL](https://www.openssl.org/) +In the default installation configuration, the `X25519`, `Ed25519` and +`AES-128-CBC` primitives are provided by [OpenSSL](https://www.openssl.org/) (via the [PyCA/cryptography](https://github.com/pyca/cryptography) package). The hashing functions `SHA-256` and `SHA-512` are provided by the standard Python [hashlib](https://docs.python.org/3/library/hashlib.html). The `HKDF`, @@ -350,19 +342,13 @@ provided by the following internal implementations: Reticulum also includes a complete implementation of all necessary primitives -in pure Python. If OpenSSL and PyCA are not available on the system when +in pure Python. If OpenSSL & PyCA are not available on the system when Reticulum is started, Reticulum will instead use the internal pure-python primitives. A trivial consequence of this is performance, with the OpenSSL backend being *much* faster. The most important consequence however, is the potential loss of security by using primitives that has not seen the same amount of scrutiny, testing and review as those from OpenSSL. -Please note that by default, installing Reticulum will **require** OpenSSL and -PyCA to also be automatically installed if not already available. It is only -possible to use the pure-python primitives if this requirement is specifically -overridden by the user, for example by installing the `rnspure` package instead -of the normal `rns` package, or by running directly from local source-code. - If you want to use the internal pure-python primitives, it is **highly advisable** that you have a good understanding of the risks that this pose, and make an informed decision on whether those risks are acceptable to you. @@ -386,8 +372,7 @@ projects: - [PyCA/cryptography](https://github.com/pyca/cryptography), *BSD License* - [Pure-25519](https://github.com/warner/python-pure25519) by [Brian Warner](https://github.com/warner), *MIT License* - [Pysha2](https://github.com/thomdixon/pysha2) by [Thom Dixon](https://github.com/thomdixon), *MIT License* -- [Python AES-128](https://github.com/orgurar/python-aes) by [Or Gur Arie](https://github.com/orgurar), *MIT License* -- [Python AES-256](https://github.com/boppreh/aes) by [BoppreH](https://github.com/boppreh), *MIT License* +- [Python-AES](https://github.com/orgurar/python-aes) by [Or Gur Arie](https://github.com/orgurar), *MIT License* - [Curve25519.py](https://gist.github.com/nickovs/cc3c22d15f239a2640c185035c06f8a3#file-curve25519-py) by [Nicko van Someren](https://gist.github.com/nickovs), *Public Domain* - [I2Plib](https://github.com/l-n-s/i2plib) by [Viktor Villainov](https://github.com/l-n-s) - [PySerial](https://github.com/pyserial/pyserial) by Chris Liechti, *BSD License* diff --git a/RNS/Buffer.py b/RNS/Buffer.py index 20780e2..320f67d 100644 --- a/RNS/Buffer.py +++ b/RNS/Buffer.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Channel.py b/RNS/Channel.py index 011fde1..cc144ef 100644 --- a/RNS/Channel.py +++ b/RNS/Channel.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Cryptography/AES.py b/RNS/Cryptography/AES.py index 9025ab7..8880638 100644 --- a/RNS/Cryptography/AES.py +++ b/RNS/Cryptography/AES.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2022-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, @@ -32,57 +24,23 @@ import RNS.Cryptography.Provider as cp import RNS.vendor.platformutils as pu if cp.PROVIDER == cp.PROVIDER_INTERNAL: - from .aes import AES128 - from .aes import AES256 + from .aes import AES elif cp.PROVIDER == cp.PROVIDER_PYCA: from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes - if pu.cryptography_old_api(): from cryptography.hazmat.backends import default_backend + + if pu.cryptography_old_api(): + from cryptography.hazmat.backends import default_backend class AES_128_CBC: + @staticmethod def encrypt(plaintext, key, iv): - if len(key) != 16: raise ValueError(f"Invalid key length {len(key)*8} for {self}") if cp.PROVIDER == cp.PROVIDER_INTERNAL: - cipher = AES128(key) + cipher = AES(key) return cipher.encrypt(plaintext, iv) - elif cp.PROVIDER == cp.PROVIDER_PYCA: - if not pu.cryptography_old_api(): - cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) - else: - cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) - - encryptor = cipher.encryptor() - ciphertext = encryptor.update(plaintext) + encryptor.finalize() - return ciphertext - - @staticmethod - def decrypt(ciphertext, key, iv): - if len(key) != 16: raise ValueError(f"Invalid key length {len(key)*8} for {self}") - if cp.PROVIDER == cp.PROVIDER_INTERNAL: - cipher = AES128(key) - return cipher.decrypt(ciphertext, iv) - - elif cp.PROVIDER == cp.PROVIDER_PYCA: - if not pu.cryptography_old_api(): - cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) - else: - cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) - - decryptor = cipher.decryptor() - plaintext = decryptor.update(ciphertext) + decryptor.finalize() - return plaintext - -class AES_256_CBC: - @staticmethod - def encrypt(plaintext, key, iv): - if len(key) != 32: raise ValueError(f"Invalid key length {len(key)*8} for {self}") - if cp.PROVIDER == cp.PROVIDER_INTERNAL: - cipher = AES256(key) - return cipher.encrypt_cbc(plaintext, iv) - elif cp.PROVIDER == cp.PROVIDER_PYCA: if not pu.cryptography_old_api(): cipher = Cipher(algorithms.AES(key), modes.CBC(iv)) @@ -95,10 +53,9 @@ class AES_256_CBC: @staticmethod def decrypt(ciphertext, key, iv): - if len(key) != 32: raise ValueError(f"Invalid key length {len(key)*8} for {self}") if cp.PROVIDER == cp.PROVIDER_INTERNAL: - cipher = AES256(key) - return cipher.decrypt_cbc(ciphertext, iv) + cipher = AES(key) + return cipher.decrypt(ciphertext, iv) elif cp.PROVIDER == cp.PROVIDER_PYCA: if not pu.cryptography_old_api(): diff --git a/RNS/Cryptography/Ed25519.py b/RNS/Cryptography/Ed25519.py index b7ffccd..38fdb86 100644 --- a/RNS/Cryptography/Ed25519.py +++ b/RNS/Cryptography/Ed25519.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2022-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Cryptography/HKDF.py b/RNS/Cryptography/HKDF.py index 33d95d1..67b04f1 100644 --- a/RNS/Cryptography/HKDF.py +++ b/RNS/Cryptography/HKDF.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2022-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Cryptography/Hashes.py b/RNS/Cryptography/Hashes.py index 02b8514..595718b 100644 --- a/RNS/Cryptography/Hashes.py +++ b/RNS/Cryptography/Hashes.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2022-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Cryptography/PKCS7.py b/RNS/Cryptography/PKCS7.py index a046131..3787d36 100644 --- a/RNS/Cryptography/PKCS7.py +++ b/RNS/Cryptography/PKCS7.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2022-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Cryptography/Provider.py b/RNS/Cryptography/Provider.py index 9c78c89..5c2b82a 100644 --- a/RNS/Cryptography/Provider.py +++ b/RNS/Cryptography/Provider.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2022-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Cryptography/Proxies.py b/RNS/Cryptography/Proxies.py index 876baff..fc84604 100644 --- a/RNS/Cryptography/Proxies.py +++ b/RNS/Cryptography/Proxies.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2022-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Cryptography/Token.py b/RNS/Cryptography/Token.py index c0e457d..8687ae9 100644 --- a/RNS/Cryptography/Token.py +++ b/RNS/Cryptography/Token.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2022-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, @@ -33,9 +25,7 @@ import time from RNS.Cryptography import HMAC from RNS.Cryptography import PKCS7 -from RNS.Cryptography import AES from RNS.Cryptography.AES import AES_128_CBC -from RNS.Cryptography.AES import AES_256_CBC class Token(): """ @@ -50,65 +40,71 @@ class Token(): TOKEN_OVERHEAD = 48 # Bytes @staticmethod - def generate_key(mode=AES_256_CBC): - if mode == AES_128_CBC: return os.urandom(32) - elif mode == AES_256_CBC: return os.urandom(64) - else: raise TypeError(f"Invalid token mode: {mode}") + def generate_key(): + return os.urandom(32) - def __init__(self, key=None, mode=AES): - if key == None: raise ValueError("Token key cannot be None") + def __init__(self, key = None): + if key == None: + raise ValueError("Token key cannot be None") - if mode == AES: - if len(key) == 32: - self.mode = AES_128_CBC - self._signing_key = key[:16] - self._encryption_key = key[16:] - - elif len(key) == 64: - self.mode = AES_256_CBC - self._signing_key = key[:32] - self._encryption_key = key[32:] - - else: raise ValueError("Token key must be 128 or 256 bits, not "+str(len(key)*8)) - - else: raise TypeError(f"Invalid token mode: {mode}") + if len(key) != 32: + raise ValueError("Token key must be 32 bytes, not "+str(len(key))) + + self._signing_key = key[:16] + self._encryption_key = key[16:] def verify_hmac(self, token): - if len(token) <= 32: raise ValueError("Cannot verify HMAC on token of only "+str(len(token))+" bytes") + if len(token) <= 32: + raise ValueError("Cannot verify HMAC on token of only "+str(len(token))+" bytes") else: received_hmac = token[-32:] expected_hmac = HMAC.new(self._signing_key, token[:-32]).digest() - if received_hmac == expected_hmac: return True - else: return False + if received_hmac == expected_hmac: + return True + else: + return False def encrypt(self, data = None): - if not isinstance(data, bytes): raise TypeError("Token plaintext input must be bytes") iv = os.urandom(16) + current_time = int(time.time()) - ciphertext = self.mode.encrypt( + if not isinstance(data, bytes): + raise TypeError("Token plaintext input must be bytes") + + ciphertext = AES_128_CBC.encrypt( plaintext = PKCS7.pad(data), key = self._encryption_key, - iv = iv) + iv = iv, + ) signed_parts = iv+ciphertext + return signed_parts + HMAC.new(self._signing_key, signed_parts).digest() def decrypt(self, token = None): - if not isinstance(token, bytes): raise TypeError("Token must be bytes") - if not self.verify_hmac(token): raise ValueError("Token HMAC was invalid") + if not isinstance(token, bytes): + raise TypeError("Token must be bytes") + + if not self.verify_hmac(token): + raise ValueError("Token HMAC was invalid") iv = token[:16] ciphertext = token[16:-32] try: - return PKCS7.unpad( - self.mode.decrypt( - ciphertext = ciphertext, - key = self._encryption_key, - iv = iv)) + plaintext = PKCS7.unpad( + AES_128_CBC.decrypt( + ciphertext, + self._encryption_key, + iv, + ) + ) - except Exception as e: raise ValueError(f"Could not decrypt token: {e}") + return plaintext + + except Exception as e: + raise ValueError("Could not decrypt token") \ No newline at end of file diff --git a/RNS/Cryptography/__init__.py b/RNS/Cryptography/__init__.py index 925221d..fc292b8 100644 --- a/RNS/Cryptography/__init__.py +++ b/RNS/Cryptography/__init__.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2022-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Cryptography/aes/__init__.py b/RNS/Cryptography/aes/__init__.py index f747978..60e1669 100644 --- a/RNS/Cryptography/aes/__init__.py +++ b/RNS/Cryptography/aes/__init__.py @@ -1,2 +1 @@ -from .aes128 import AES128 -from .aes256 import AES256 \ No newline at end of file +from .aes import AES \ No newline at end of file diff --git a/RNS/Cryptography/aes/aes.py b/RNS/Cryptography/aes/aes.py new file mode 100644 index 0000000..eabb20b --- /dev/null +++ b/RNS/Cryptography/aes/aes.py @@ -0,0 +1,271 @@ +# MIT License + +# Copyright (c) 2021 Or Gur Arie + +# 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. + +from .utils import * + + +class AES: + # AES-128 block size + block_size = 16 + # AES-128 encrypts messages with 10 rounds + _rounds = 10 + + + # initiate the AES objecy + def __init__(self, key): + """ + Initializes the object with a given key. + """ + # make sure key length is right + assert len(key) == AES.block_size + + # ExpandKey + self._round_keys = self._expand_key(key) + + + # will perform the AES ExpandKey phase + def _expand_key(self, master_key): + """ + Expands and returns a list of key matrices for the given master_key. + """ + + # Initialize round keys with raw key material. + key_columns = bytes2matrix(master_key) + iteration_size = len(master_key) // 4 + + # Each iteration has exactly as many columns as the key material. + i = 1 + while len(key_columns) < (self._rounds + 1) * 4: + # Copy previous word. + word = list(key_columns[-1]) + + # Perform schedule_core once every "row". + if len(key_columns) % iteration_size == 0: + # Circular shift. + word.append(word.pop(0)) + # Map to S-BOX. + word = [s_box[b] for b in word] + # XOR with first byte of R-CON, since the others bytes of R-CON are 0. + word[0] ^= r_con[i] + i += 1 + elif len(master_key) == 32 and len(key_columns) % iteration_size == 4: + # Run word through S-box in the fourth iteration when using a + # 256-bit key. + word = [s_box[b] for b in word] + + # XOR with equivalent word from previous iteration. + word = bytes(i^j for i, j in zip(word, key_columns[-iteration_size])) + key_columns.append(word) + + # Group key words in 4x4 byte matrices. + return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)] + + + # encrypt a single block of data with AES + def _encrypt_block(self, plaintext): + """ + Encrypts a single block of 16 byte long plaintext. + """ + # length of a single block + assert len(plaintext) == AES.block_size + + # perform on a matrix + state = bytes2matrix(plaintext) + + # AddRoundKey + add_round_key(state, self._round_keys[0]) + + # 9 main rounds + for i in range(1, self._rounds): + # SubBytes + sub_bytes(state) + # ShiftRows + shift_rows(state) + # MixCols + mix_columns(state) + # AddRoundKey + add_round_key(state, self._round_keys[i]) + + # last round, w/t AddRoundKey step + sub_bytes(state) + shift_rows(state) + add_round_key(state, self._round_keys[-1]) + + # return the encrypted matrix as bytes + return matrix2bytes(state) + + + # decrypt a single block of data with AES + def _decrypt_block(self, ciphertext): + """ + Decrypts a single block of 16 byte long ciphertext. + """ + # length of a single block + assert len(ciphertext) == AES.block_size + + # perform on a matrix + state = bytes2matrix(ciphertext) + + # in reverse order, last round is first + add_round_key(state, self._round_keys[-1]) + inv_shift_rows(state) + inv_sub_bytes(state) + + for i in range(self._rounds - 1, 0, -1): + # nain rounds + add_round_key(state, self._round_keys[i]) + inv_mix_columns(state) + inv_shift_rows(state) + inv_sub_bytes(state) + + # initial AddRoundKey phase + add_round_key(state, self._round_keys[0]) + + # return bytes + return matrix2bytes(state) + + + # will encrypt the entire data + def encrypt(self, plaintext, iv): + """ + Encrypts `plaintext` using CBC mode and PKCS#7 padding, with the given + initialization vector (iv). + """ + # iv length must be same as block size + assert len(iv) == AES.block_size + + assert len(plaintext) % AES.block_size == 0 + + ciphertext_blocks = [] + + previous = iv + for plaintext_block in split_blocks(plaintext): + # in CBC mode every block is XOR'd with the previous block + xorred = xor_bytes(plaintext_block, previous) + + # encrypt current block + block = self._encrypt_block(xorred) + previous = block + + # append to ciphertext + ciphertext_blocks.append(block) + + # return as bytes + return b''.join(ciphertext_blocks) + + + # will decrypt the entire data + def decrypt(self, ciphertext, iv): + """ + Decrypts `ciphertext` using CBC mode and PKCS#7 padding, with the given + initialization vector (iv). + """ + # iv length must be same as block size + assert len(iv) == AES.block_size + + plaintext_blocks = [] + + previous = iv + for ciphertext_block in split_blocks(ciphertext): + # in CBC mode every block is XOR'd with the previous block + xorred = xor_bytes(previous, self._decrypt_block(ciphertext_block)) + + # append plaintext + plaintext_blocks.append(xorred) + previous = ciphertext_block + + return b''.join(plaintext_blocks) + + +def test(): + # modules and classes requiered for test only + import os + class bcolors: + OK = '\033[92m' #GREEN + WARNING = '\033[93m' #YELLOW + FAIL = '\033[91m' #RED + RESET = '\033[0m' #RESET COLOR + + # will test AES class by performing an encryption / decryption + print("AES Tests") + print("=========") + + # generate a secret key and print details + key = os.urandom(AES.block_size) + _aes = AES(key) + print(f"Algorithm: AES-CBC-{AES.block_size*8}") + print(f"Secret Key: {key.hex()}") + print() + + # test single block encryption / decryption + iv = os.urandom(AES.block_size) + + single_block_text = b"SingleBlock Text" + print("Single Block Tests") + print("------------------") + print(f"iv: {iv.hex()}") + + print(f"plain text: '{single_block_text.decode()}'") + ciphertext_block = _aes._encrypt_block(single_block_text) + plaintext_block = _aes._decrypt_block(ciphertext_block) + print(f"Ciphertext Hex: {ciphertext_block.hex()}") + print(f"Plaintext: {plaintext_block.decode()}") + assert plaintext_block == single_block_text + print(bcolors.OK + "Single Block Test Passed Successfully" + bcolors.RESET) + print() + + # test a less than a block length phrase + iv = os.urandom(AES.block_size) + + short_text = b"Just Text" + print("Short Text Tests") + print("----------------") + print(f"iv: {iv.hex()}") + print(f"plain text: '{short_text.decode()}'") + ciphertext_short = _aes.encrypt(short_text, iv) + plaintext_short = _aes.decrypt(ciphertext_short, iv) + print(f"Ciphertext Hex: {ciphertext_short.hex()}") + print(f"Plaintext: {plaintext_short.decode()}") + assert short_text == plaintext_short + print(bcolors.OK + "Short Text Test Passed Successfully" + bcolors.RESET) + print() + + # test an arbitrary length phrase + iv = os.urandom(AES.block_size) + + text = b"This Text is longer than one block" + print("Arbitrary Length Tests") + print("----------------------") + print(f"iv: {iv.hex()}") + print(f"plain text: '{text.decode()}'") + ciphertext = _aes.encrypt(text, iv) + plaintext = _aes.decrypt(ciphertext, iv) + print(f"Ciphertext Hex: {ciphertext.hex()}") + print(f"Plaintext: {plaintext.decode()}") + assert text == plaintext + print(bcolors.OK + "Arbitrary Length Text Test Passed Successfully" + bcolors.RESET) + print() + + +if __name__ == "__main__": + # test AES class + test() diff --git a/RNS/Cryptography/aes/aes128.py b/RNS/Cryptography/aes/aes128.py deleted file mode 100644 index 409e847..0000000 --- a/RNS/Cryptography/aes/aes128.py +++ /dev/null @@ -1,326 +0,0 @@ -# MIT License - -# Copyright (c) 2021 Or Gur Arie - -# 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. - -## AES lookup tables -# resource: https://en.wikipedia.org/wiki/Rijndael_S-box -s_box = ( - 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76, - 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, - 0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15, - 0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75, - 0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84, - 0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF, - 0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8, - 0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2, - 0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73, - 0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB, - 0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79, - 0xE7, 0xC8, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08, - 0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A, - 0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E, - 0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF, - 0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16, -) - -inv_s_box = ( - 0x52, 0x09, 0x6A, 0xD5, 0x30, 0x36, 0xA5, 0x38, 0xBF, 0x40, 0xA3, 0x9E, 0x81, 0xF3, 0xD7, 0xFB, - 0x7C, 0xE3, 0x39, 0x82, 0x9B, 0x2F, 0xFF, 0x87, 0x34, 0x8E, 0x43, 0x44, 0xC4, 0xDE, 0xE9, 0xCB, - 0x54, 0x7B, 0x94, 0x32, 0xA6, 0xC2, 0x23, 0x3D, 0xEE, 0x4C, 0x95, 0x0B, 0x42, 0xFA, 0xC3, 0x4E, - 0x08, 0x2E, 0xA1, 0x66, 0x28, 0xD9, 0x24, 0xB2, 0x76, 0x5B, 0xA2, 0x49, 0x6D, 0x8B, 0xD1, 0x25, - 0x72, 0xF8, 0xF6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xD4, 0xA4, 0x5C, 0xCC, 0x5D, 0x65, 0xB6, 0x92, - 0x6C, 0x70, 0x48, 0x50, 0xFD, 0xED, 0xB9, 0xDA, 0x5E, 0x15, 0x46, 0x57, 0xA7, 0x8D, 0x9D, 0x84, - 0x90, 0xD8, 0xAB, 0x00, 0x8C, 0xBC, 0xD3, 0x0A, 0xF7, 0xE4, 0x58, 0x05, 0xB8, 0xB3, 0x45, 0x06, - 0xD0, 0x2C, 0x1E, 0x8F, 0xCA, 0x3F, 0x0F, 0x02, 0xC1, 0xAF, 0xBD, 0x03, 0x01, 0x13, 0x8A, 0x6B, - 0x3A, 0x91, 0x11, 0x41, 0x4F, 0x67, 0xDC, 0xEA, 0x97, 0xF2, 0xCF, 0xCE, 0xF0, 0xB4, 0xE6, 0x73, - 0x96, 0xAC, 0x74, 0x22, 0xE7, 0xAD, 0x35, 0x85, 0xE2, 0xF9, 0x37, 0xE8, 0x1C, 0x75, 0xDF, 0x6E, - 0x47, 0xF1, 0x1A, 0x71, 0x1D, 0x29, 0xC5, 0x89, 0x6F, 0xB7, 0x62, 0x0E, 0xAA, 0x18, 0xBE, 0x1B, - 0xFC, 0x56, 0x3E, 0x4B, 0xC6, 0xD2, 0x79, 0x20, 0x9A, 0xDB, 0xC0, 0xFE, 0x78, 0xCD, 0x5A, 0xF4, - 0x1F, 0xDD, 0xA8, 0x33, 0x88, 0x07, 0xC7, 0x31, 0xB1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xEC, 0x5F, - 0x60, 0x51, 0x7F, 0xA9, 0x19, 0xB5, 0x4A, 0x0D, 0x2D, 0xE5, 0x7A, 0x9F, 0x93, 0xC9, 0x9C, 0xEF, - 0xA0, 0xE0, 0x3B, 0x4D, 0xAE, 0x2A, 0xF5, 0xB0, 0xC8, 0xEB, 0xBB, 0x3C, 0x83, 0x53, 0x99, 0x61, - 0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D, -) - - -## AES AddRoundKey -# Round constants https://en.wikipedia.org/wiki/AES_key_schedule#Round_constants -r_con = ( - 0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, - 0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A, - 0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A, - 0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39, -) - -def add_round_key(s, k): - for i in range(4): - for j in range(4): - s[i][j] ^= k[i][j] - - -## AES SubBytes -def sub_bytes(s): - for i in range(4): - for j in range(4): - s[i][j] = s_box[s[i][j]] - - -def inv_sub_bytes(s): - for i in range(4): - for j in range(4): - s[i][j] = inv_s_box[s[i][j]] - - -## AES ShiftRows -def shift_rows(s): - s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1] - s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2] - s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3] - - -def inv_shift_rows(s): - s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1] - s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2] - s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3] - - -## AES MixColumns -# learned from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c -xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1) - - -def mix_single_column(a): - # see Sec 4.1.2 in The Design of Rijndael - t = a[0] ^ a[1] ^ a[2] ^ a[3] - u = a[0] - a[0] ^= t ^ xtime(a[0] ^ a[1]) - a[1] ^= t ^ xtime(a[1] ^ a[2]) - a[2] ^= t ^ xtime(a[2] ^ a[3]) - a[3] ^= t ^ xtime(a[3] ^ u) - - -def mix_columns(s): - for i in range(4): - mix_single_column(s[i]) - - -def inv_mix_columns(s): - # see Sec 4.1.3 in The Design of Rijndael - for i in range(4): - u = xtime(xtime(s[i][0] ^ s[i][2])) - v = xtime(xtime(s[i][1] ^ s[i][3])) - s[i][0] ^= u - s[i][1] ^= v - s[i][2] ^= u - s[i][3] ^= v - - mix_columns(s) - -## AES Bytes -def bytes2matrix(text): - """ Converts a 16-byte array into a 4x4 matrix. """ - return [list(text[i:i+4]) for i in range(0, len(text), 4)] - -def matrix2bytes(matrix): - """ Converts a 4x4 matrix into a 16-byte array. """ - return bytes(sum(matrix, [])) - - -def xor_bytes(a, b): - """ Returns a new byte array with the elements xor'ed. """ - return bytes(i^j for i, j in zip(a, b)) - - -def split_blocks(message, block_size=16, require_padding=True): - assert len(message) % block_size == 0 or not require_padding - return [message[i:i+16] for i in range(0, len(message), block_size)] - -class AES128: - # AES-128 block size - block_size = 16 - # AES-128 encrypts messages with 10 rounds - _rounds = 10 - - - # initiate the AES objecy - def __init__(self, key): - """ - Initializes the object with a given key. - """ - # make sure key length is right - assert len(key) == AES128.block_size - - # ExpandKey - self._round_keys = self._expand_key(key) - - - # will perform the AES ExpandKey phase - def _expand_key(self, master_key): - """ - Expands and returns a list of key matrices for the given master_key. - """ - - # Initialize round keys with raw key material. - key_columns = bytes2matrix(master_key) - iteration_size = len(master_key) // 4 - - # Each iteration has exactly as many columns as the key material. - i = 1 - while len(key_columns) < (self._rounds + 1) * 4: - # Copy previous word. - word = list(key_columns[-1]) - - # Perform schedule_core once every "row". - if len(key_columns) % iteration_size == 0: - # Circular shift. - word.append(word.pop(0)) - # Map to S-BOX. - word = [s_box[b] for b in word] - # XOR with first byte of R-CON, since the others bytes of R-CON are 0. - word[0] ^= r_con[i] - i += 1 - elif len(master_key) == 32 and len(key_columns) % iteration_size == 4: - # Run word through S-box in the fourth iteration when using a - # 256-bit key. - word = [s_box[b] for b in word] - - # XOR with equivalent word from previous iteration. - word = bytes(i^j for i, j in zip(word, key_columns[-iteration_size])) - key_columns.append(word) - - # Group key words in 4x4 byte matrices. - return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)] - - - # encrypt a single block of data with AES - def _encrypt_block(self, plaintext): - """ - Encrypts a single block of 16 byte long plaintext. - """ - # length of a single block - assert len(plaintext) == AES128.block_size - - # perform on a matrix - state = bytes2matrix(plaintext) - - # AddRoundKey - add_round_key(state, self._round_keys[0]) - - # 9 main rounds - for i in range(1, self._rounds): - # SubBytes - sub_bytes(state) - # ShiftRows - shift_rows(state) - # MixCols - mix_columns(state) - # AddRoundKey - add_round_key(state, self._round_keys[i]) - - # last round, w/t AddRoundKey step - sub_bytes(state) - shift_rows(state) - add_round_key(state, self._round_keys[-1]) - - # return the encrypted matrix as bytes - return matrix2bytes(state) - - - # decrypt a single block of data with AES - def _decrypt_block(self, ciphertext): - """ - Decrypts a single block of 16 byte long ciphertext. - """ - # length of a single block - assert len(ciphertext) == AES128.block_size - - # perform on a matrix - state = bytes2matrix(ciphertext) - - # in reverse order, last round is first - add_round_key(state, self._round_keys[-1]) - inv_shift_rows(state) - inv_sub_bytes(state) - - for i in range(self._rounds - 1, 0, -1): - # nain rounds - add_round_key(state, self._round_keys[i]) - inv_mix_columns(state) - inv_shift_rows(state) - inv_sub_bytes(state) - - # initial AddRoundKey phase - add_round_key(state, self._round_keys[0]) - - # return bytes - return matrix2bytes(state) - - - # will encrypt the entire data - def encrypt(self, plaintext, iv): - """ - Encrypts `plaintext` using CBC mode and PKCS#7 padding, with the given - initialization vector (iv). - """ - # iv length must be same as block size - assert len(iv) == AES128.block_size - - assert len(plaintext) % AES128.block_size == 0 - - ciphertext_blocks = [] - - previous = iv - for plaintext_block in split_blocks(plaintext): - # in CBC mode every block is XOR'd with the previous block - xorred = xor_bytes(plaintext_block, previous) - - # encrypt current block - block = self._encrypt_block(xorred) - previous = block - - # append to ciphertext - ciphertext_blocks.append(block) - - # return as bytes - return b''.join(ciphertext_blocks) - - - # will decrypt the entire data - def decrypt(self, ciphertext, iv): - """ - Decrypts `ciphertext` using CBC mode and PKCS#7 padding, with the given - initialization vector (iv). - """ - # iv length must be same as block size - assert len(iv) == AES128.block_size - - plaintext_blocks = [] - - previous = iv - for ciphertext_block in split_blocks(ciphertext): - # in CBC mode every block is XOR'd with the previous block - xorred = xor_bytes(previous, self._decrypt_block(ciphertext_block)) - - # append plaintext - plaintext_blocks.append(xorred) - previous = ciphertext_block - - return b''.join(plaintext_blocks) diff --git a/RNS/Cryptography/aes/aes256.py b/RNS/Cryptography/aes/utils.py similarity index 60% rename from RNS/Cryptography/aes/aes256.py rename to RNS/Cryptography/aes/utils.py index 1529a66..bc0d2dd 100644 --- a/RNS/Cryptography/aes/aes256.py +++ b/RNS/Cryptography/aes/utils.py @@ -1,17 +1,17 @@ # MIT License -# -# Copyright (c) 2024 BoppreH -# + +# Copyright (c) 2021 Or Gur Arie + # 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 @@ -20,6 +20,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +''' +Utils class for AES encryption / decryption +''' + +## AES lookup tables +# resource: https://en.wikipedia.org/wiki/Rijndael_S-box s_box = ( 0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5, 0x30, 0x01, 0x67, 0x2B, 0xFE, 0xD7, 0xAB, 0x76, 0xCA, 0x82, 0xC9, 0x7D, 0xFA, 0x59, 0x47, 0xF0, 0xAD, 0xD4, 0xA2, 0xAF, 0x9C, 0xA4, 0x72, 0xC0, @@ -58,33 +64,53 @@ inv_s_box = ( 0x17, 0x2B, 0x04, 0x7E, 0xBA, 0x77, 0xD6, 0x26, 0xE1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0C, 0x7D, ) -def sub_bytes(s): - for i in range(4): - for j in range(4): - s[i][j] = s_box[s[i][j]] -def inv_sub_bytes(s): - for i in range(4): - for j in range(4): - s[i][j] = inv_s_box[s[i][j]] - -def shift_rows(s): - s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1] - s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2] - s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3] - -def inv_shift_rows(s): - s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1] - s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2] - s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3] +## AES AddRoundKey +# Round constants https://en.wikipedia.org/wiki/AES_key_schedule#Round_constants +r_con = ( + 0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, + 0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A, + 0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A, + 0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39, +) def add_round_key(s, k): for i in range(4): for j in range(4): s[i][j] ^= k[i][j] + +## AES SubBytes +def sub_bytes(s): + for i in range(4): + for j in range(4): + s[i][j] = s_box[s[i][j]] + + +def inv_sub_bytes(s): + for i in range(4): + for j in range(4): + s[i][j] = inv_s_box[s[i][j]] + + +## AES ShiftRows +def shift_rows(s): + s[0][1], s[1][1], s[2][1], s[3][1] = s[1][1], s[2][1], s[3][1], s[0][1] + s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2] + s[0][3], s[1][3], s[2][3], s[3][3] = s[3][3], s[0][3], s[1][3], s[2][3] + + +def inv_shift_rows(s): + s[0][1], s[1][1], s[2][1], s[3][1] = s[3][1], s[0][1], s[1][1], s[2][1] + s[0][2], s[1][2], s[2][2], s[3][2] = s[2][2], s[3][2], s[0][2], s[1][2] + s[0][3], s[1][3], s[2][3], s[3][3] = s[1][3], s[2][3], s[3][3], s[0][3] + + +## AES MixColumns +# learned from http://cs.ucsb.edu/~koc/cs178/projects/JT/aes.c xtime = lambda a: (((a << 1) ^ 0x1B) & 0xFF) if (a & 0x80) else (a << 1) + def mix_single_column(a): # see Sec 4.1.2 in The Design of Rijndael t = a[0] ^ a[1] ^ a[2] ^ a[3] @@ -94,10 +120,12 @@ def mix_single_column(a): a[2] ^= t ^ xtime(a[2] ^ a[3]) a[3] ^= t ^ xtime(a[3] ^ u) + def mix_columns(s): for i in range(4): mix_single_column(s[i]) + def inv_mix_columns(s): # see Sec 4.1.3 in The Design of Rijndael for i in range(4): @@ -110,127 +138,22 @@ def inv_mix_columns(s): mix_columns(s) -r_con = ( - 0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, - 0x80, 0x1B, 0x36, 0x6C, 0xD8, 0xAB, 0x4D, 0x9A, - 0x2F, 0x5E, 0xBC, 0x63, 0xC6, 0x97, 0x35, 0x6A, - 0xD4, 0xB3, 0x7D, 0xFA, 0xEF, 0xC5, 0x91, 0x39, -) + +## AES Bytes +def bytes2matrix(text): + """ Converts a 16-byte array into a 4x4 matrix. """ + return [list(text[i:i+4]) for i in range(0, len(text), 4)] + +def matrix2bytes(matrix): + """ Converts a 4x4 matrix into a 16-byte array. """ + return bytes(sum(matrix, [])) -def bytes2matrix(text): return [list(text[i:i+4]) for i in range(0, len(text), 4)] -def matrix2bytes(matrix): return bytes(sum(matrix, [])) -def xor_bytes(a, b): return bytes(i^j for i, j in zip(a, b)) +def xor_bytes(a, b): + """ Returns a new byte array with the elements xor'ed. """ + return bytes(i^j for i, j in zip(a, b)) -def inc_bytes(a): - out = list(a) - for i in reversed(range(len(out))): - if out[i] == 0xFF: - out[i] = 0 - else: - out[i] += 1 - break - return bytes(out) def split_blocks(message, block_size=16, require_padding=True): - assert len(message) % block_size == 0 or not require_padding - return [message[i:i+16] for i in range(0, len(message), block_size)] - -class AES256: - rounds_by_key_size = {32: 14} - def __init__(self, master_key): - assert len(master_key) in AES256.rounds_by_key_size - self.n_rounds = AES256.rounds_by_key_size[len(master_key)] - self._key_matrices = self._expand_key(master_key) - - def _expand_key(self, master_key): - # Initialize round keys with raw key material. - key_columns = bytes2matrix(master_key) - iteration_size = len(master_key) // 4 - - i = 1 - while len(key_columns) < (self.n_rounds + 1) * 4: - # Copy previous word. - word = list(key_columns[-1]) - - # Perform schedule_core once every "row". - if len(key_columns) % iteration_size == 0: - # Circular shift. - word.append(word.pop(0)) - # Map to S-BOX. - word = [s_box[b] for b in word] - # XOR with first byte of R-CON, since the others bytes of R-CON are 0. - word[0] ^= r_con[i] - i += 1 - elif len(master_key) == 32 and len(key_columns) % iteration_size == 4: - # Run word through S-box in the fourth iteration when using a - # 256-bit key. - word = [s_box[b] for b in word] - - # XOR with equivalent word from previous iteration. - word = xor_bytes(word, key_columns[-iteration_size]) - key_columns.append(word) - - # Group key words in 4x4 byte matrices. - return [key_columns[4*i : 4*(i+1)] for i in range(len(key_columns) // 4)] - - def encrypt_block(self, plaintext): - assert len(plaintext) == 16 - - plain_state = bytes2matrix(plaintext) - - add_round_key(plain_state, self._key_matrices[0]) - - for i in range(1, self.n_rounds): - sub_bytes(plain_state) - shift_rows(plain_state) - mix_columns(plain_state) - add_round_key(plain_state, self._key_matrices[i]) - - sub_bytes(plain_state) - shift_rows(plain_state) - add_round_key(plain_state, self._key_matrices[-1]) - - return matrix2bytes(plain_state) - - def decrypt_block(self, ciphertext): - assert len(ciphertext) == 16 - - cipher_state = bytes2matrix(ciphertext) - - add_round_key(cipher_state, self._key_matrices[-1]) - inv_shift_rows(cipher_state) - inv_sub_bytes(cipher_state) - - for i in range(self.n_rounds - 1, 0, -1): - add_round_key(cipher_state, self._key_matrices[i]) - inv_mix_columns(cipher_state) - inv_shift_rows(cipher_state) - inv_sub_bytes(cipher_state) - - add_round_key(cipher_state, self._key_matrices[0]) - - return matrix2bytes(cipher_state) - - def encrypt_cbc(self, plaintext, iv): - if len(iv) != 16: raise ValueError(f"Invalid IV length: {len(iv)}") - blocks = [] - previous = iv - for plaintext_block in split_blocks(plaintext): - block = self.encrypt_block(xor_bytes(plaintext_block, previous)) - blocks.append(block) - previous = block - - return b''.join(blocks) - - def decrypt_cbc(self, ciphertext, iv): - if len(iv) != 16: raise ValueError(f"Invalid IV length: {len(iv)}") - blocks = [] - previous = iv - for ciphertext_block in split_blocks(ciphertext): - blocks.append(xor_bytes(previous, self.decrypt_block(ciphertext_block))) - previous = ciphertext_block - - return b''.join(blocks) - -__all__ = ["AES256"] \ No newline at end of file + assert len(message) % block_size == 0 or not require_padding + return [message[i:i+16] for i in range(0, len(message), block_size)] \ No newline at end of file diff --git a/RNS/Destination.py b/RNS/Destination.py index 7f97c23..b2bbda3 100755 --- a/RNS/Destination.py +++ b/RNS/Destination.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, @@ -210,16 +202,12 @@ class Destination: def _persist_ratchets(self): try: with self.ratchet_file_lock: - temp_write_path = self.ratchets_path+".tmp" packed_ratchets = umsgpack.packb(self.ratchets) persisted_data = {"signature": self.sign(packed_ratchets), "ratchets": packed_ratchets} - ratchets_file = open(temp_write_path, "wb") + ratchets_file = open(self.ratchets_path, "wb") ratchets_file.write(umsgpack.packb(persisted_data)) ratchets_file.close() - if os.path.isfile(self.ratchets_path): os.unlink(self.ratchets_path) - os.rename(temp_write_path, self.ratchets_path) except Exception as e: - RNS.trace_exception(e) self.ratchets = None self.ratchets_path = None raise OSError("Could not write ratchet file contents for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR) @@ -377,7 +365,7 @@ class Destination: else: self.proof_strategy = proof_strategy - def register_request_handler(self, path, response_generator = None, allow = ALLOW_NONE, allowed_list = None, auto_compress = True): + def register_request_handler(self, path, response_generator = None, allow = ALLOW_NONE, allowed_list = None): """ Registers a request handler. @@ -385,15 +373,17 @@ class Destination: :param response_generator: A function or method with the signature *response_generator(path, data, request_id, link_id, remote_identity, requested_at)* to be called. Whatever this funcion returns will be sent as a response to the requester. If the function returns ``None``, no response will be sent. :param allow: One of ``RNS.Destination.ALLOW_NONE``, ``RNS.Destination.ALLOW_ALL`` or ``RNS.Destination.ALLOW_LIST``. If ``RNS.Destination.ALLOW_LIST`` is set, the request handler will only respond to requests for identified peers in the supplied list. :param allowed_list: A list of *bytes-like* :ref:`RNS.Identity` hashes. - :param auto_compress: If ``True`` or ``False``, determines whether automatic compression of responses should be carried out. If set to an integer value, responses will only be auto-compressed if under this size in bytes. If omitted, the default compression settings will be followed. :raises: ``ValueError`` if any of the supplied arguments are invalid. """ - if path == None or path == "": raise ValueError("Invalid path specified") - elif not callable(response_generator): raise ValueError("Invalid response generator specified") - elif not allow in Destination.request_policies: raise ValueError("Invalid request policy") + if path == None or path == "": + raise ValueError("Invalid path specified") + elif not callable(response_generator): + raise ValueError("Invalid response generator specified") + elif not allow in Destination.request_policies: + raise ValueError("Invalid request policy") else: path_hash = RNS.Identity.truncated_hash(path.encode("utf-8")) - request_handler = [path, response_generator, allow, allowed_list, auto_compress] + request_handler = [path, response_generator, allow, allowed_list] self.request_handlers[path_hash] = request_handler def deregister_request_handler(self, path): @@ -417,8 +407,7 @@ class Destination: else: plaintext = self.decrypt(packet.data) packet.ratchet_id = self.latest_ratchet_id - if plaintext == None: return False - else: + if plaintext != None: if packet.packet_type == RNS.Packet.DATA: if self.callbacks.packet != None: try: @@ -426,8 +415,6 @@ class Destination: except Exception as e: RNS.log("Error while executing receive callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR) - return True - def incoming_link_request(self, data, packet): if self.accept_link_requests: link = RNS.Link.validate_request(self, data, packet) @@ -463,7 +450,6 @@ class Destination: self.ratchets_path = None RNS.trace_exception(e) raise OSError("Could not read ratchet file contents for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR) - else: RNS.log("No existing ratchet data found, initialising new ratchet file for "+str(self), RNS.LOG_DEBUG) self.ratchets = [] @@ -489,6 +475,7 @@ class Destination: self.latest_ratchet_time = 0 self._reload_ratchets(ratchets_path) + # TODO: Remove at some point RNS.log("Ratchets enabled on "+str(self), RNS.LOG_DEBUG) return True @@ -642,7 +629,7 @@ class Destination: RNS.log(f"Decryption still failing after ratchet reload. The contained exception was: {e}", RNS.LOG_ERROR) raise e - if decrypted: RNS.log("Decryption succeeded after ratchet reload", RNS.LOG_NOTICE) + RNS.log("Decryption succeeded after ratchet reload", RNS.LOG_NOTICE) return decrypted diff --git a/RNS/Identity.py b/RNS/Identity.py index 7fb2be5..5c39b2a 100644 --- a/RNS/Identity.py +++ b/RNS/Identity.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, @@ -79,16 +71,13 @@ class Identity: HASHLENGTH = 256 # In bits SIGLENGTH = KEYSIZE # In bits - NAME_HASH_LENGTH = 80 - TRUNCATED_HASHLENGTH = RNS.Reticulum.TRUNCATED_HASHLENGTH + NAME_HASH_LENGTH = 80 + TRUNCATED_HASHLENGTH = RNS.Reticulum.TRUNCATED_HASHLENGTH """ Constant specifying the truncated hash length (in bits) used by Reticulum for addressable hashes and other purposes. Non-configurable. """ - DERIVED_KEY_LENGTH = 512//8 - DERIVED_KEY_LENGTH_LEGACY = 256//8 - # Storage known_destinations = {} known_ratchets = {} @@ -680,7 +669,7 @@ class Identity: shared_key = ephemeral_key.exchange(target_public_key) derived_key = RNS.Cryptography.hkdf( - length=Identity.DERIVED_KEY_LENGTH, + length=32, derive_from=shared_key, salt=self.get_salt(), context=self.get_context(), @@ -694,16 +683,6 @@ class Identity: else: raise KeyError("Encryption failed because identity does not hold a public key") - def __decrypt(self, shared_key, ciphertext): - derived_key = RNS.Cryptography.hkdf( - length=Identity.DERIVED_KEY_LENGTH, - derive_from=shared_key, - salt=self.get_salt(), - context=self.get_context()) - - token = Token(derived_key) - plaintext = token.decrypt(ciphertext) - return plaintext def decrypt(self, ciphertext_token, ratchets=None, enforce_ratchets=False, ratchet_id_receiver=None): """ @@ -713,7 +692,6 @@ class Identity: :returns: Plaintext as *bytes*, or *None* if decryption fails. :raises: *KeyError* if the instance does not hold a private key. """ - if self.prv != None: if len(ciphertext_token) > Identity.KEYSIZE//8//2: plaintext = None @@ -728,7 +706,15 @@ class Identity: ratchet_prv = X25519PrivateKey.from_private_bytes(ratchet) ratchet_id = Identity._get_ratchet_id(ratchet_prv.public_key().public_bytes()) shared_key = ratchet_prv.exchange(peer_pub) - plaintext = self.__decrypt(shared_key, ciphertext) + derived_key = RNS.Cryptography.hkdf( + length=32, + derive_from=shared_key, + salt=self.get_salt(), + context=self.get_context(), + ) + + token = Token(derived_key) + plaintext = token.decrypt(ciphertext) if ratchet_id_receiver: ratchet_id_receiver.latest_ratchet_id = ratchet_id @@ -745,8 +731,15 @@ class Identity: if plaintext == None: shared_key = self.prv.exchange(peer_pub) - plaintext = self.__decrypt(shared_key, ciphertext) + derived_key = RNS.Cryptography.hkdf( + length=32, + derive_from=shared_key, + salt=self.get_salt(), + context=self.get_context(), + ) + token = Token(derived_key) + plaintext = token.decrypt(ciphertext) if ratchet_id_receiver: ratchet_id_receiver.latest_ratchet_id = None @@ -755,8 +748,7 @@ class Identity: if ratchet_id_receiver: ratchet_id_receiver.latest_ratchet_id = None - return plaintext - + return plaintext; else: RNS.log("Decryption failed because the token size was invalid.", RNS.LOG_DEBUG) return None diff --git a/RNS/Interfaces/AX25KISSInterface.py b/RNS/Interfaces/AX25KISSInterface.py index abda34f..2df42ba 100644 --- a/RNS/Interfaces/AX25KISSInterface.py +++ b/RNS/Interfaces/AX25KISSInterface.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Interfaces/Android/KISSInterface.py b/RNS/Interfaces/Android/KISSInterface.py index b14f45c..c4d33a0 100644 --- a/RNS/Interfaces/Android/KISSInterface.py +++ b/RNS/Interfaces/Android/KISSInterface.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Interfaces/Android/RNodeInterface.py b/RNS/Interfaces/Android/RNodeInterface.py index d720df8..62fab51 100644 --- a/RNS/Interfaces/Android/RNodeInterface.py +++ b/RNS/Interfaces/Android/RNodeInterface.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Interfaces/Android/SerialInterface.py b/RNS/Interfaces/Android/SerialInterface.py index 2e0cb17..41b6276 100644 --- a/RNS/Interfaces/Android/SerialInterface.py +++ b/RNS/Interfaces/Android/SerialInterface.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Interfaces/Android/__init__.py b/RNS/Interfaces/Android/__init__.py index a9a74ff..299b693 100644 --- a/RNS/Interfaces/Android/__init__.py +++ b/RNS/Interfaces/Android/__init__.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2022 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Interfaces/AutoInterface.py b/RNS/Interfaces/AutoInterface.py index 5bdf468..993fbb8 100644 --- a/RNS/Interfaces/AutoInterface.py +++ b/RNS/Interfaces/AutoInterface.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Interfaces/BackboneInterface.py b/RNS/Interfaces/BackboneInterface.py index af883d2..8ecef12 100644 --- a/RNS/Interfaces/BackboneInterface.py +++ b/RNS/Interfaces/BackboneInterface.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, @@ -319,8 +311,7 @@ class BackboneInterface(Interface): client_socket, address = server_socket.accept() client_socket.setblocking(0) if not owner_interface.incoming_connection(client_socket): - try: client_socket.close() - except Exception as e: RNS.log(f"Error while closing socket for failed incoming connection: {e}", RNS.LOG_ERROR) + client_socket.close() elif fileno == server_socket.fileno() and (event & select.EPOLLHUP): try: BackboneInterface.deregister_fileno(fileno) @@ -338,53 +329,48 @@ class BackboneInterface(Interface): def incoming_connection(self, socket): RNS.log("Accepting incoming connection", RNS.LOG_VERBOSE) - try: - spawned_configuration = {"name": "Client on "+self.name, "target_host": None, "target_port": None} - spawned_interface = BackboneClientInterface(self.owner, spawned_configuration, connected_socket=socket) - spawned_interface.OUT = self.OUT - spawned_interface.IN = self.IN - spawned_interface.socket = socket - spawned_interface.target_ip = socket.getpeername()[0] - spawned_interface.target_port = str(socket.getpeername()[1]) - spawned_interface.parent_interface = self - spawned_interface.bitrate = self.bitrate - spawned_interface.optimise_mtu() - - spawned_interface.ifac_size = self.ifac_size - spawned_interface.ifac_netname = self.ifac_netname - spawned_interface.ifac_netkey = self.ifac_netkey - if spawned_interface.ifac_netname != None or spawned_interface.ifac_netkey != None: - ifac_origin = b"" - if spawned_interface.ifac_netname != None: - ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netname.encode("utf-8")) - if spawned_interface.ifac_netkey != None: - ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netkey.encode("utf-8")) + spawned_configuration = {"name": "Client on "+self.name, "target_host": None, "target_port": None} + spawned_interface = BackboneClientInterface(self.owner, spawned_configuration, connected_socket=socket) + spawned_interface.OUT = self.OUT + spawned_interface.IN = self.IN + spawned_interface.socket = socket + spawned_interface.target_ip = socket.getpeername()[0] + spawned_interface.target_port = str(socket.getpeername()[1]) + spawned_interface.parent_interface = self + spawned_interface.bitrate = self.bitrate + spawned_interface.optimise_mtu() + + spawned_interface.ifac_size = self.ifac_size + spawned_interface.ifac_netname = self.ifac_netname + spawned_interface.ifac_netkey = self.ifac_netkey + if spawned_interface.ifac_netname != None or spawned_interface.ifac_netkey != None: + ifac_origin = b"" + if spawned_interface.ifac_netname != None: + ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netname.encode("utf-8")) + if spawned_interface.ifac_netkey != None: + ifac_origin += RNS.Identity.full_hash(spawned_interface.ifac_netkey.encode("utf-8")) - ifac_origin_hash = RNS.Identity.full_hash(ifac_origin) - spawned_interface.ifac_key = RNS.Cryptography.hkdf( - length=64, - derive_from=ifac_origin_hash, - salt=RNS.Reticulum.IFAC_SALT, - context=None - ) - spawned_interface.ifac_identity = RNS.Identity.from_bytes(spawned_interface.ifac_key) - spawned_interface.ifac_signature = spawned_interface.ifac_identity.sign(RNS.Identity.full_hash(spawned_interface.ifac_key)) + ifac_origin_hash = RNS.Identity.full_hash(ifac_origin) + spawned_interface.ifac_key = RNS.Cryptography.hkdf( + length=64, + derive_from=ifac_origin_hash, + salt=RNS.Reticulum.IFAC_SALT, + context=None + ) + spawned_interface.ifac_identity = RNS.Identity.from_bytes(spawned_interface.ifac_key) + spawned_interface.ifac_signature = spawned_interface.ifac_identity.sign(RNS.Identity.full_hash(spawned_interface.ifac_key)) - spawned_interface.announce_rate_target = self.announce_rate_target - spawned_interface.announce_rate_grace = self.announce_rate_grace - spawned_interface.announce_rate_penalty = self.announce_rate_penalty - spawned_interface.mode = self.mode - spawned_interface.HW_MTU = self.HW_MTU - spawned_interface.online = True - RNS.log("Spawned new BackboneClient Interface: "+str(spawned_interface), RNS.LOG_VERBOSE) - RNS.Transport.interfaces.append(spawned_interface) - while spawned_interface in self.spawned_interfaces: self.spawned_interfaces.remove(spawned_interface) - self.spawned_interfaces.append(spawned_interface) - BackboneInterface.add_client_socket(socket, spawned_interface) - - except Exception as e: - RNS.log(f"An error occurred while accepting incoming connection on {self}: {e}", RNS.LOG_ERROR) - return False + spawned_interface.announce_rate_target = self.announce_rate_target + spawned_interface.announce_rate_grace = self.announce_rate_grace + spawned_interface.announce_rate_penalty = self.announce_rate_penalty + spawned_interface.mode = self.mode + spawned_interface.HW_MTU = self.HW_MTU + spawned_interface.online = True + RNS.log("Spawned new BackboneClient Interface: "+str(spawned_interface), RNS.LOG_VERBOSE) + RNS.Transport.interfaces.append(spawned_interface) + while spawned_interface in self.spawned_interfaces: self.spawned_interfaces.remove(spawned_interface) + self.spawned_interfaces.append(spawned_interface) + BackboneInterface.add_client_socket(socket, spawned_interface) return True diff --git a/RNS/Interfaces/I2PInterface.py b/RNS/Interfaces/I2PInterface.py index 6cac70c..78c802d 100644 --- a/RNS/Interfaces/I2PInterface.py +++ b/RNS/Interfaces/I2PInterface.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Interfaces/Interface.py b/RNS/Interfaces/Interface.py index cf00f4e..41583c5 100755 --- a/RNS/Interfaces/Interface.py +++ b/RNS/Interfaces/Interface.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, @@ -135,23 +127,23 @@ class Interface: def optimise_mtu(self): if self.AUTOCONFIGURE_MTU: - if self.bitrate >= 1_000_000_000: + if self.bitrate > 500_000_000: self.HW_MTU = 524288 - elif self.bitrate > 750_000_000: + elif self.bitrate > 16_000_000: self.HW_MTU = 262144 - elif self.bitrate > 400_000_000: + elif self.bitrate > 8_000_000: self.HW_MTU = 131072 - elif self.bitrate > 200_000_000: + elif self.bitrate > 4_000_000: self.HW_MTU = 65536 - elif self.bitrate > 100_000_000: - self.HW_MTU = 32768 - elif self.bitrate > 10_000_000: - self.HW_MTU = 16384 - elif self.bitrate > 5_000_000: - self.HW_MTU = 8192 elif self.bitrate > 2_000_000: - self.HW_MTU = 4096 + self.HW_MTU = 32768 elif self.bitrate > 1_000_000: + self.HW_MTU = 16384 + elif self.bitrate > 500_000: + self.HW_MTU = 8192 + elif self.bitrate > 250_000: + self.HW_MTU = 4096 + elif self.bitrate > 125_000: self.HW_MTU = 2048 elif self.bitrate > 62_500: self.HW_MTU = 1024 diff --git a/RNS/Interfaces/KISSInterface.py b/RNS/Interfaces/KISSInterface.py index d343f0a..3a0c3eb 100644 --- a/RNS/Interfaces/KISSInterface.py +++ b/RNS/Interfaces/KISSInterface.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Interfaces/LocalInterface.py b/RNS/Interfaces/LocalInterface.py index 445ba28..5f318fa 100644 --- a/RNS/Interfaces/LocalInterface.py +++ b/RNS/Interfaces/LocalInterface.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, @@ -70,7 +62,7 @@ class LocalClientInterface(Interface): self.HW_MTU = 262144 self.online = False - if socket_path != None and RNS.Reticulum.get_instance().use_af_unix: self.socket_path = f"\0rns/{socket_path}" + if socket_path != None and RNS.vendor.platformutils.use_af_unix(): self.socket_path = f"\0rns/{socket_path}" else: self.socket_path = None self.IN = True @@ -337,8 +329,8 @@ class LocalClientInterface(Interface): def __str__(self): - if self.socket_path: return "LocalInterface["+str(self.socket_path.replace("\0", ""))+"]" - else: return "LocalInterface["+str(self.target_port)+"]" + if self.socket_path: return "Shared Instance["+str(self.socket_path.replace("\0", ""))+"]" + else: return "Shared Instance["+str(self.target_port)+"]" class LocalServerInterface(Interface): @@ -350,7 +342,7 @@ class LocalServerInterface(Interface): self.online = False self.clients = 0 - if socket_path != None and RNS.Reticulum.get_instance().use_af_unix: self.socket_path = f"\0rns/{socket_path}" + if socket_path != None and RNS.vendor.platformutils.use_af_unix(): self.socket_path = f"\0rns/{socket_path}" else: self.socket_path = None self.IN = True diff --git a/RNS/Interfaces/PipeInterface.py b/RNS/Interfaces/PipeInterface.py index 1574753..27e96fc 100644 --- a/RNS/Interfaces/PipeInterface.py +++ b/RNS/Interfaces/PipeInterface.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Interfaces/RNodeInterface.py b/RNS/Interfaces/RNodeInterface.py index f9f3790..385d084 100644 --- a/RNS/Interfaces/RNodeInterface.py +++ b/RNS/Interfaces/RNodeInterface.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Interfaces/RNodeMultiInterface.py b/RNS/Interfaces/RNodeMultiInterface.py index d08a8fd..21477f6 100644 --- a/RNS/Interfaces/RNodeMultiInterface.py +++ b/RNS/Interfaces/RNodeMultiInterface.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2024 Jacob Eva. Adapted from the RNodeInterface by Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Interfaces/SerialInterface.py b/RNS/Interfaces/SerialInterface.py index 7c9d0da..4a68971 100755 --- a/RNS/Interfaces/SerialInterface.py +++ b/RNS/Interfaces/SerialInterface.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Interfaces/TCPInterface.py b/RNS/Interfaces/TCPInterface.py index 0a15220..2745c32 100644 --- a/RNS/Interfaces/TCPInterface.py +++ b/RNS/Interfaces/TCPInterface.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Interfaces/UDPInterface.py b/RNS/Interfaces/UDPInterface.py index 8966bbe..db1bcad 100644 --- a/RNS/Interfaces/UDPInterface.py +++ b/RNS/Interfaces/UDPInterface.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Interfaces/__init__.py b/RNS/Interfaces/__init__.py index 9a11d8f..db199c4 100755 --- a/RNS/Interfaces/__init__.py +++ b/RNS/Interfaces/__init__.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Interfaces/util/netinfo.py b/RNS/Interfaces/util/netinfo.py index 7a88d7c..12b08cc 100644 --- a/RNS/Interfaces/util/netinfo.py +++ b/RNS/Interfaces/util/netinfo.py @@ -1,7 +1,7 @@ # MIT License # -# Copyright (c) 2014 Stefan C. Mueller # Copyright (c) 2025 Mark Qvist +# Copyright (c) 2014 Stefan C. Mueller # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal diff --git a/RNS/Link.py b/RNS/Link.py index f65ce12..4ddd67d 100644 --- a/RNS/Link.py +++ b/RNS/Link.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, @@ -40,7 +32,6 @@ import struct import math import time import RNS -import io class LinkCallbacks: def __init__(self): @@ -80,23 +71,19 @@ class Link: LINK_MTU_SIZE = 3 TRAFFIC_TIMEOUT_MIN_MS = 5 TRAFFIC_TIMEOUT_FACTOR = 6 - KEEPALIVE_MAX_RTT = 1.75 KEEPALIVE_TIMEOUT_FACTOR = 4 """ RTT timeout factor used in link timeout calculation. """ - STALE_GRACE = 5 + STALE_GRACE = 2 """ Grace period in seconds used in link timeout calculation. """ - KEEPALIVE_MAX = 360 - KEEPALIVE_MIN = 5 - KEEPALIVE = KEEPALIVE_MAX + KEEPALIVE = 360 """ - Default interval for sending keep-alive packets on established links in seconds. + Interval for sending keep-alive packets on established links in seconds. """ - STALE_FACTOR = 2 - STALE_TIME = STALE_FACTOR*KEEPALIVE + STALE_TIME = 2*KEEPALIVE """ If no traffic or keep-alive packets are received within this period, the link will be marked as stale, and a final keep-alive packet will be sent. @@ -105,82 +92,39 @@ class Link: and will be torn down. """ - WATCHDOG_MAX_SLEEP = 5 + PENDING = 0x00 + HANDSHAKE = 0x01 + ACTIVE = 0x02 + STALE = 0x03 + CLOSED = 0x04 - PENDING = 0x00 - HANDSHAKE = 0x01 - ACTIVE = 0x02 - STALE = 0x03 - CLOSED = 0x04 + TIMEOUT = 0x01 + INITIATOR_CLOSED = 0x02 + DESTINATION_CLOSED = 0x03 - TIMEOUT = 0x01 - INITIATOR_CLOSED = 0x02 - DESTINATION_CLOSED = 0x03 - - ACCEPT_NONE = 0x00 - ACCEPT_APP = 0x01 - ACCEPT_ALL = 0x02 + ACCEPT_NONE = 0x00 + ACCEPT_APP = 0x01 + ACCEPT_ALL = 0x02 resource_strategies = [ACCEPT_NONE, ACCEPT_APP, ACCEPT_ALL] - MODE_AES128_CBC = 0x00 - MODE_AES256_CBC = 0x01 - MODE_AES256_GCM = 0x02 - MODE_OTP_RESERVED = 0x03 - MODE_PQ_RESERVED_1 = 0x04 - MODE_PQ_RESERVED_2 = 0x05 - MODE_PQ_RESERVED_3 = 0x06 - MODE_PQ_RESERVED_4 = 0x07 - ENABLED_MODES = [MODE_AES256_CBC] - MODE_DEFAULT = MODE_AES256_CBC - MODE_DESCRIPTIONS = {MODE_AES128_CBC: "AES_128_CBC", - MODE_AES256_CBC: "AES_256_CBC", - MODE_AES256_GCM: "MODE_AES256_GCM", - MODE_OTP_RESERVED: "MODE_OTP_RESERVED", - MODE_PQ_RESERVED_1: "MODE_PQ_RESERVED_1", - MODE_PQ_RESERVED_2: "MODE_PQ_RESERVED_2", - MODE_PQ_RESERVED_3: "MODE_PQ_RESERVED_3", - MODE_PQ_RESERVED_4: "MODE_PQ_RESERVED_4"} - - MTU_BYTEMASK = 0x1FFFFF - MODE_BYTEMASK = 0xE0 - @staticmethod - def signalling_bytes(mtu, mode): - if not mode in Link.ENABLED_MODES: raise TypeError(f"Requested link mode {Link.MODE_DESCRIPTIONS[mode]} not enabled") - signalling_value = (mtu & Link.MTU_BYTEMASK)+(((mode<<5) & Link.MODE_BYTEMASK)<<16) - return struct.pack(">I", signalling_value)[1:] + def mtu_bytes(mtu): + return struct.pack(">I", mtu & 0xFFFFFF)[1:] @staticmethod def mtu_from_lr_packet(packet): if len(packet.data) == Link.ECPUBSIZE+Link.LINK_MTU_SIZE: - return (packet.data[Link.ECPUBSIZE] << 16) + (packet.data[Link.ECPUBSIZE+1] << 8) + (packet.data[Link.ECPUBSIZE+2]) & Link.MTU_BYTEMASK - else: return None + return (packet.data[Link.ECPUBSIZE] << 16) + (packet.data[Link.ECPUBSIZE+1] << 8) + (packet.data[Link.ECPUBSIZE+2]) + else: + return None @staticmethod def mtu_from_lp_packet(packet): if len(packet.data) == RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2+Link.LINK_MTU_SIZE: mtu_bytes = packet.data[RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2:RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2+Link.LINK_MTU_SIZE] - return (mtu_bytes[0] << 16) + (mtu_bytes[1] << 8) + (mtu_bytes[2]) & Link.MTU_BYTEMASK - else: return None - - @staticmethod - def mode_byte(mode): - if mode in Link.ENABLED_MODES: return (mode << 5) & Link.MODE_BYTEMASK - else: raise TypeError(f"Requested link mode {mode} not enabled") - - @staticmethod - def mode_from_lr_packet(packet): - if len(packet.data) > Link.ECPUBSIZE: - mode = (packet.data[Link.ECPUBSIZE] & Link.MODE_BYTEMASK) >> 5 - return mode - else: return Link.MODE_DEFAULT - - @staticmethod - def mode_from_lp_packet(packet): - if len(packet.data) > RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2: - mode = packet.data[RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2] >> 5 - return mode - else: return Link.MODE_DEFAULT + return (mtu_bytes[0] << 16) + (mtu_bytes[1] << 8) + (mtu_bytes[2]) + else: + return None @staticmethod def validate_request(owner, data, packet): @@ -197,11 +141,6 @@ class Link: RNS.trace_exception(e) link.mtu = RNS.Reticulum.MTU - link.mode = Link.mode_from_lr_packet(packet) - - # TODO: Remove debug - RNS.log(f"Incoming link request with mode {Link.MODE_DESCRIPTIONS[link.mode]}", RNS.LOG_DEBUG) - link.update_mdu() link.destination = packet.destination link.establishment_timeout = Link.ESTABLISHMENT_TIMEOUT_PER_HOP * max(1, packet.hops) + Link.KEEPALIVE @@ -230,9 +169,9 @@ class Link: return None - def __init__(self, destination=None, established_callback=None, closed_callback=None, owner=None, peer_pub_bytes=None, peer_sig_pub_bytes=None, mode=MODE_DEFAULT): - if destination != None and destination.type != RNS.Destination.SINGLE: raise TypeError("Links can only be established to the \"single\" destination type") - self.mode = mode + def __init__(self, destination=None, established_callback = None, closed_callback = None, owner=None, peer_pub_bytes = None, peer_sig_pub_bytes = None): + if destination != None and destination.type != RNS.Destination.SINGLE: + raise TypeError("Links can only be established to the \"single\" destination type") self.rtt = None self.mtu = RNS.Reticulum.MTU self.establishment_cost = 0 @@ -247,7 +186,6 @@ class Link: self.pending_requests = [] self.last_inbound = 0 self.last_outbound = 0 - self.last_keepalive = 0 self.last_proof = 0 self.last_data = 0 self.tx = 0 @@ -306,14 +244,12 @@ class Link: self.set_link_closed_callback(closed_callback) if self.initiator: - signalling_bytes = b"" + link_mtu = b"" nh_hw_mtu = RNS.Transport.next_hop_interface_hw_mtu(destination.hash) if RNS.Reticulum.link_mtu_discovery() and nh_hw_mtu: - signalling_bytes = Link.signalling_bytes(nh_hw_mtu, self.mode) + link_mtu = Link.mtu_bytes(nh_hw_mtu) RNS.log(f"Signalling link MTU of {RNS.prettysize(nh_hw_mtu)} for link", RNS.LOG_DEBUG) # TODO: Remove debug - else: signalling_bytes = Link.signalling_bytes(RNS.Reticulum.MTU, self.mode) - RNS.log(f"Establishing link with mode {Link.MODE_DESCRIPTIONS[self.mode]}", RNS.LOG_DEBUG) # TODO: Remove debug - self.request_data = self.pub_bytes+self.sig_pub_bytes+signalling_bytes + self.request_data = self.pub_bytes+self.sig_pub_bytes+link_mtu self.packet = RNS.Packet(destination, self.request_data, packet_type=RNS.Packet.LINKREQUEST) self.packet.pack() self.establishment_cost += len(self.packet.raw) @@ -355,25 +291,25 @@ class Link: self.status = Link.HANDSHAKE self.shared_key = self.prv.exchange(self.peer_pub) - if self.mode == Link.MODE_AES128_CBC: derived_key_length = 32 - elif self.mode == Link.MODE_AES256_CBC: derived_key_length = 64 - else: raise TypeError(f"Invalid link mode {self.mode} on {self}") - self.derived_key = RNS.Cryptography.hkdf( - length=derived_key_length, + length=32, derive_from=self.shared_key, salt=self.get_salt(), - context=self.get_context()) - - else: RNS.log("Handshake attempt on "+str(self)+" with invalid state "+str(self.status), RNS.LOG_ERROR) + context=self.get_context(), + ) + else: + RNS.log("Handshake attempt on "+str(self)+" with invalid state "+str(self.status), RNS.LOG_ERROR) def prove(self): - signalling_bytes = Link.signalling_bytes(self.mtu, self.mode) - signed_data = self.link_id+self.pub_bytes+self.sig_pub_bytes+signalling_bytes + mtu_bytes = b"" + if self.mtu != RNS.Reticulum.MTU: + mtu_bytes = Link.mtu_bytes(self.mtu) + + signed_data = self.link_id+self.pub_bytes+self.sig_pub_bytes+mtu_bytes signature = self.owner.identity.sign(signed_data) - proof_data = signature+self.pub_bytes+signalling_bytes + proof_data = signature+self.pub_bytes+mtu_bytes proof = RNS.Packet(self, proof_data, packet_type=RNS.Packet.PROOF, context=RNS.Packet.LRPROOF) proof.send() self.establishment_cost += len(proof.raw) @@ -396,14 +332,11 @@ class Link: def validate_proof(self, packet): try: if self.status == Link.PENDING: - signalling_bytes = b"" + mtu_bytes = b"" confirmed_mtu = None - mode = Link.mode_from_lp_packet(packet) - RNS.log(f"Validating link request proof with mode {Link.MODE_DESCRIPTIONS[mode]}", RNS.LOG_DEBUG) # TODO: Remove debug - if mode != self.mode: raise TypeError(f"Invalid link mode {mode} in link request proof") if len(packet.data) == RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2+Link.LINK_MTU_SIZE: confirmed_mtu = Link.mtu_from_lp_packet(packet) - signalling_bytes = Link.signalling_bytes(confirmed_mtu, mode) + mtu_bytes = Link.mtu_bytes(confirmed_mtu) packet.data = packet.data[:RNS.Identity.SIGLENGTH//8+Link.ECPUBSIZE//2] RNS.log(f"Destination confirmed link MTU of {RNS.prettysize(confirmed_mtu)}", RNS.LOG_DEBUG) # TODO: Remove debug @@ -414,7 +347,7 @@ class Link: self.handshake() self.establishment_cost += len(packet.raw) - signed_data = self.link_id+self.peer_pub_bytes+self.peer_sig_pub_bytes+signalling_bytes + signed_data = self.link_id+self.peer_pub_bytes+self.peer_sig_pub_bytes+mtu_bytes signature = packet.data[:RNS.Identity.SIGLENGTH//8] if self.destination.identity.validate(signature, signed_data): @@ -430,13 +363,11 @@ class Link: self.activated_at = time.time() self.last_proof = self.activated_at RNS.Transport.activate_link(self) - RNS.log("Link "+str(self)+" established with "+str(self.destination)+", RTT is "+RNS.prettyshorttime(self.rtt), RNS.LOG_DEBUG) + RNS.log("Link "+str(self)+" established with "+str(self.destination)+", RTT is "+str(round(self.rtt, 3))+"s", RNS.LOG_DEBUG) if self.rtt != None and self.establishment_cost != None and self.rtt > 0 and self.establishment_cost > 0: self.establishment_rate = self.establishment_cost/self.rtt - self.__update_keepalive() - rtt_data = umsgpack.packb(self.rtt) rtt_packet = RNS.Packet(self, rtt_data, context=RNS.Packet.LRRTT) rtt_packet.send() @@ -544,8 +475,6 @@ class Link: if self.rtt != None and self.establishment_cost != None and self.rtt > 0 and self.establishment_cost > 0: self.establishment_rate = self.establishment_cost/self.rtt - self.__update_keepalive() - try: if self.owner.callbacks.link_established != None: self.owner.callbacks.link_established(self) @@ -633,12 +562,6 @@ class Link: else: return None - def get_mode(self): - """ - :returns: The mode of an established link. - """ - return self.mode - def get_salt(self): return self.link_id @@ -688,23 +611,23 @@ class Link: def had_outbound(self, is_keepalive=False): self.last_outbound = time.time() - if not is_keepalive: self.last_data = self.last_outbound - else: self.last_keepalive = self.last_outbound - - def __teardown_packet(self): - teardown_packet = RNS.Packet(self, self.link_id, context=RNS.Packet.LINKCLOSE) - teardown_packet.send() - self.had_outbound() + if not is_keepalive: + self.last_data = self.last_outbound def teardown(self): """ Closes the link and purges encryption keys. New keys will be used if a new link to the same destination is established. """ - if self.status != Link.PENDING and self.status != Link.CLOSED: self.__teardown_packet() + if self.status != Link.PENDING and self.status != Link.CLOSED: + teardown_packet = RNS.Packet(self, self.link_id, context=RNS.Packet.LINKCLOSE) + teardown_packet.send() + self.had_outbound() self.status = Link.CLOSED - if self.initiator: self.teardown_reason = Link.INITIATOR_CLOSED - else: self.teardown_reason = Link.DESTINATION_CLOSED + if self.initiator: + self.teardown_reason = Link.INITIATOR_CLOSED + else: + self.teardown_reason = Link.DESTINATION_CLOSED self.link_closed() def teardown_packet(self, packet): @@ -791,10 +714,9 @@ class Link: elif self.status == Link.ACTIVE: activated_at = self.activated_at if self.activated_at != None else 0 last_inbound = max(max(self.last_inbound, self.last_proof), activated_at) - now = time.time() - if now >= last_inbound + self.keepalive: - if self.initiator and now >= self.last_keepalive + self.keepalive: + if time.time() >= last_inbound + self.keepalive: + if self.initiator: self.send_keepalive() if time.time() >= last_inbound + self.stale_time: @@ -808,7 +730,6 @@ class Link: elif self.status == Link.STALE: sleep_time = 0.001 - self.__teardown_packet() self.status = Link.CLOSED self.teardown_reason = Link.TIMEOUT self.link_closed() @@ -821,7 +742,6 @@ class Link: self.teardown() sleep_time = 0.1 - sleep_time = min(sleep_time, Link.WATCHDOG_MAX_SLEEP) sleep(sleep_time) if not self.__track_phy_stats: @@ -844,10 +764,6 @@ class Link: self.snr = packet.snr if packet.q != None: self.q = packet.q - - def __update_keepalive(self): - self.keepalive = max(min(self.rtt*(Link.KEEPALIVE_MAX/Link.KEEPALIVE_MAX_RTT), Link.KEEPALIVE_MAX), Link.KEEPALIVE_MIN) - self.stale_time = self.keepalive * Link.STALE_FACTOR def send_keepalive(self): keepalive_packet = RNS.Packet(self, bytes([0xFF]), context=RNS.Packet.KEEPALIVE) @@ -866,7 +782,6 @@ class Link: response_generator = request_handler[1] allow = request_handler[2] allowed_list = request_handler[3] - auto_compress = request_handler[4] allowed = False if not allow == RNS.Destination.ALLOW_NONE: @@ -885,29 +800,18 @@ class Link: else: raise TypeError("Invalid signature for response generator callback") - file_response = False - file_handle = None - if type(response) == list or type(response) == tuple: - metadata = None - if len(response) > 0 and type(response[0]) == io.BufferedReader: - if len(response) > 1: metadata = response[1] - file_handle = response[0] - file_response = True - if response != None: - if file_response: - response_resource = RNS.Resource(file_handle, self, metadata=metadata, request_id = request_id, is_response = True, auto_compress=auto_compress) + packed_response = umsgpack.packb([request_id, response]) + + if len(packed_response) <= self.mdu: + RNS.Packet(self, packed_response, RNS.Packet.DATA, context = RNS.Packet.RESPONSE).send() else: - packed_response = umsgpack.packb([request_id, response]) - if len(packed_response) <= self.mdu: - RNS.Packet(self, packed_response, RNS.Packet.DATA, context = RNS.Packet.RESPONSE).send() - else: - response_resource = RNS.Resource(packed_response, self, request_id = request_id, is_response = True, auto_compress=auto_compress) + response_resource = RNS.Resource(packed_response, self, request_id = request_id, is_response = True) else: identity_string = str(self.get_remote_identity()) if self.get_remote_identity() != None else "" RNS.log("Request "+RNS.prettyhexrep(request_id)+" from "+identity_string+" not allowed for: "+str(path), RNS.LOG_DEBUG) - def handle_response(self, request_id, response_data, response_size, response_transfer_size, metadata=None): + def handle_response(self, request_id, response_data, response_size, response_transfer_size): if self.status == Link.ACTIVE: remove = None for pending_request in self.pending_requests: @@ -918,7 +822,7 @@ class Link: if pending_request.response_transfer_size == None: pending_request.response_transfer_size = 0 pending_request.response_transfer_size += response_transfer_size - pending_request.response_received(response_data, metadata) + pending_request.response_received(response_data) except Exception as e: RNS.log("Error occurred while handling response. The contained exception was: "+str(e), RNS.LOG_ERROR) @@ -941,21 +845,12 @@ class Link: def response_resource_concluded(self, resource): if resource.status == RNS.Resource.COMPLETE: - # If the response resource has metadata, this - # is a file response, and we'll pass the open - # file handle directly. - if resource.has_metadata: - self.handle_response(resource.request_id, resource.data, resource.total_size, resource.size, metadata=resource.metadata) - - # If not, we'll unpack the response data and - # pass the unpacked structure to the handler - else: - packed_response = resource.data.read() - unpacked_response = umsgpack.unpackb(packed_response) - request_id = unpacked_response[0] - response_data = unpacked_response[1] - self.handle_response(request_id, response_data, resource.total_size, resource.size) + packed_response = resource.data.read() + unpacked_response = umsgpack.unpackb(packed_response) + request_id = unpacked_response[0] + response_data = unpacked_response[1] + self.handle_response(request_id, response_data, resource.total_size, resource.size) else: RNS.log("Incoming response resource failed with status: "+RNS.hexrep([resource.status]), RNS.LOG_DEBUG) for pending_request in self.pending_requests: @@ -1190,7 +1085,8 @@ class Link: def encrypt(self, plaintext): try: if not self.token: - try: self.token = Token(self.derived_key) + try: + self.token = Token(self.derived_key) except Exception as e: RNS.log("Could not instantiate token while performing encryption on link "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR) raise e @@ -1204,7 +1100,9 @@ class Link: def decrypt(self, ciphertext): try: - if not self.token: self.token = Token(self.derived_key) + if not self.token: + self.token = Token(self.derived_key) + return self.token.decrypt(ciphertext) except Exception as e: @@ -1379,7 +1277,6 @@ class RequestReceipt(): self.response = None self.response_transfer_size = None self.response_size = None - self.metadata = None self.status = RequestReceipt.SENT self.sent_at = time.time() self.progress = 0 @@ -1466,11 +1363,10 @@ class RequestReceipt(): resource.cancel() - def response_received(self, response, metadata=None): + def response_received(self, response): if not self.status == RequestReceipt.FAILED: self.progress = 1.0 self.response = response - self.metadata = metadata self.status = RequestReceipt.READY self.response_concluded_at = time.time() diff --git a/RNS/Packet.py b/RNS/Packet.py index 76f5bcc..bcd5e47 100755 --- a/RNS/Packet.py +++ b/RNS/Packet.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, @@ -43,10 +35,10 @@ class Packet: For ``RNS.Destination.GROUP`` destinations, Reticulum will use the pre-shared key configured for the destination. All packets to group - destinations are encrypted with the same AES-256 key. + destinations are encrypted with the same AES-128 key. For ``RNS.Destination.SINGLE`` destinations, Reticulum will use a newly - derived ephemeral AES-256 key for every packet. + derived ephemeral AES-128 key for every packet. For :ref:`RNS.Link` destinations, Reticulum will use per-link ephemeral keys, and offers **Forward Secrecy**. diff --git a/RNS/Resolver.py b/RNS/Resolver.py index fa514a4..fb09bc8 100644 --- a/RNS/Resolver.py +++ b/RNS/Resolver.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Resource.py b/RNS/Resource.py index 689c76c..1fc3237 100644 --- a/RNS/Resource.py +++ b/RNS/Resource.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, @@ -33,7 +25,6 @@ import os import bz2 import math import time -import struct import tempfile import threading from threading import Lock @@ -108,20 +99,22 @@ class Resource: # it is to be handled within reasonable # time constraint, even on small systems. # + # A small system in this regard is + # defined as a Raspberry Pi, which should + # be able to compress, encrypt and hash-map + # the resource in about 10 seconds. + # # This constant will be used when determining # how to sequence the sending of large resources. # # Capped at 16777215 (0xFFFFFF) per segment to # fit in 3 bytes in resource advertisements. - MAX_EFFICIENT_SIZE = 1 * 1024 * 1024 - 1 + MAX_EFFICIENT_SIZE = 16 * 1024 * 1024 - 1 RESPONSE_MAX_GRACE_TIME = 10 - - # Max metadata size is 16777215 (0xFFFFFF) bytes - METADATA_MAX_SIZE = 16 * 1024 * 1024 - 1 # The maximum size to auto-compress with # bz2 before sending. - AUTO_COMPRESS_MAX_SIZE = 64 * 1024 * 1024 + AUTO_COMPRESS_MAX_SIZE = MAX_EFFICIENT_SIZE PART_TIMEOUT_FACTOR = 4 PART_TIMEOUT_FACTOR_AFTER_RTT = 2 @@ -195,15 +188,12 @@ class Resource: resource.started_transferring = resource.last_activity resource.storagepath = RNS.Reticulum.resourcepath+"/"+resource.original_hash.hex() - resource.meta_storagepath = resource.storagepath+".meta" resource.segment_index = adv.i resource.total_segments = adv.l - - if adv.l > 1: resource.split = True - else: resource.split = False - - if adv.x: resource.has_metadata = True - else: resource.has_metadata = False + if adv.l > 1: + resource.split = True + else: + resource.split = False resource.hashmap = [None] * resource.total_parts resource.hashmap_height = 0 @@ -229,7 +219,9 @@ class Resource: RNS.log("Error while executing resource started callback from "+str(resource)+". The contained exception was: "+str(e), RNS.LOG_ERROR) resource.hashmap_update(0, resource.hashmap_raw) + resource.watchdog_job() + return resource else: @@ -243,33 +235,15 @@ class Resource: # Create a resource for transmission to a remote destination # The data passed can be either a bytes-array or a file opened # in binary read mode. - def __init__(self, data, link, metadata=None, advertise=True, auto_compress=True, callback=None, progress_callback=None, - timeout = None, segment_index = 1, original_hash = None, request_id = None, is_response = False, sent_metadata_size=0): - + def __init__(self, data, link, advertise=True, auto_compress=True, callback=None, progress_callback=None, timeout = None, segment_index = 1, original_hash = None, request_id = None, is_response = False): data_size = None resource_data = None self.assembly_lock = False self.preparing_next_segment = False self.next_segment = None - self.metadata = None - self.has_metadata = False - self.metadata_size = sent_metadata_size - - if metadata != None: - packed_metadata = umsgpack.packb(metadata) - metadata_size = len(packed_metadata) - if metadata_size > Resource.METADATA_MAX_SIZE: - raise SystemError("Resource metadata size exceeded") - else: - self.metadata = struct.pack(">I", metadata_size)[1:] + packed_metadata - self.metadata_size = len(self.metadata) - self.has_metadata = True - else: - self.metadata = b"" - if sent_metadata_size > 0: self.has_metadata = True if data != None: - if not hasattr(data, "read") and self.metadata_size + len(data) > Resource.MAX_EFFICIENT_SIZE: + if not hasattr(data, "read") and len(data) > Resource.MAX_EFFICIENT_SIZE: original_data = data data_size = len(original_data) data = tempfile.TemporaryFile() @@ -277,43 +251,31 @@ class Resource: del original_data if hasattr(data, "read"): - if data_size == None: data_size = os.stat(data.name).st_size - self.total_size = data_size + self.metadata_size + if data_size == None: + data_size = os.stat(data.name).st_size - if self.total_size <= Resource.MAX_EFFICIENT_SIZE: + self.total_size = data_size + + if data_size <= Resource.MAX_EFFICIENT_SIZE: self.total_segments = 1 self.segment_index = 1 self.split = False resource_data = data.read() data.close() - else: - # self.total_segments = ((data_size-1)//Resource.MAX_EFFICIENT_SIZE)+1 - # self.segment_index = segment_index - # self.split = True - # seek_index = segment_index-1 - # seek_position = seek_index*Resource.MAX_EFFICIENT_SIZE - - self.total_segments = ((self.total_size-1)//Resource.MAX_EFFICIENT_SIZE)+1 + self.total_segments = ((data_size-1)//Resource.MAX_EFFICIENT_SIZE)+1 self.segment_index = segment_index self.split = True seek_index = segment_index-1 - first_read_size = Resource.MAX_EFFICIENT_SIZE - self.metadata_size - - if segment_index == 1: - seek_position = 0 - segment_read_size = first_read_size - else: - seek_position = first_read_size + ((seek_index-1)*Resource.MAX_EFFICIENT_SIZE) - segment_read_size = Resource.MAX_EFFICIENT_SIZE + seek_position = seek_index*Resource.MAX_EFFICIENT_SIZE data.seek(seek_position) - resource_data = data.read(segment_read_size) + resource_data = data.read(Resource.MAX_EFFICIENT_SIZE) self.input_file = data elif isinstance(data, bytes): data_size = len(data) - self.total_size = data_size + self.metadata_size + self.total_size = data_size resource_data = data self.total_segments = 1 @@ -326,9 +288,7 @@ class Resource: else: raise TypeError("Invalid data instance type passed to resource initialisation") - if resource_data: - if self.has_metadata: data = self.metadata + resource_data - else: data = resource_data + data = resource_data self.status = Resource.NONE self.link = link @@ -359,16 +319,7 @@ class Resource: self.request_id = request_id self.started_transferring = None self.is_response = is_response - self.auto_compress_limit = Resource.AUTO_COMPRESS_MAX_SIZE - self.auto_compress_option = auto_compress - - if type(auto_compress) == bool: - self.auto_compress = auto_compress - elif type(auto_compress) == int: - self.auto_compress = True - self.auto_compress_limit = auto_compress - else: - raise TypeError(f"Invalid type {type(auto_compress)} for auto_compress option") + self.auto_compress = auto_compress self.req_hashlist = [] self.receiver_min_consecutive_height = 0 @@ -384,7 +335,7 @@ class Resource: self.uncompressed_data = data compression_began = time.time() - if self.auto_compress and data_size <= self.auto_compress_limit: + if (auto_compress and len(self.uncompressed_data) <= Resource.AUTO_COMPRESS_MAX_SIZE): RNS.log("Compressing resource data...", RNS.LOG_EXTREME) self.compressed_data = bz2.compress(self.uncompressed_data) RNS.log("Compression completed in "+str(round(time.time()-compression_began, 3))+" seconds", RNS.LOG_EXTREME) @@ -403,20 +354,19 @@ class Resource: self.data += self.compressed_data self.compressed = True + self.uncompressed_data = None else: self.data = b"" self.data += RNS.Identity.get_random_hash()[:Resource.RANDOM_HASH_SIZE] self.data += self.uncompressed_data + self.uncompressed_data = self.data self.compressed = False self.compressed_data = None - if self.auto_compress and data_size <= self.auto_compress_limit: + if auto_compress: RNS.log("Compression did not decrease size, sending uncompressed", RNS.LOG_EXTREME) - self.compressed_data = None - self.uncompressed_data = None - # Resources handle encryption directly to # make optimal use of packet MTU on an entire # encrypted stream. The Resource instance will @@ -469,8 +419,7 @@ class Resource: self.parts.append(part) RNS.log("Hashmap computation concluded in "+str(round(time.time()-hashmap_computation_began, 3))+" seconds", RNS.LOG_EXTREME) - - self.data = None + if advertise: self.advertise() else: @@ -555,7 +504,8 @@ class Resource: if self.link: self.link.expected_rate = self.eifr def watchdog_job(self): - thread = threading.Thread(target=self.__watchdog_job, daemon=True) + thread = threading.Thread(target=self.__watchdog_job) + thread.daemon = True thread.start() def __watchdog_job(self): @@ -601,7 +551,6 @@ class Resource: else: sleep_time = self.last_activity + self.part_timeout_factor*((3*self.sdu)/self.eifr) + Resource.RETRY_GRACE_TIME + extra_wait - time.time() - # TODO: Remove debug at some point # RNS.log(f"EIFR {RNS.prettyspeed(self.eifr)}, ETOF {RNS.prettyshorttime(expected_tof_remaining)} ", RNS.LOG_DEBUG, pt=True) # RNS.log(f"Resource ST {RNS.prettyshorttime(sleep_time)}, RTT {RNS.prettyshorttime(self.rtt or self.link.rtt)}, {self.outstanding_parts} left", RNS.LOG_DEBUG, pt=True) @@ -671,37 +620,29 @@ class Resource: self.status = Resource.ASSEMBLING stream = b"".join(self.parts) - if self.encrypted: data = self.link.decrypt(stream) - else: data = stream + if self.encrypted: + data = self.link.decrypt(stream) + else: + data = stream # Strip off random hash data = data[Resource.RANDOM_HASH_SIZE:] - if self.compressed: self.data = bz2.decompress(data) - else: self.data = data + if self.compressed: + self.data = bz2.decompress(data) + else: + self.data = data calculated_hash = RNS.Identity.full_hash(self.data+self.random_hash) - if calculated_hash == self.hash: - if self.has_metadata and self.segment_index == 1: - # TODO: Add early metadata_ready callback - metadata_size = self.data[0] << 16 | self.data[1] << 8 | self.data[2] - packed_metadata = self.data[3:3+metadata_size] - metadata_file = open(self.meta_storagepath, "wb") - metadata_file.write(packed_metadata) - metadata_file.close() - del packed_metadata - data = self.data[3+metadata_size:] - else: - data = self.data + if calculated_hash == self.hash: self.file = open(self.storagepath, "ab") - self.file.write(data) + self.file.write(self.data) self.file.close() self.status = Resource.COMPLETE - del data self.prove() - - else: self.status = Resource.CORRUPT + else: + self.status = Resource.CORRUPT except Exception as e: @@ -713,27 +654,21 @@ class Resource: if self.segment_index == self.total_segments: if self.callback != None: - if not os.path.isfile(self.meta_storagepath): - self.metadata = None - else: - metadata_file = open(self.meta_storagepath, "rb") - self.metadata = umsgpack.unpackb(metadata_file.read()) - metadata_file.close() - try: os.unlink(self.meta_storagepath) - except Exception as e: - RNS.log(f"Error while cleaning up resource metadata file, the contained exception was: {e}", RNS.LOG_ERROR) - self.data = open(self.storagepath, "rb") - try: self.callback(self) + try: + self.callback(self) except Exception as e: RNS.log("Error while executing resource assembled callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR) try: - if hasattr(self.data, "close") and callable(self.data.close): self.data.close() - if os.path.isfile(self.storagepath): os.unlink(self.storagepath) + if hasattr(self.data, "close") and callable(self.data.close): + self.data.close() + + os.unlink(self.storagepath) except Exception as e: - RNS.log(f"Error while cleaning up resource files, the contained exception was: {e}", RNS.LOG_ERROR) + RNS.log("Error while cleaning up resource files, the contained exception was:", RNS.LOG_ERROR) + RNS.log(str(e)) else: RNS.log("Resource segment "+str(self.segment_index)+" of "+str(self.total_segments)+" received, waiting for next segment to be announced", RNS.LOG_DEBUG) @@ -764,8 +699,7 @@ class Resource: request_id = self.request_id, is_response = self.is_response, advertise = False, - auto_compress = self.auto_compress_option, - sent_metadata_size = self.metadata_size, + auto_compress = self.auto_compress, ) def validate_proof(self, proof_data): @@ -778,18 +712,18 @@ class Resource: # If all segments were processed, we'll # signal that the resource sending concluded if self.callback != None: - try: self.callback(self) - except Exception as e: RNS.log("Error while executing resource concluded callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR) + try: + self.callback(self) + except Exception as e: + RNS.log("Error while executing resource concluded callback from "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR) finally: try: if hasattr(self, "input_file"): - if hasattr(self.input_file, "close") and callable(self.input_file.close): self.input_file.close() - except Exception as e: RNS.log("Error while closing resource input file: "+str(e), RNS.LOG_ERROR) - else: - try: - if hasattr(self, "input_file"): - if hasattr(self.input_file, "close") and callable(self.input_file.close): self.input_file.close() - except Exception as e: RNS.log("Error while closing resource input file: "+str(e), RNS.LOG_ERROR) + if hasattr(self.input_file, "close") and callable(self.input_file.close): + self.input_file.close() + + except Exception as e: + RNS.log("Error while closing resource input file: "+str(e), RNS.LOG_ERROR) else: # Otherwise we'll recursively create the # next segment of the resource @@ -797,16 +731,8 @@ class Resource: RNS.log(f"Next segment preparation for resource {self} was not started yet, manually preparing now. This will cause transfer slowdown.", RNS.LOG_WARNING) self.__prepare_next_segment() - while self.next_segment == None: time.sleep(0.05) - - self.data = None - self.metadata = None - self.parts = None - self.input_file = None - self.link = None - self.req_hashlist = None - self.hashmap = None - + while self.next_segment == None: + time.sleep(0.05) self.next_segment.advertise() else: pass @@ -1268,7 +1194,6 @@ class ResourceAdvertisement: self.c = resource.compressed # Compression flag self.e = resource.encrypted # Encryption flag self.s = resource.split # Split flag - self.x = resource.has_metadata # Metadata flag self.i = resource.segment_index # Segment index self.l = resource.total_segments # Total segments self.q = resource.request_id # ID of associated request @@ -1284,7 +1209,7 @@ class ResourceAdvertisement: self.p = True # Flags - self.f = 0x00 | self.x << 5 | self.p << 4 | self.u << 3 | self.s << 2 | self.c << 1 | self.e + self.f = 0x00 | self.p << 4 | self.u << 3 | self.s << 2 | self.c << 1 | self.e def get_transfer_size(self): return self.t @@ -1304,9 +1229,6 @@ class ResourceAdvertisement: def is_compressed(self): return self.c - def has_metadata(self): - return self.x - def get_link(self): return self.link @@ -1356,6 +1278,5 @@ class ResourceAdvertisement: adv.s = True if ((adv.f >> 2) & 0x01) == 0x01 else False adv.u = True if ((adv.f >> 3) & 0x01) == 0x01 else False adv.p = True if ((adv.f >> 4) & 0x01) == 0x01 else False - adv.x = True if ((adv.f >> 5) & 0x01) == 0x01 else False return adv \ No newline at end of file diff --git a/RNS/Reticulum.py b/RNS/Reticulum.py index 23d88f6..39c5fa9 100755 --- a/RNS/Reticulum.py +++ b/RNS/Reticulum.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, @@ -211,8 +203,7 @@ class Reticulum: """ return Reticulum.__instance - def __init__(self,configdir=None, loglevel=None, logdest=None, verbosity=None, - require_shared_instance=False, shared_instance_type=None): + def __init__(self,configdir=None, loglevel=None, logdest=None, verbosity=None, require_shared_instance=False): """ Initialises and starts a Reticulum instance. This must be done before any other operations, and Reticulum will not @@ -264,11 +255,9 @@ class Reticulum: self.local_control_port = 37429 self.local_socket_path = None self.share_instance = True - self.shared_instance_type = shared_instance_type self.rpc_listener = None self.rpc_key = None self.rpc_type = "AF_INET" - self.use_af_unix = False self.ifac_salt = Reticulum.IFAC_SALT @@ -325,11 +314,12 @@ class Reticulum: self.__apply_config() RNS.log(f"Utilising cryptography backend \"{RNS.Cryptography.Provider.backend()}\"", RNS.LOG_DEBUG) RNS.log(f"Configuration loaded from {self.configpath}", RNS.LOG_VERBOSE) - + RNS.Identity.load_known_destinations() + RNS.Transport.start(self) - if self.use_af_unix: + if RNS.vendor.platformutils.use_af_unix(): self.rpc_addr = f"\0rns/{self.local_socket_path}/rpc" self.rpc_type = "AF_UNIX" else: @@ -457,11 +447,7 @@ class Reticulum: if option == "instance_name": value = self.config["reticulum"][option] self.local_socket_path = value - if option == "shared_instance_type": - if self.shared_instance_type == None: - value = self.config["reticulum"][option].lower() - if value in ["tcp", "unix"]: - self.shared_instance_type = value + else: self.local_socket_path = "default" if option == "shared_instance_port": value = int(self.config["reticulum"][option]) self.local_interface_port = value @@ -520,17 +506,6 @@ class Reticulum: if RNS.compiled: RNS.log("Reticulum running in compiled mode", RNS.LOG_DEBUG) else: RNS.log("Reticulum running in interpreted mode", RNS.LOG_DEBUG) - - if RNS.vendor.platformutils.use_af_unix(): - if self.shared_instance_type == "tcp": self.use_af_unix = False - else: self.use_af_unix = True - else: - self.shared_instance_type = "tcp" - self.use_af_unix = False - - if self.local_socket_path == None and self.use_af_unix: - self.local_socket_path = "default" - self.__start_local_interface() if self.is_shared_instance or self.is_standalone_instance: @@ -1385,24 +1360,12 @@ share_instance = Yes # If you want to run multiple *different* shared instances # on the same system, you will need to specify different -# instance names for each. On platforms supporting domain -# sockets, this can be done with the instance_name option: +# shared instance ports for each. The defaults are given +# below, and again, these options can be left out if you +# don't need them. -instance_name = default - -# Some platforms don't support domain sockets, and if that -# is the case, you can isolate different instances by -# specifying a unique set of ports for each: - -# shared_instance_port = 37428 -# instance_control_port = 37429 - - -# If you want to explicitly use TCP for shared instance -# communication, instead of domain sockets, this is also -# possible, by using the following option: - -# shared_instance_type = tcp +shared_instance_port = 37428 +instance_control_port = 37429 # You can configure Reticulum to panic and forcibly close @@ -1411,7 +1374,7 @@ instance_name = default # an optional directive, and can be left out for brevity. # This behaviour is disabled by default. -# panic_on_interface_error = No +panic_on_interface_error = No [logging] diff --git a/RNS/Transport.py b/RNS/Transport.py index 7166050..244932f 100755 --- a/RNS/Transport.py +++ b/RNS/Transport.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, @@ -1070,7 +1062,7 @@ class Transport: if should_transmit: if not stored_hash: - Transport.add_packet_hash(packet.packet_hash) + Transport.packet_hashlist.add(packet.packet_hash) stored_hash = True Transport.transmit(interface, packet.raw) @@ -1082,16 +1074,11 @@ class Transport: Transport.jobs_locked = False return sent - @staticmethod - def add_packet_hash(packet_hash): - if not Transport.owner.is_connected_to_shared_instance: - Transport.packet_hashlist.add(packet_hash) - @staticmethod def packet_filter(packet): - # If connected to a shared instance, it will handle - # packet filtering - if Transport.owner.is_connected_to_shared_instance: return True + # TODO: Think long and hard about this. + # Is it even strictly necessary with the current + # transport rules? # Filter packets intended for other transport instances if packet.transport_id != None and packet.packet_type != RNS.Packet.ANNOUNCE: @@ -1279,7 +1266,7 @@ class Transport: remember_packet_hash = False if remember_packet_hash: - Transport.add_packet_hash(packet.packet_hash) + Transport.packet_hashlist.add(packet.packet_hash) # TODO: Enable when caching has been redesigned # Transport.cache(packet) @@ -1366,7 +1353,6 @@ class Transport: proof_timeout += now + RNS.Link.ESTABLISHMENT_TIMEOUT_PER_HOP * max(1, remaining_hops) path_mtu = RNS.Link.mtu_from_lr_packet(packet) - mode = RNS.Link.mode_from_lr_packet(packet) nh_mtu = outbound_interface.HW_MTU if path_mtu: if outbound_interface.HW_MTU == None: @@ -1380,7 +1366,7 @@ class Transport: else: if nh_mtu < path_mtu: path_mtu = nh_mtu - clamped_mtu = RNS.Link.signalling_bytes(path_mtu, mode) + clamped_mtu = RNS.Link.mtu_bytes(path_mtu) RNS.log(f"Clamping link MTU to {RNS.prettysize(nh_mtu)}", RNS.LOG_DEBUG) # TODO: Remove debug new_raw = new_raw[:-RNS.Link.LINK_MTU_SIZE]+clamped_mtu @@ -1445,7 +1431,7 @@ class Transport: # Add this packet to the filter hashlist if we # have determined that it's actually our turn # to process it. - Transport.add_packet_hash(packet.packet_hash) + Transport.packet_hashlist.add(packet.packet_hash) new_raw = packet.raw[0:1] new_raw += struct.pack("!B", packet.hops) @@ -1834,7 +1820,6 @@ class Transport: for destination in Transport.destinations: if destination.hash == packet.destination_hash and destination.type == packet.destination_type: path_mtu = RNS.Link.mtu_from_lr_packet(packet) - mode = RNS.Link.mode_from_lr_packet(packet) if packet.receiving_interface.AUTOCONFIGURE_MTU or packet.receiving_interface.FIXED_MTU: nh_mtu = packet.receiving_interface.HW_MTU else: @@ -1848,7 +1833,7 @@ class Transport: else: if nh_mtu < path_mtu: path_mtu = nh_mtu - clamped_mtu = RNS.Link.signalling_bytes(path_mtu, mode) + clamped_mtu = RNS.Link.mtu_bytes(path_mtu) RNS.log(f"Clamping link MTU to {RNS.prettysize(nh_mtu)}", RNS.LOG_DEBUG) # TODO: Remove debug packet.data = packet.data[:-RNS.Link.LINK_MTU_SIZE]+clamped_mtu @@ -1886,17 +1871,18 @@ class Transport: for destination in Transport.destinations: if destination.hash == packet.destination_hash and destination.type == packet.destination_type: packet.destination = destination - if destination.receive(packet): - if destination.proof_strategy == RNS.Destination.PROVE_ALL: - packet.prove() + destination.receive(packet) - elif destination.proof_strategy == RNS.Destination.PROVE_APP: - if destination.callbacks.proof_requested: - try: - if destination.callbacks.proof_requested(packet): - packet.prove() - except Exception as e: - RNS.log("Error while executing proof request callback. The contained exception was: "+str(e), RNS.LOG_ERROR) + if destination.proof_strategy == RNS.Destination.PROVE_ALL: + packet.prove() + + elif destination.proof_strategy == RNS.Destination.PROVE_APP: + if destination.callbacks.proof_requested: + try: + if destination.callbacks.proof_requested(packet): + packet.prove() + except Exception as e: + RNS.log("Error while executing proof request callback. The contained exception was: "+str(e), RNS.LOG_ERROR) # Handling for proofs and link-request proofs elif packet.packet_type == RNS.Packet.PROOF: @@ -1909,15 +1895,15 @@ class Transport: if packet.receiving_interface == link_entry[IDX_LT_NH_IF]: try: if len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2 or len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE: - signalling_bytes = b"" + mtu_bytes = b"" if len(packet.data) == RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2+RNS.Link.LINK_MTU_SIZE: - signalling_bytes = RNS.Link.signalling_bytes(RNS.Link.mtu_from_lp_packet(packet), RNS.Link.mode_from_lp_packet(packet)) + mtu_bytes = RNS.Link.mtu_bytes(RNS.Link.mtu_from_lp_packet(packet)) peer_pub_bytes = packet.data[RNS.Identity.SIGLENGTH//8:RNS.Identity.SIGLENGTH//8+RNS.Link.ECPUBSIZE//2] peer_identity = RNS.Identity.recall(link_entry[IDX_LT_DSTHASH]) peer_sig_pub_bytes = peer_identity.get_public_key()[RNS.Link.ECPUBSIZE//2:RNS.Link.ECPUBSIZE] - signed_data = packet.destination_hash+peer_pub_bytes+peer_sig_pub_bytes+signalling_bytes + signed_data = packet.destination_hash+peer_pub_bytes+peer_sig_pub_bytes+mtu_bytes signature = packet.data[:RNS.Identity.SIGLENGTH//8] if peer_identity.validate(signature, signed_data): @@ -1959,7 +1945,7 @@ class Transport: # Add this packet to the filter hashlist if we # have determined that it's actually destined # for this system, and then validate the proof - Transport.add_packet_hash(packet.packet_hash) + Transport.packet_hashlist.add(packet.packet_hash) link.validate_proof(packet) elif packet.context == RNS.Packet.RESOURCE_PRF: @@ -2777,6 +2763,7 @@ class Transport: Transport.reverse_table = {} Transport.link_table = {} Transport.held_announces = {} + Transport.announce_handlers = [] Transport.tunnels = {} @staticmethod diff --git a/RNS/Utilities/__init__.py b/RNS/Utilities/__init__.py index a9a74ff..299b693 100644 --- a/RNS/Utilities/__init__.py +++ b/RNS/Utilities/__init__.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2022 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Utilities/rncp.py b/RNS/Utilities/rncp.py index b1311a5..9fd6734 100644 --- a/RNS/Utilities/rncp.py +++ b/RNS/Utilities/rncp.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -11,16 +11,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, @@ -33,7 +25,6 @@ import RNS import argparse import threading -import shutil import time import sys import os @@ -43,7 +34,6 @@ from RNS._version import __version__ APP_NAME = "rncp" allow_all = False allow_fetch = False -allow_overwrite_on_receive = False fetch_auto_compress = True fetch_jail = None save_path = None @@ -57,14 +47,13 @@ erase_str = "\33[2K\r" def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identity = False, limit = None, disable_auth = None, fetch_allowed = False, no_compress=False, - jail = None, save = None, announce = False, allow_overwrite=False): + jail = None, save = None, announce = False): - global allow_all, allow_fetch, allowed_identity_hashes, fetch_jail, save_path - global fetch_auto_compress, allow_overwrite_on_receive + global allow_all, allow_fetch, allowed_identity_hashes, fetch_jail, save_path, fetch_auto_compress + from tempfile import TemporaryFile allow_fetch = fetch_allowed fetch_auto_compress = not no_compress - allow_overwrite_on_receive = allow_overwrite identity = None if announce < 0: announce = False @@ -186,15 +175,22 @@ def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identi if target_link != None: RNS.log("Sending file "+str(file_path)+" to client", RNS.LOG_VERBOSE) - try: - metadata = {"name": os.path.basename(file_path).encode("utf-8") } - fetch_resource = RNS.Resource(open(file_path, "rb"), target_link, metadata=metadata, auto_compress=fetch_auto_compress) - return True + temp_file = TemporaryFile() + real_file = open(file_path, "rb") + filename_bytes = os.path.basename(file_path).encode("utf-8") + filename_len = len(filename_bytes) - except Exception as e: - RNS.log(f"Could not send file to client. The contained exception was: {e}", RNS.LOG_ERROR) - return False + if filename_len > 0xFFFF: + print("Filename exceeds max size, cannot send") + RNS.exit(1) + temp_file.write(filename_len.to_bytes(2, "big")) + temp_file.write(filename_bytes) + temp_file.write(real_file.read()) + temp_file.seek(0) + + fetch_resource = RNS.Resource(temp_file, target_link, auto_compress=fetch_auto_compress) + return True else: return None @@ -219,7 +215,8 @@ def listen(configdir, verbosity = 0, quietness = 0, allowed = [], display_identi threading.Thread(target=job, daemon=True).start() - while True: time.sleep(1) + while True: + time.sleep(1) def client_link_established(link): RNS.log("Incoming link established", RNS.LOG_VERBOSE) @@ -264,42 +261,34 @@ def receive_resource_started(resource): print("Starting resource transfer "+RNS.prettyhexrep(resource.hash)+id_str) def receive_resource_concluded(resource): - global save_path, allow_overwrite_on_receive + global save_path if resource.status == RNS.Resource.COMPLETE: print(str(resource)+" completed") - if resource.metadata == None: - print("Invalid data received, ignoring resource") - return + if resource.total_size > 4: + filename_len = int.from_bytes(resource.data.read(2), "big") + filename = resource.data.read(filename_len).decode("utf-8") + + counter = 0 + if save_path: + saved_filename = os.path.abspath(os.path.expanduser(save_path+"/"+filename)) + if not saved_filename.startswith(save_path+"/"): + RNS.log(f"Invalid save path {saved_filename}, ignoring", RNS.LOG_ERROR) + return + else: + saved_filename = filename + + full_save_path = saved_filename + while os.path.isfile(full_save_path): + counter += 1 + full_save_path = saved_filename+"."+str(counter) + + file = open(full_save_path, "wb") + file.write(resource.data.read()) + file.close() else: - try: - filename = os.path.basename(resource.metadata["name"].decode("utf-8")) - counter = 0 - if save_path: - saved_filename = os.path.abspath(os.path.expanduser(save_path+"/"+filename)) - if not saved_filename.startswith(save_path+"/"): - RNS.log(f"Invalid save path {saved_filename}, ignoring", RNS.LOG_ERROR) - return - else: - saved_filename = filename - - full_save_path = saved_filename - if allow_overwrite_on_receive: - if os.path.isfile(full_save_path): - try: os.unlink(full_save_path) - except Exception as e: - RNS.log(f"Could not overwrite existing file {full_save_path}, renaming instead", RNS.LOG_ERROR) - - while os.path.isfile(full_save_path): - counter += 1 - full_save_path = saved_filename+"."+str(counter) - - shutil.move(resource.data.name, full_save_path) - - except Exception as e: - RNS.log(f"An error occurred while saving received resource: {e}", RNS.LOG_ERROR) - return + print("Invalid data received, ignoring resource") else: print("Resource failed") @@ -345,11 +334,10 @@ def sender_progress(resource): resource_done = True link = None -def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, save=None, allow_overwrite=False): - global current_resource, resource_done, link, speed, show_phy_rates, save_path, allow_overwrite_on_receive +def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, save=None): + global current_resource, resource_done, link, speed, show_phy_rates, save_path targetloglevel = 3+verbosity-quietness show_phy_rates = phy_rates - allow_overwrite_on_receive = allow_overwrite if save: sp = os.path.abspath(os.path.expanduser(save)) @@ -485,40 +473,32 @@ def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = No def fetch_resource_concluded(resource): nonlocal resource_resolved, resource_status - global save_path, allow_overwrite_on_receive + global save_path if resource.status == RNS.Resource.COMPLETE: - if resource.metadata == None: - print("Invalid data received, ignoring resource") - return + if resource.total_size > 4: + filename_len = int.from_bytes(resource.data.read(2), "big") + filename = resource.data.read(filename_len).decode("utf-8") + + counter = 0 + if save_path: + saved_filename = os.path.abspath(os.path.expanduser(save_path+"/"+filename)) + + else: + saved_filename = filename + + full_save_path = saved_filename + while os.path.isfile(full_save_path): + counter += 1 + full_save_path = saved_filename+"."+str(counter) + + file = open(full_save_path, "wb") + file.write(resource.data.read()) + file.close() + resource_status = "completed" else: - try: - filename = os.path.basename(resource.metadata["name"].decode("utf-8")) - counter = 0 - if save_path: - saved_filename = os.path.abspath(os.path.expanduser(save_path+"/"+filename)) - if not saved_filename.startswith(save_path+"/"): - print(f"Invalid save path {saved_filename}, ignoring") - return - else: - saved_filename = filename - - full_save_path = saved_filename - if allow_overwrite_on_receive: - if os.path.isfile(full_save_path): - try: os.unlink(full_save_path) - except Exception as e: - print(f"Could not overwrite existing file {full_save_path}, renaming instead") - - while os.path.isfile(full_save_path): - counter += 1 - full_save_path = saved_filename+"."+str(counter) - - shutil.move(resource.data.name, full_save_path) - - except Exception as e: - print(f"An error occurred while saving received resource: {e}") - return + print("Invalid data received, ignoring resource") + resource_status = "invalid_data" else: print("Resource failed") @@ -616,6 +596,7 @@ def fetch(configdir, verbosity = 0, quietness = 0, destination = None, file = No def send(configdir, verbosity = 0, quietness = 0, destination = None, file = None, timeout = RNS.Transport.PATH_REQUEST_TIMEOUT, silent=False, phy_rates=False, no_compress=False): global current_resource, resource_done, link, speed, show_phy_rates, phy_got_total, phy_speed + from tempfile import TemporaryFile targetloglevel = 3+verbosity-quietness show_phy_rates = phy_rates @@ -637,7 +618,21 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non print("File not found") sys.exit(1) - metadata = {"name": os.path.basename(file_path).encode("utf-8") } + temp_file = TemporaryFile() + real_file = open(file_path, "rb") + filename_bytes = os.path.basename(file_path).encode("utf-8") + filename_len = len(filename_bytes) + + if filename_len > 0xFFFF: + print("Filename exceeds max size, cannot send") + RNS.exit(1) + else: + print("Preparing file...", end=es) + + temp_file.write(filename_len.to_bytes(2, "big")) + temp_file.write(filename_bytes) + temp_file.write(real_file.read()) + temp_file.seek(0) print(f"{erase_str}", end="") @@ -724,12 +719,9 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non link.identify(identity) auto_compress = True - if no_compress: auto_compress = False - try: resource = RNS.Resource(open(file_path, "rb"), link, metadata=metadata, callback = sender_progress, progress_callback = sender_progress, auto_compress = auto_compress) - except Exception as e: - print(f"Could not start transfer: {e}") - RNS.exit(1) - + if no_compress: + auto_compress = False + resource = RNS.Resource(temp_file, link, callback = sender_progress, progress_callback = sender_progress, auto_compress = auto_compress) current_resource = resource while resource.status < RNS.Resource.TRANSFERRING: @@ -800,6 +792,8 @@ def send(configdir, verbosity = 0, quietness = 0, destination = None, file = Non print("\n"+str(file_path)+" copied to "+RNS.prettyhexrep(destination_hash)) link.teardown() time.sleep(0.25) + real_file.close() + temp_file.close() RNS.exit(0) def main(): @@ -817,7 +811,6 @@ def main(): parser.add_argument("-f", '--fetch', action='store_true', default=False, help="fetch file from remote listener instead of sending") parser.add_argument("-j", "--jail", metavar="path", action="store", default=None, help="restrict fetch requests to specified path", type=str) parser.add_argument("-s", "--save", metavar="path", action="store", default=None, help="save received files in specified path", type=str) - parser.add_argument('-O', '--overwrite', action='store_true', default=False, help="Allow overwriting received files, instead of adding postfix") parser.add_argument("-b", action='store', metavar="seconds", default=-1, help="announce interval, 0 to only announce at startup", type=int) parser.add_argument('-a', metavar="allowed_hash", dest="allowed", action='append', help="allow this identity (or add in ~/.rncp/allowed_identities)", type=str) parser.add_argument('-n', '--no-auth', action='store_true', default=False, help="accept requests from anyone") @@ -843,7 +836,6 @@ def main(): # limit=args.limit, disable_auth=args.no_auth, announce=args.b, - allow_overwrite=args.overwrite, ) elif args.fetch: @@ -858,7 +850,6 @@ def main(): silent = args.silent, phy_rates = args.phy_rates, save = args.save, - allow_overwrite=args.overwrite, ) else: print("") diff --git a/RNS/Utilities/rnid.py b/RNS/Utilities/rnid.py index 68f5fab..be7e908 100644 --- a/RNS/Utilities/rnid.py +++ b/RNS/Utilities/rnid.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2023-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -11,16 +11,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Utilities/rnir.py b/RNS/Utilities/rnir.py index 7f37576..f5d2388 100644 --- a/RNS/Utilities/rnir.py +++ b/RNS/Utilities/rnir.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2023-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -11,16 +11,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Utilities/rnodeconf.py b/RNS/Utilities/rnodeconf.py index 7d48499..10443a3 100755 --- a/RNS/Utilities/rnodeconf.py +++ b/RNS/Utilities/rnodeconf.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2018-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -11,16 +11,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Utilities/rnpath.py b/RNS/Utilities/rnpath.py index c7a56c6..95e49eb 100644 --- a/RNS/Utilities/rnpath.py +++ b/RNS/Utilities/rnpath.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -11,16 +11,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Utilities/rnprobe.py b/RNS/Utilities/rnprobe.py index aecff37..392f664 100644 --- a/RNS/Utilities/rnprobe.py +++ b/RNS/Utilities/rnprobe.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -11,16 +11,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Utilities/rnsd.py b/RNS/Utilities/rnsd.py index 15ae69c..459c034 100755 --- a/RNS/Utilities/rnsd.py +++ b/RNS/Utilities/rnsd.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -11,16 +11,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, @@ -117,24 +109,12 @@ share_instance = Yes # If you want to run multiple *different* shared instances # on the same system, you will need to specify different -# instance names for each. On platforms supporting domain -# sockets, this can be done with the instance_name option: +# shared instance ports for each. The defaults are given +# below, and again, these options can be left out if you +# don't need them. -instance_name = default - -# Some platforms don't support domain sockets, and if that -# is the case, you can isolate different instances by -# specifying a unique set of ports for each: - -# shared_instance_port = 37428 -# instance_control_port = 37429 - - -# If you want to explicitly use TCP for shared instance -# communication, instead of domain sockets, this is also -# possible, by using the following option: - -# shared_instance_type = tcp +shared_instance_port = 37428 +instance_control_port = 37429 # On systems where running instances may not have access @@ -166,7 +146,7 @@ instance_name = default # an optional directive, and can be left out for brevity. # This behaviour is disabled by default. -# panic_on_interface_error = No +panic_on_interface_error = No # When Transport is enabled, it is possible to allow the @@ -177,7 +157,7 @@ instance_name = default # Transport Instance, and printed to the log at startup. # Optional, and disabled by default. -# respond_to_probes = No +respond_to_probes = No [logging] diff --git a/RNS/Utilities/rnstatus.py b/RNS/Utilities/rnstatus.py index f23e076..b109695 100644 --- a/RNS/Utilities/rnstatus.py +++ b/RNS/Utilities/rnstatus.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -11,16 +11,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/Utilities/rnx.py b/RNS/Utilities/rnx.py index 6db6f26..ffc9c47 100644 --- a/RNS/Utilities/rnx.py +++ b/RNS/Utilities/rnx.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -11,16 +11,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/__init__.py b/RNS/__init__.py index 9845264..a08ced3 100755 --- a/RNS/__init__.py +++ b/RNS/__init__.py @@ -1,6 +1,6 @@ -# Reticulum License +# MIT License # -# Copyright (c) 2016-2025 Mark Qvist +# Copyright (c) 2016-2025 Mark Qvist / unsigned.io and contributors # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -9,16 +9,8 @@ # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # -# - The Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. +# 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, diff --git a/RNS/_version.py b/RNS/_version.py index f5b7730..e94731c 100644 --- a/RNS/_version.py +++ b/RNS/_version.py @@ -1 +1 @@ -__version__ = "0.9.7" +__version__ = "0.9.4" diff --git a/RNS/vendor/platformutils.py b/RNS/vendor/platformutils.py index 7399acd..6bcbb5a 100644 --- a/RNS/vendor/platformutils.py +++ b/RNS/vendor/platformutils.py @@ -1,33 +1,3 @@ -# Reticulum License -# -# Copyright (c) 2016-2025 Mark Qvist -# -# 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 Software shall not be used in any kind of system which includes amongst -# its functions the ability to purposefully do harm to human beings. -# -# - The Software shall not be used, directly or indirectly, in the creation of -# an artificial intelligence, machine learning or language model training -# dataset, including but not limited to any use that contributes to the -# training or development of such a model or algorithm. -# -# - 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. - def get_platform(): from os import environ if "ANDROID_ARGUMENT" in environ: return "android" diff --git a/Roadmap.md b/Roadmap.md index 5689e30..e00e813 100644 --- a/Roadmap.md +++ b/Roadmap.md @@ -14,6 +14,18 @@ This document outlines the currently established development roadmap for Reticul ## Currently Active Work Areas For each release cycle of Reticulum, improvements and additions from the five [Primary Efforts](#primary-efforts) are selected as active work areas, and can be expected to be included in the upcoming releases within that cycle. While not entirely set in stone for each release cycle, they serve as a pointer of what to expect in the near future. +- The current `0.8.x` release cycle aims at completing + - [ ] Hot-pluggable interface system + - [ ] External interface plugins + - [ ] Network-wide path balancing and multi-pathing + - [ ] Expanded hardware support + - [ ] Overhauling and updating the documentation + - [ ] Distributed Destination Naming System + - [ ] A standalone RNS Daemon app for Android + - [ ] Addding automatic retries to all use cases of the `Request` API + - [ ] Performance and memory optimisations of the Python reference implementation + - [ ] Fixing bugs discovered while operating Reticulum systems and applications + ## Primary Efforts The development path for Reticulum is currently laid out in five distinct areas: *Comprehensibility*, *Universality*, *Functionality*, *Usability & Utility* and *Interfaceability*. Conceptualising the development of Reticulum into these areas serves to advance the implementation and work towards the Foundational Goals & Values of Reticulum. @@ -38,14 +50,17 @@ These efforts are aimed at improving the ease of which Reticulum is understood, ### Universality These efforts seek to broaden the universality of the Reticulum software and hardware ecosystem by continously diversifying platform support, and by improving the overall availability and ease of deployment of the Reticulum stack. +- OpenWRT support - Create a standalone RNS Daemon app for Android - A lightweight and portable C implementation for microcontrollers, µRNS - A portable, high-performance Reticulum implementation in C/C++, see [#21](https://github.com/markqvist/Reticulum/discussions/21) +- Performance and memory optimisations of the Python implementation - Bindings for other programming languages ### Functionality These efforts aim to expand and improve the core functionality and reliability of Reticulum. +- Add support for user-supplied external interface drivers - Add interface hot-plug and live up/down control to running instances - Add automatic retries to all use cases of the `Request` API - Network-wide path balancing @@ -55,11 +70,11 @@ These efforts aim to expand and improve the core functionality and reliability o - [Metric-based path selection and multiple paths](https://github.com/markqvist/Reticulum/discussions/86) ### Usability & Utility -These efforts seek to make Reticulum easier to use and operate, and to expand the utility of the stack on deployed systems. +These effors seek to make Reticulum easier to use and operate, and to expand the utility of the stack on deployed systems. - Easy way to share interface configurations, see [#19](https://github.com/markqvist/Reticulum/discussions/19) -- Transit traffic display in `rnstatus` -- `rnsconfig` utility +- Transit traffic display in rnstatus +- rnsconfig utility ### Interfaceability These efforts aim to expand the types of physical and virtual interfaces that Reticulum can natively use to transport data. diff --git a/docs/Reticulum Manual.epub b/docs/Reticulum Manual.epub index fccd712..55970f8 100644 Binary files a/docs/Reticulum Manual.epub and b/docs/Reticulum Manual.epub differ diff --git a/docs/Reticulum Manual.pdf b/docs/Reticulum Manual.pdf index 7665288..46d290d 100644 Binary files a/docs/Reticulum Manual.pdf and b/docs/Reticulum Manual.pdf differ diff --git a/docs/manual/.buildinfo b/docs/manual/.buildinfo index b3345dc..642799f 100644 --- a/docs/manual/.buildinfo +++ b/docs/manual/.buildinfo @@ -1,4 +1,4 @@ # Sphinx build info version 1 # This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. -config: cce3deb193d6110f18b4ace0b3f4099b +config: b499af51edc22529181ef3b25973fa2b tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/docs/manual/_sources/support.rst.txt b/docs/manual/_sources/support.rst.txt index 5a563b9..3f0af03 100644 --- a/docs/manual/_sources/support.rst.txt +++ b/docs/manual/_sources/support.rst.txt @@ -22,9 +22,6 @@ Donations are gratefully accepted via the following channels: Bitcoin: 3CPmacGm34qYvR6XWLVEJmi2aNe3PZqUuq - Liberapay: - https://liberapay.com/Reticulum/ - Ko-Fi: https://ko-fi.com/markqvist diff --git a/docs/manual/_sources/understanding.rst.txt b/docs/manual/_sources/understanding.rst.txt index 168d4a5..31e32c9 100644 --- a/docs/manual/_sources/understanding.rst.txt +++ b/docs/manual/_sources/understanding.rst.txt @@ -453,7 +453,7 @@ For exchanges of small amounts of information, Reticulum offers the *Packet* API public signing key. * | In case the packet is addressed to a *group* destination type, the packet will be encrypted with the - pre-shared AES-256 key associated with the destination. In case the packet is addressed to a *plain* + pre-shared AES-128 key associated with the destination. In case the packet is addressed to a *plain* destination type, the payload data will not be encrypted. Neither of these two destination types can offer forward secrecy. In general, it is recommended to always use the *single* destination type, unless it is strictly necessary to use one of the others. @@ -858,17 +858,9 @@ of the different interface modes, and how they are configured. Cryptographic Primitives ------------------------ -Reticulum uses a simple suite of efficient, strong and well-tested cryptographic -primitives, with widely available implementations that can be used both on -general-purpose CPUs and on microcontrollers. - -One of the primary considerations for choosing this particular set of primitives is -that they can be implemented *safely* with relatively few pitfalls, on practically -all current computing platforms. - -The primitives listed here **are authoritative**. Anything claiming to be Reticulum, -but not using these exact primitives **is not** Reticulum, and possibly an -intentionally compromised or weakened clone. The utilised primitives are: +Reticulum has been designed to use a simple suite of efficient, strong and modern +cryptographic primitives, with widely available implementations that can be used +both on general-purpose CPUs and on microcontrollers. The necessary primitives are: * Ed25519 for signatures @@ -880,11 +872,11 @@ intentionally compromised or weakened clone. The utilised primitives are: * Ephemeral keys derived from an ECDH key exchange on Curve25519 - * AES-256 in CBC mode with PKCS7 padding + * AES-128 in CBC mode with PKCS7 padding * HMAC using SHA256 for message authentication - * IVs must be generated through ``os.urandom()`` or better + * IVs are generated through os.urandom() * No Fernet version and timestamp metadata fields @@ -892,7 +884,7 @@ intentionally compromised or weakened clone. The utilised primitives are: * SHA-512 -In the default installation configuration, the ``X25519``, ``Ed25519`` and ``AES-256-CBC`` +In the default installation configuration, the ``X25519``, ``Ed25519`` and ``AES-128-CBC`` primitives are provided by `OpenSSL `_ (via the `PyCA/cryptography `_ package). The hashing functions ``SHA-256`` and ``SHA-512`` are provided by the standard Python `hashlib `_. The ``HKDF``, ``HMAC``, diff --git a/docs/manual/_sources/using.rst.txt b/docs/manual/_sources/using.rst.txt index 73aab5d..20260c0 100644 --- a/docs/manual/_sources/using.rst.txt +++ b/docs/manual/_sources/using.rst.txt @@ -69,12 +69,12 @@ configuration file is created. The default configuration looks like this: # If you enable Transport, your system will route traffic # for other peers, pass announces and serve path requests. - # This should be done for systems that are suited to act - # as transport nodes, ie. if they are stationary and + # This should only be done for systems that are suited to + # act as transport nodes, ie. if they are stationary and # always-on. This directive is optional and can be removed # for brevity. - enable_transport = No + enable_transport = False # By default, the first program to launch the Reticulum @@ -91,24 +91,12 @@ configuration file is created. The default configuration looks like this: # If you want to run multiple *different* shared instances # on the same system, you will need to specify different - # instance names for each. On platforms supporting domain - # sockets, this can be done with the instance_name option: + # shared instance ports for each. The defaults are given + # below, and again, these options can be left out if you + # don't need them. - instance_name = default - - # Some platforms don't support domain sockets, and if that - # is the case, you can isolate different instances by - # specifying a unique set of ports for each: - - # shared_instance_port = 37428 - # instance_control_port = 37429 - - - # If you want to explicitly use TCP for shared instance - # communication, instead of domain sockets, this is also - # possible, by using the following option: - - # shared_instance_type = tcp + shared_instance_port = 37428 + instance_control_port = 37429 # On systems where running instances may not have access @@ -122,25 +110,13 @@ configuration file is created. The default configuration looks like this: # rpc_key = e5c032d3ec4e64a6aca9927ba8ab73336780f6d71790 - # It is possible to allow remote management of Reticulum - # systems using the various built-in utilities, such as - # rnstatus and rnpath. You will need to specify one or - # more Reticulum Identity hashes for authenticating the - # queries from client programs. For this purpose, you can - # use existing identity files, or generate new ones with - # the rnid utility. - - # enable_remote_management = yes - # remote_management_allowed = 9fb6d773498fb3feda407ed8ef2c3229, 2d882c5586e548d79b5af27bca1776dc - - # You can configure Reticulum to panic and forcibly close # if an unrecoverable interface error occurs, such as the # hardware device for an interface disappearing. This is # an optional directive, and can be left out for brevity. # This behaviour is disabled by default. - # panic_on_interface_error = No + panic_on_interface_error = No # When Transport is enabled, it is possible to allow the @@ -151,7 +127,7 @@ configuration file is created. The default configuration looks like this: # Transport Instance, and printed to the log at startup. # Optional, and disabled by default. - # respond_to_probes = No + respond_to_probes = No [logging] diff --git a/docs/manual/_sources/whatis.rst.txt b/docs/manual/_sources/whatis.rst.txt index 5297d68..fb67b19 100644 --- a/docs/manual/_sources/whatis.rst.txt +++ b/docs/manual/_sources/whatis.rst.txt @@ -68,7 +68,7 @@ What does Reticulum Offer? * Ephemeral per-packet and link keys and derived from an ECDH key exchange on Curve25519 - * AES-256 in CBC mode with PKCS7 padding + * AES-128 in CBC mode with PKCS7 padding * HMAC using SHA256 for authentication diff --git a/docs/manual/_static/documentation_options.js b/docs/manual/_static/documentation_options.js index 2eef2aa..dd602dc 100644 --- a/docs/manual/_static/documentation_options.js +++ b/docs/manual/_static/documentation_options.js @@ -1,6 +1,6 @@ var DOCUMENTATION_OPTIONS = { URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), - VERSION: '0.9.6 beta', + VERSION: '0.9.4 beta', LANGUAGE: 'en', COLLAPSE_INDEX: false, BUILDER: 'html', diff --git a/docs/manual/examples.html b/docs/manual/examples.html index 5279891..e53658a 100644 --- a/docs/manual/examples.html +++ b/docs/manual/examples.html @@ -6,7 +6,7 @@ - Code Examples - Reticulum Network Stack 0.9.6 beta documentation + Code Examples - Reticulum Network Stack 0.9.4 beta documentation @@ -141,7 +141,7 @@
@@ -167,7 +167,7 @@
- Reticulum Network Stack 0.9.6 beta documentation + Reticulum Network Stack 0.9.4 beta documentation