v1.0.0: Boundary mode with bidirectional LoRa↔TCP transport

Vendor microReticulum library with boundary mode transport fixes:
- Two-whitelist system gates backbone traffic (local addresses +
  mentioned addresses from local devices)
- Allow control_hashes and local destinations through boundary filter
  (fixes backbone→LoRa path discovery)
- Fix get_cached_packet() to call unpack() instead of update_hash()
  (fixes empty destination_hash in path responses)
- LRPROOF Identity::recall null guard
- remaining_hops HEADER_1/BROADCAST fix for final-hop delivery
- PROOF packets excluded from boundary wrapping
- Iterator invalidation fix in transport table cleanup
- is_backbone flag replaces string matching for interface identification

Firmware changes:
- Set is_backbone(true) on backbone TCP interface
- Rename default TcpInterface name to BackboneInterface
- Update comments for dual-use TcpInterface (backbone + local AP)
- Use vendored lib/microReticulum instead of PlatformIO registry
This commit is contained in:
James L
2026-02-23 18:08:29 -05:00
parent e741d1cd64
commit 5ed70dcca9
66 changed files with 18264 additions and 35 deletions

View File

@@ -0,0 +1,89 @@
#pragma once
#include "CBC.h"
#include "../Bytes.h"
#include <AES.h>
namespace RNS { namespace Cryptography {
class AES_128_CBC {
public:
static inline const Bytes encrypt(const Bytes& plaintext, const Bytes& key, const Bytes& iv) {
CBC<AES128> cbc;
cbc.setKey(key.data(), key.size());
cbc.setIV(iv.data(), iv.size());
Bytes ciphertext;
cbc.encrypt(ciphertext.writable(plaintext.size()), plaintext.data(), plaintext.size());
return ciphertext;
}
static inline const Bytes decrypt(const Bytes& ciphertext, const Bytes& key, const Bytes& iv) {
CBC<AES128> cbc;
cbc.setKey(key.data(), key.size());
cbc.setIV(iv.data(), iv.size());
Bytes plaintext;
cbc.decrypt(plaintext.writable(ciphertext.size()), ciphertext.data(), ciphertext.size());
return plaintext;
}
// EXPERIMENTAL - overwrites passed buffer
static inline void inplace_encrypt(Bytes& plaintext, const Bytes& key, const Bytes& iv) {
CBC<AES128> cbc;
cbc.setKey(key.data(), key.size());
cbc.setIV(iv.data(), iv.size());
cbc.encrypt((uint8_t*)plaintext.data(), plaintext.data(), plaintext.size());
}
// EXPERIMENTAL - overwrites passed buffer
static inline void inplace_decrypt(Bytes& ciphertext, const Bytes& key, const Bytes& iv) {
CBC<AES128> cbc;
cbc.setKey(key.data(), key.size());
cbc.setIV(iv.data(), iv.size());
cbc.decrypt((uint8_t*)ciphertext.data(), ciphertext.data(), ciphertext.size());
}
};
class AES_256_CBC {
public:
static inline const Bytes encrypt(const Bytes& plaintext, const Bytes& key, const Bytes& iv) {
CBC<AES256> cbc;
cbc.setKey(key.data(), key.size());
cbc.setIV(iv.data(), iv.size());
Bytes ciphertext;
cbc.encrypt(ciphertext.writable(plaintext.size()), plaintext.data(), plaintext.size());
return ciphertext;
}
static inline const Bytes decrypt(const Bytes& ciphertext, const Bytes& key, const Bytes& iv) {
CBC<AES256> cbc;
cbc.setKey(key.data(), key.size());
cbc.setIV(iv.data(), iv.size());
Bytes plaintext;
cbc.decrypt(plaintext.writable(ciphertext.size()), ciphertext.data(), ciphertext.size());
return plaintext;
}
// EXPERIMENTAL - overwrites passed buffer
static inline void inplace_encrypt(Bytes& plaintext, const Bytes& key, const Bytes& iv) {
CBC<AES256> cbc;
cbc.setKey(key.data(), key.size());
cbc.setIV(iv.data(), iv.size());
cbc.encrypt((uint8_t*)plaintext.data(), plaintext.data(), plaintext.size());
}
// EXPERIMENTAL - overwrites passed buffer
static inline void inplace_decrypt(Bytes& ciphertext, const Bytes& key, const Bytes& iv) {
CBC<AES256> cbc;
cbc.setKey(key.data(), key.size());
cbc.setIV(iv.data(), iv.size());
cbc.decrypt((uint8_t*)ciphertext.data(), ciphertext.data(), ciphertext.size());
}
};
} }

View File

@@ -0,0 +1,171 @@
/*
* Copyright (C) 2015 Southern Storm Software, Pty Ltd.
*
* 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.
*/
#include "CBC.h"
#include <Crypto.h>
#include <string.h>
/**
* \class CBCCommon CBC.h <CBC.h>
* \brief Concrete base class to assist with implementing CBC for
* 128-bit block ciphers.
*
* Reference: http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation
*
* \sa CBC
*/
/**
* \brief Constructs a new cipher in CBC mode.
*
* This constructor should be followed by a call to setBlockCipher().
*/
CBCCommon::CBCCommon()
: blockCipher(0)
, posn(16)
{
}
/**
* \brief Destroys this cipher object after clearing sensitive information.
*/
CBCCommon::~CBCCommon()
{
clean(iv);
clean(temp);
}
size_t CBCCommon::keySize() const
{
return blockCipher->keySize();
}
size_t CBCCommon::ivSize() const
{
return 16;
}
bool CBCCommon::setKey(const uint8_t* key, size_t len)
{
// Verify the cipher's block size, just in case.
if (blockCipher->blockSize() != 16)
return false;
// Set the key on the underlying block cipher.
return blockCipher->setKey(key, len);
}
bool CBCCommon::setIV(const uint8_t* iv, size_t len)
{
if (len != 16)
return false;
memcpy(this->iv, iv, 16);
posn = 16;
return true;
}
void CBCCommon::encrypt(uint8_t* output, const uint8_t* input, size_t len)
{
uint8_t posn;
while (len >= 16) {
for (posn = 0; posn < 16; ++posn)
iv[posn] ^= *input++;
blockCipher->encryptBlock(iv, iv);
for (posn = 0; posn < 16; ++posn)
*output++ = iv[posn];
len -= 16;
}
}
void CBCCommon::decrypt(uint8_t* output, const uint8_t* input, size_t len)
{
uint8_t posn;
while (len >= 16) {
blockCipher->decryptBlock(temp, input);
for (posn = 0; posn < 16; ++posn) {
uint8_t in = *input++;
*output++ = temp[posn] ^ iv[posn];
iv[posn] = in;
}
len -= 16;
}
}
void CBCCommon::clear()
{
blockCipher->clear();
clean(iv);
clean(temp);
posn = 16;
}
/**
* \fn void CBCCommon::setBlockCipher(BlockCipher *cipher)
* \brief Sets the block cipher to use for this CBC object.
*
* \param cipher The block cipher to use to implement CBC mode,
* which must have a block size of 16 bytes (128 bits).
*/
/**
* \class CBC CBC.h <CBC.h>
* \brief Implementation of the Cipher Block Chaining (CBC) mode for
* 128-bit block ciphers.
*
* The template parameter T must be a concrete subclass of BlockCipher
* indicating the specific block cipher to use. T must have a block size
* of 16 bytes (128 bits).
*
* For example, the following creates a CBC object using AES192 as the
* underlying cipher:
*
* \code
* CBC<AES192> cbc;
* cbc.setKey(key, 24);
* cbc.setIV(iv, 16);
* cbc.encrypt(output, input, len);
* \endcode
*
* Decryption is similar:
*
* \code
* CBC<AES192> cbc;
* cbc.setKey(key, 24);
* cbc.setIV(iv, 16);
* cbc.decrypt(output, input, len);
* \endcode
*
* The size of the ciphertext will always be the same as the size of
* the plaintext. Also, the length of the plaintext/ciphertext must be a
* multiple of 16. Extra bytes are ignored and not encrypted. The caller
* is responsible for padding the underlying data to a multiple of 16
* using an appropriate padding scheme for the application.
*
* Reference: http://en.wikipedia.org/wiki/Block_cipher_mode_of_operation
*
* \sa CTR, CFB, OFB
*/
/**
* \fn CBC::CBC()
* \brief Constructs a new CBC object for the block cipher T.
*/

View File

@@ -0,0 +1,66 @@
/*
* Copyright (C) 2015 Southern Storm Software, Pty Ltd.
*
* 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.
*/
#ifndef CRYPTO_CBC_h
#define CRYPTO_CBC_h
#include <Cipher.h>
#include <BlockCipher.h>
class CBCCommon : public Cipher
{
public:
virtual ~CBCCommon();
size_t keySize() const;
size_t ivSize() const;
bool setKey(const uint8_t* key, size_t len);
bool setIV(const uint8_t* iv, size_t len);
void encrypt(uint8_t* output, const uint8_t* input, size_t len);
void decrypt(uint8_t* output, const uint8_t* input, size_t len);
void clear();
protected:
CBCCommon();
void setBlockCipher(BlockCipher *cipher) { blockCipher = cipher; }
private:
BlockCipher *blockCipher;
uint8_t iv[16];
uint8_t temp[16];
uint8_t posn;
};
template <typename T>
class CBC : public CBCCommon
{
public:
CBC() { setBlockCipher(&cipher); }
private:
T cipher;
};
#endif

View File

@@ -0,0 +1,3 @@
#include "Ed25519.h"
using namespace RNS::Cryptography;

View File

@@ -0,0 +1,102 @@
#pragma once
#include "Bytes.h"
#include <Ed25519.h>
#include <memory>
/*
Note that the library currently in use for Ed25519 does not support generating keys from a seed.
*/
namespace RNS { namespace Cryptography {
class Ed25519PublicKey {
public:
using Ptr = std::shared_ptr<Ed25519PublicKey>;
public:
Ed25519PublicKey(const Bytes& publicKey) {
_publicKey = publicKey;
}
~Ed25519PublicKey() {}
public:
// creates a new instance with specified seed
static inline Ptr from_public_bytes(const Bytes& publicKey) {
return Ptr(new Ed25519PublicKey(publicKey));
}
inline const Bytes& public_bytes() {
return _publicKey;
}
inline bool verify(const Bytes& signature, const Bytes& message) {
return Ed25519::verify(signature.data(), _publicKey.data(), message.data(), message.size());
}
private:
Bytes _publicKey;
};
class Ed25519PrivateKey {
public:
using Ptr = std::shared_ptr<Ed25519PrivateKey>;
public:
Ed25519PrivateKey(const Bytes& privateKey) {
if (privateKey) {
// use specified private key
_privateKey = privateKey;
}
else {
// create random private key
Ed25519::generatePrivateKey(_privateKey.writable(32));
}
// derive public key from private key
Ed25519::derivePublicKey(_publicKey.writable(32), _privateKey.data());
}
~Ed25519PrivateKey() {}
public:
// creates a new instance with a random seed
static inline Ptr generate() {
// CBA TODO determine why below is confused with (implicit) copy constructor
//return Ptr(new Ed25519PrivateKey({Bytes::NONE}));
return Ptr(new Ed25519PrivateKey(Bytes::NONE));
}
// creates a new instance with specified seed
static inline Ptr from_private_bytes(const Bytes& privateKey) {
return Ptr(new Ed25519PrivateKey(privateKey));
}
inline const Bytes& private_bytes() {
return _privateKey;
}
// creates a new instance of public key for this private key
inline Ed25519PublicKey::Ptr public_key() {
return Ed25519PublicKey::from_public_bytes(_publicKey);
}
inline const Bytes sign(const Bytes& message) {
//z return _sk.sign(message);
Bytes signature;
Ed25519::sign(signature.writable(64), _privateKey.data(), _publicKey.data(), message.data(), message.size());
return signature;
}
private:
Bytes _privateKey;
Bytes _publicKey;
};
} }

View File

@@ -0,0 +1,116 @@
#include "Fernet.h"
#include "HMAC.h"
#include "PKCS7.h"
#include "AES.h"
#include "../Log.h"
#include <stdexcept>
#include <time.h>
using namespace RNS;
using namespace RNS::Cryptography;
Fernet::Fernet(const Bytes& key) {
if (!key) {
throw std::invalid_argument("Fernet key cannot be None");
}
if (key.size() != 32) {
throw std::invalid_argument("Fernet key must be 32 bytes, not " + std::to_string(key.size()));
}
//self._signing_key = key[:16]
_signing_key = key.left(16);
//self._encryption_key = key[16:]
_encryption_key = key.mid(16);
MEM("Fernet object created");
}
Fernet::~Fernet() {
MEM("Fernet object destroyed");
}
bool Fernet::verify_hmac(const Bytes& token) {
if (token.size() <= 32) {
throw std::invalid_argument("Cannot verify HMAC on token of only " + std::to_string(token.size()) + " bytes");
}
//received_hmac = token[-32:]
Bytes received_hmac = token.right(32);
DEBUG("Fernet::verify_hmac: received_hmac: " + received_hmac.toHex());
//expected_hmac = HMAC.new(self._signing_key, token[:-32]).digest()
Bytes expected_hmac = HMAC::generate(_signing_key, token.left(token.size()-32))->digest();
DEBUG("Fernet::verify_hmac: expected_hmac: " + expected_hmac.toHex());
return (received_hmac == expected_hmac);
}
const Bytes Fernet::encrypt(const Bytes& data) {
DEBUG("Fernet::encrypt: plaintext length: " + std::to_string(data.size()));
Bytes iv = random(16);
//double current_time = OS::time();
TRACE("Fernet::encrypt: iv: " + iv.toHex());
TRACE("Fernet::encrypt: plaintext: " + data.toHex());
Bytes ciphertext = AES_128_CBC::encrypt(
PKCS7::pad(data),
_encryption_key,
iv
);
DEBUG("Fernet::encrypt: padded ciphertext length: " + std::to_string(ciphertext.size()));
TRACE("Fernet::encrypt: ciphertext: " + ciphertext.toHex());
Bytes signed_parts = iv + ciphertext;
//return signed_parts + HMAC::generate(_signing_key, signed_parts)->digest();
Bytes sig(HMAC::generate(_signing_key, signed_parts)->digest());
TRACE("Fernet::encrypt: sig: " + sig.toHex());
Bytes token(signed_parts + sig);
DEBUG("Fernet::encrypt: token length: " + std::to_string(token.size()));
return token;
}
const Bytes Fernet::decrypt(const Bytes& token) {
DEBUG("Fernet::decrypt: token length: " + std::to_string(token.size()));
if (token.size() < 48) {
throw std::invalid_argument("Cannot decrypt token of only " + std::to_string(token.size()) + " bytes");
}
if (!verify_hmac(token)) {
throw std::invalid_argument("Fernet token HMAC was invalid");
}
//iv = token[:16]
Bytes iv = token.left(16);
TRACE("Fernet::decrypt: iv: " + iv.toHex());
//ciphertext = token[16:-32]
Bytes ciphertext = token.mid(16, token.size()-48);
TRACE("Fernet::decrypt: ciphertext: " + ciphertext.toHex());
try {
Bytes plaintext = PKCS7::unpad(
AES_128_CBC::decrypt(
ciphertext,
_encryption_key,
iv
)
);
DEBUG("Fernet::encrypt: unpadded plaintext length: " + std::to_string(plaintext.size()));
TRACE("Fernet::decrypt: plaintext: " + plaintext.toHex());
DEBUG("Fernet::decrypt: plaintext length: " + std::to_string(plaintext.size()));
return plaintext;
}
catch (std::exception& e) {
WARNING("Could not decrypt Fernet token");
throw std::runtime_error("Could not decrypt Fernet token");
}
}

View File

@@ -0,0 +1,41 @@
#pragma once
#include "Random.h"
#include "../Bytes.h"
#include <stdint.h>
namespace RNS { namespace Cryptography {
/*
This class provides a slightly modified implementation of the Fernet spec
found at: https://github.com/fernet/spec/blob/master/Spec.md
According to the spec, a Fernet token includes a one byte VERSION and
eight byte TIMESTAMP field at the start of each token. These fields are
not relevant to Reticulum. They are therefore stripped from this
implementation, since they incur overhead and leak initiator metadata.
*/
class Fernet {
public:
using Ptr = std::shared_ptr<Fernet>;
public:
static inline const Bytes generate_key() { return random(32); }
public:
Fernet(const Bytes& key);
~Fernet();
public:
bool verify_hmac(const Bytes& token);
const Bytes encrypt(const Bytes& data);
const Bytes decrypt(const Bytes& token);
private:
Bytes _signing_key;
Bytes _encryption_key;
};
} }

View File

@@ -0,0 +1,28 @@
#include "HKDF.h"
#include <HKDF.h>
#include <SHA256.h>
using namespace RNS;
const Bytes RNS::Cryptography::hkdf(size_t length, const Bytes& derive_from, const Bytes& salt /*= {Bytes::NONE}*/, const Bytes& context /*= {Bytes::NONE}*/) {
if (length <= 0) {
throw std::invalid_argument("Invalid output key length");
}
if (!derive_from) {
throw std::invalid_argument("Cannot derive key from empty input material");
}
HKDF<SHA256> hkdf;
if (salt) {
hkdf.setKey(derive_from.data(), derive_from.size(), salt.data(), salt.size());
}
else {
hkdf.setKey(derive_from.data(), derive_from.size());
}
Bytes derived;
hkdf.extract(derived.writable(length), length);
return derived;
}

View File

@@ -0,0 +1,9 @@
#pragma once
#include "../Bytes.h"
namespace RNS { namespace Cryptography {
const Bytes hkdf(size_t length, const Bytes& derive_from, const Bytes& salt = {Bytes::NONE}, const Bytes& context = {Bytes::NONE});
} }

View File

@@ -0,0 +1,109 @@
#pragma once
#include "../Bytes.h"
#include <Hash.h>
#include <SHA256.h>
#include <SHA512.h>
#include <stdexcept>
#include <memory>
#include <cassert>
namespace RNS { namespace Cryptography {
class HMAC {
public:
enum Digest {
DIGEST_NONE,
DIGEST_SHA256,
DIGEST_SHA512,
};
using Ptr = std::shared_ptr<HMAC>;
public:
/*
Create a new HMAC object.
key: bytes or buffer, key for the keyed hash object.
msg: bytes or buffer, Initial input for the hash or None.
digest: The underlying hash algorithm to use
*/
HMAC(const Bytes& key, const Bytes& msg = {Bytes::NONE}, Digest digest = DIGEST_SHA256) {
if (digest == DIGEST_NONE) {
throw std::invalid_argument("Cannot derive key from empty input material");
}
switch (digest) {
case DIGEST_SHA256:
_hash = std::unique_ptr<Hash>(new SHA256());
break;
case DIGEST_SHA512:
_hash = std::unique_ptr<Hash>(new SHA512());
break;
default:
throw std::invalid_argument("Unknown ior unsuppored digest");
}
_key = key;
_hash->resetHMAC(key.data(), key.size());
if (msg) {
update(msg);
}
}
/*
Feed data from msg into this hashing object.
*/
void update(const Bytes& msg) {
assert(_hash);
_hash->update(msg.data(), msg.size());
}
/*
Return the hash value of this hashing object.
This returns the hmac value as bytes. The object is
not altered in any way by this function; you can continue
updating the object after calling this function.
*/
Bytes digest() {
assert(_hash);
Bytes result;
_hash->finalizeHMAC(_key.data(), _key.size(), result.writable(_hash->hashSize()), _hash->hashSize());
return result;
}
/*
Create a new hashing object and return it.
key: bytes or buffer, The starting key for the hash.
msg: bytes or buffer, Initial input for the hash, or None.
digest: The underlying hash algorithm to use.
You can now feed arbitrary bytes into the object using its update()
method, and can ask for the hash value at any time by calling its digest()
method.
*/
static inline Ptr generate(const Bytes& key, const Bytes& msg = {Bytes::NONE}, Digest digest = DIGEST_SHA256) {
return Ptr(new HMAC(key, msg, digest));
}
private:
Bytes _key;
std::unique_ptr<Hash> _hash;
};
/*
Fast inline implementation of HMAC.
key: bytes or buffer, The key for the keyed hash object.
msg: bytes or buffer, Input message.
digest: The underlying hash algorithm to use.
*/
inline const Bytes digest(const Bytes& key, const Bytes& msg, HMAC::Digest digest = HMAC::DIGEST_SHA256) {
HMAC hmac(key, msg, digest);
hmac.update(msg);
return hmac.digest();
}
} }

View File

@@ -0,0 +1,36 @@
#include "Hashes.h"
#include "../Bytes.h"
#include <SHA256.h>
#include <SHA512.h>
using namespace RNS;
/*
The SHA primitives are abstracted here to allow platform-
aware hardware acceleration in the future. Currently only
uses Python's internal SHA-256 implementation. All SHA-256
calls in RNS end up here.
*/
const Bytes RNS::Cryptography::sha256(const Bytes& data) {
//TRACE("Cryptography::sha256: data: " + data.toHex() );
SHA256 digest;
digest.reset();
digest.update(data.data(), data.size());
Bytes hash;
digest.finalize(hash.writable(32), 32);
//TRACE("Cryptography::sha256: hash: " + hash.toHex() );
return hash;
}
const Bytes RNS::Cryptography::sha512(const Bytes& data) {
SHA512 digest;
digest.reset();
digest.update(data.data(), data.size());
Bytes hash;
digest.finalize(hash.writable(64), 64);
//TRACE("Cryptography::sha512: hash: " + hash.toHex() );
return hash;
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include "../Bytes.h"
#include <stdint.h>
namespace RNS { namespace Cryptography {
const Bytes sha256(const Bytes& data);
const Bytes sha512(const Bytes& data);
} }

View File

@@ -0,0 +1,66 @@
#pragma once
#include "../Bytes.h"
//#include "../Log.h"
#include <stdexcept>
namespace RNS { namespace Cryptography {
class PKCS7 {
public:
static const size_t BLOCKSIZE = 16;
static inline const Bytes pad(const Bytes& data, size_t bs = BLOCKSIZE) {
Bytes padded(data);
inplace_pad(padded, bs);
return padded;
}
static inline const Bytes unpad(const Bytes& data, size_t bs = BLOCKSIZE) {
Bytes unpadded(data);
inplace_unpad(unpadded, bs);
return unpadded;
}
// updates passed buffer
static inline void inplace_pad(Bytes& data, size_t bs = BLOCKSIZE) {
size_t len = data.size();
//DEBUG("PKCS7::pad: len: " + std::to_string(len));
size_t padlen = bs - (len % bs);
//DEBUG("PKCS7::pad: pad len: " + std::to_string(padlen));
// create zero-filled byte padding array of size padlen
//p v = bytes([padlen])
//uint8_t pad[padlen] = {0};
uint8_t pad[padlen];
memset(pad, 0, padlen);
// set last byte of padding array to size of padding
pad[padlen-1] = (uint8_t)padlen;
// concatenate data with padding
//p return data+v*padlen
data.append(pad, padlen);
//DEBUG("PKCS7::pad: data size: " + std::to_string(data.size()));
}
// updates passed buffer
static inline void inplace_unpad(Bytes& data, size_t bs = BLOCKSIZE) {
size_t len = data.size();
//DEBUG("PKCS7::unpad: len: " + std::to_string(len));
// read last byte which is pad length
//pad = data[-1]
size_t padlen = (size_t)data.data()[data.size()-1];
//DEBUG("PKCS7::unpad: pad len: " + std::to_string(padlen));
if (padlen > bs) {
throw std::runtime_error("Cannot unpad, invalid padding length of " + std::to_string(padlen) + " bytes");
}
// truncate data to strip padding
//return data[:len-padlen]
data.resize(len - padlen);
//DEBUG("PKCS7::unpad: data size: " + std::to_string(data.size()));
}
};
} }

View File

@@ -0,0 +1,38 @@
#pragma once
#include "../Bytes.h"
#include <RNG.h>
#include <stdint.h>
namespace RNS { namespace Cryptography {
// return vector specified length of random bytes
inline const Bytes random(size_t length) {
Bytes rand;
RNG.rand(rand.writable(length), length);
return rand;
}
// return 32 bit random unigned int
inline uint32_t randomnum() {
Bytes rand;
RNG.rand(rand.writable(4), 4);
uint32_t randnum = uint32_t((unsigned char)(rand.data()[0]) << 24 |
(unsigned char)(rand.data()[0]) << 16 |
(unsigned char)(rand.data()[0]) << 8 |
(unsigned char)(rand.data()[0]));
return randnum;
}
// return 32 bit random unsigned int between 0 and specified value
inline uint32_t randomnum(uint32_t max) {
return randomnum() % max;
}
// return random float value from 0 to 1
inline float random() {
return (float)(randomnum() / (float)0xffffffff);
}
} }

View File

@@ -0,0 +1,159 @@
#include "Token.h"
#include "HMAC.h"
#include "PKCS7.h"
#include "AES.h"
#include "../Log.h"
#include <stdexcept>
#include <time.h>
using namespace RNS;
using namespace RNS::Cryptography;
using namespace RNS::Type::Cryptography::Token;
Token::Token(const Bytes& key, token_mode mode /*= AES*/) {
if (!key) {
throw std::invalid_argument("Token key cannot be None");
}
if (mode == MODE_AES) {
if (key.size() == 32) {
_mode = MODE_AES_128_CBC;
//p self._signing_key = key[:16]
_signing_key = key.left(16);
//p self._encryption_key = key[16:]
_encryption_key = key.mid(16);
}
else if (key.size() == 64) {
_mode = MODE_AES_256_CBC;
//p self._signing_key = key[:32]
_signing_key = key.left(32);
//p self._encryption_key = key[32:]
_encryption_key = key.mid(32);
}
else {
throw std::invalid_argument("Token key must be 128 or 256 bits, not " + std::to_string(key.size()*8));
}
}
else {
throw std::invalid_argument("Invalid token mode: " + std::to_string(mode));
}
MEM("Token object created");
}
Token::~Token() {
MEM("Token object destroyed");
}
bool Token::verify_hmac(const Bytes& token) {
if (token.size() <= 32) {
throw std::invalid_argument("Cannot verify HMAC on token of only " + std::to_string(token.size()) + " bytes");
}
//received_hmac = token[-32:]
Bytes received_hmac = token.right(32);
DEBUG("Token::verify_hmac: received_hmac: " + received_hmac.toHex());
//expected_hmac = HMAC.new(self._signing_key, token[:-32]).digest()
Bytes expected_hmac = HMAC::generate(_signing_key, token.left(token.size()-32))->digest();
DEBUG("Token::verify_hmac: expected_hmac: " + expected_hmac.toHex());
return (received_hmac == expected_hmac);
}
const Bytes Token::encrypt(const Bytes& data) {
DEBUG("Token::encrypt: plaintext length: " + std::to_string(data.size()));
Bytes iv = random(16);
//double current_time = OS::time();
TRACE("Token::encrypt: iv: " + iv.toHex());
TRACE("Token::encrypt: plaintext: " + data.toHex());
Bytes ciphertext;
if (_mode == MODE_AES_128_CBC) {
ciphertext = AES_128_CBC::encrypt(
PKCS7::pad(data),
_encryption_key,
iv
);
}
else if (_mode == MODE_AES_256_CBC) {
ciphertext = AES_256_CBC::encrypt(
PKCS7::pad(data),
_encryption_key,
iv
);
}
else {
throw new std::invalid_argument("Invalid token mode "+std::to_string(_mode));
}
DEBUG("Token::encrypt: padded ciphertext length: " + std::to_string(ciphertext.size()));
TRACE("Token::encrypt: ciphertext: " + ciphertext.toHex());
Bytes signed_parts = iv + ciphertext;
//return signed_parts + HMAC::generate(_signing_key, signed_parts)->digest();
Bytes sig(HMAC::generate(_signing_key, signed_parts)->digest());
TRACE("Token::encrypt: sig: " + sig.toHex());
Bytes token(signed_parts + sig);
DEBUG("Token::encrypt: token length: " + std::to_string(token.size()));
return token;
}
const Bytes Token::decrypt(const Bytes& token) {
DEBUG("Token::decrypt: token length: " + std::to_string(token.size()));
if (token.size() < 48) {
throw std::invalid_argument("Cannot decrypt token of only " + std::to_string(token.size()) + " bytes");
}
if (!verify_hmac(token)) {
throw std::invalid_argument("Token token HMAC was invalid");
}
//iv = token[:16]
Bytes iv = token.left(16);
TRACE("Token::decrypt: iv: " + iv.toHex());
//ciphertext = token[16:-32]
Bytes ciphertext = token.mid(16, token.size()-48);
TRACE("Token::decrypt: ciphertext: " + ciphertext.toHex());
try {
Bytes plaintext;
if (_mode == MODE_AES_128_CBC) {
plaintext = PKCS7::unpad(
AES_128_CBC::decrypt(
ciphertext,
_encryption_key,
iv
)
);
}
else if (_mode == MODE_AES_256_CBC) {
plaintext = PKCS7::unpad(
AES_256_CBC::decrypt(
ciphertext,
_encryption_key,
iv
)
);
}
else {
throw new std::invalid_argument("Invalid token mode "+std::to_string(_mode));
}
DEBUG("Token::encrypt: unpadded plaintext length: " + std::to_string(plaintext.size()));
TRACE("Token::decrypt: plaintext: " + plaintext.toHex());
DEBUG("Token::decrypt: plaintext length: " + std::to_string(plaintext.size()));
return plaintext;
}
catch (std::exception& e) {
WARNING("Could not decrypt Token token");
throw std::runtime_error("Could not decrypt Token token");
}
}

View File

@@ -0,0 +1,47 @@
#pragma once
#include "Random.h"
#include "../Bytes.h"
#include "../Type.h"
#include <stdint.h>
namespace RNS { namespace Cryptography {
/*
This class provides a slightly modified implementation of the Fernet spec
found at: https://github.com/fernet/spec/blob/master/Spec.md
According to the spec, a Fernet token includes a one byte VERSION and
eight byte TIMESTAMP field at the start of each token. These fields are
not relevant to Reticulum. They are therefore stripped from this
implementation, since they incur overhead and leak initiator metadata.
*/
class Token {
public:
using Ptr = std::shared_ptr<Token>;
public:
static inline const Bytes generate_key(RNS::Type::Cryptography::Token::token_mode mode = RNS::Type::Cryptography::Token::MODE_AES_256_CBC) {
if (mode == RNS::Type::Cryptography::Token::MODE_AES_128_CBC) return random(32);
else if (mode == RNS::Type::Cryptography::Token::MODE_AES_256_CBC) return random(64);
else throw new std::invalid_argument("Invalid token mode: " + std::to_string(mode));
}
public:
Token(const Bytes& key, RNS::Type::Cryptography::Token::token_mode mode = RNS::Type::Cryptography::Token::MODE_AES);
~Token();
public:
bool verify_hmac(const Bytes& token);
const Bytes encrypt(const Bytes& data);
const Bytes decrypt(const Bytes& token);
private:
RNS::Type::Cryptography::Token::token_mode _mode = RNS::Type::Cryptography::Token::MODE_AES_256_CBC;
Bytes _signing_key;
Bytes _encryption_key;
};
} }

View File

@@ -0,0 +1,3 @@
#include "X25519.h"
using namespace RNS::Cryptography;

View File

@@ -0,0 +1,203 @@
#pragma once
#include "Bytes.h"
#include "Log.h"
#include <Curve25519.h>
#include <memory>
#include <stdexcept>
#include <stdint.h>
namespace RNS { namespace Cryptography {
class X25519PublicKey {
public:
using Ptr = std::shared_ptr<X25519PublicKey>;
public:
/*
X25519PublicKey(const Bytes& x) {
_x = x;
}
*/
X25519PublicKey(const Bytes& publicKey) {
_publicKey = publicKey;
}
~X25519PublicKey() {}
public:
// creates a new instance with specified seed
/*
static inline Ptr from_public_bytes(const Bytes& data) {
return Ptr(new X25519PublicKey(_unpack_number(data)));
}
*/
static inline Ptr from_public_bytes(const Bytes& publicKey) {
return Ptr(new X25519PublicKey(publicKey));
}
/*
Bytes public_bytes() {
return _pack_number(_x);
}
*/
Bytes public_bytes() {
return _publicKey;
}
private:
//Bytes _x;
Bytes _publicKey;
};
class X25519PrivateKey {
public:
const float MIN_EXEC_TIME = 2; // in milliseconds
const float MAX_EXEC_TIME = 500; // in milliseconds
const uint8_t DELAY_WINDOW = 10;
//z T_CLEAR = None
const uint8_t T_MAX = 0;
using Ptr = std::shared_ptr<X25519PrivateKey>;
public:
/*
X25519PrivateKey(const Bytes& a) {
_a = a;
}
*/
X25519PrivateKey(const Bytes& privateKey) {
if (privateKey) {
// use specified private key
_privateKey = privateKey;
// similar to derive public key from private key
// second param "f" is secret
//eval(uint8_t result[32], const uint8_t s[32], const uint8_t x[32])
// derive public key from private key
Curve25519::eval(_publicKey.writable(32), _privateKey.data(), 0);
}
else {
// create random private key and derive public key
// second param "f" is secret
//dh1(uint8_t k[32], uint8_t f[32])
Curve25519::dh1(_publicKey.writable(32), _privateKey.writable(32));
}
}
~X25519PrivateKey() {}
public:
// creates a new instance with a random seed
/*
static inline Ptr generate() {
return from_private_bytes(os.urandom(32));
}
*/
static inline Ptr generate() {
return from_private_bytes({Bytes::NONE});
}
// creates a new instance with specified seed
/*
static inline Ptr from_private_bytes(const Bytes& data) {
return Ptr(new X25519PrivateKey(_fix_secret(_unpack_number(data))));
}
*/
static inline Ptr from_private_bytes(const Bytes& privateKey) {
return Ptr(new X25519PrivateKey(privateKey));
}
/*
inline const Bytes private_bytes() {
return _pack_number(_a);
}
*/
inline const Bytes& private_bytes() {
return _privateKey;
}
// creates a new instance of public key for this private key
/*
inline X25519PublicKey::Ptr public_key() {
return X25519PublicKey::from_public_bytes(_pack_number(_raw_curve25519(9, _a)));
}
*/
inline X25519PublicKey::Ptr public_key() {
return X25519PublicKey::from_public_bytes(_publicKey);
}
/*
inline const Bytes exchange(const Bytes& peer_public_key) {
if isinstance(peer_public_key, bytes):
peer_public_key = X25519PublicKey.from_public_bytes(peer_public_key)
start = OS::time()
shared = _pack_number(_raw_curve25519(peer_public_key.x, _a))
end = OS::time()
duration = end-start
if X25519PrivateKey.T_CLEAR == None:
X25519PrivateKey.T_CLEAR = end + X25519PrivateKey.DELAY_WINDOW
if end > X25519PrivateKey.T_CLEAR:
X25519PrivateKey.T_CLEAR = end + X25519PrivateKey.DELAY_WINDOW
X25519PrivateKey.T_MAX = 0
if duration < X25519PrivateKey.T_MAX or duration < X25519PrivateKey.MIN_EXEC_TIME:
target = start+X25519PrivateKey.T_MAX
if target > start+X25519PrivateKey.MAX_EXEC_TIME:
target = start+X25519PrivateKey.MAX_EXEC_TIME
if target < start+X25519PrivateKey.MIN_EXEC_TIME:
target = start+X25519PrivateKey.MIN_EXEC_TIME
try:
OS::sleep(target-OS::time())
except Exception as e:
pass
elif duration > X25519PrivateKey.T_MAX:
X25519PrivateKey.T_MAX = duration
return shared
}
*/
inline const Bytes exchange(const Bytes& peer_public_key) {
DEBUG("X25519PublicKey::exchange: public key: " + _publicKey.toHex());
DEBUG("X25519PublicKey::exchange: peer public key: " + peer_public_key.toHex());
DEBUG("X25519PublicKey::exchange: pre private key: " + _privateKey.toHex());
Bytes sharedKey;
if (!Curve25519::eval(sharedKey.writable(32), _privateKey.data(), peer_public_key.data())) {
throw std::runtime_error("Peer key is invalid");
}
DEBUG("X25519PublicKey::exchange: shared key: " + sharedKey.toHex());
DEBUG("X25519PublicKey::exchange: post private key: " + _privateKey.toHex());
return sharedKey;
}
inline bool verify(const Bytes& peer_public_key) {
DEBUG("X25519PublicKey::exchange: public key: " + _publicKey.toHex());
DEBUG("X25519PublicKey::exchange: peer public key: " + peer_public_key.toHex());
DEBUG("X25519PublicKey::exchange: pre private key: " + _privateKey.toHex());
Bytes sharedKey(peer_public_key);
bool success = Curve25519::dh2(sharedKey.writable(32), _privateKey.writable(32));
DEBUG("X25519PublicKey::exchange: shared key: " + sharedKey.toHex());
DEBUG("X25519PublicKey::exchange: post private key: " + _privateKey.toHex());
return success;
}
private:
//Bytes _a;
Bytes _privateKey;
Bytes _publicKey;
};
} }