diff --git a/nixos/modules/services/networking/cjdns-hosts.sh b/nixos/modules/services/networking/cjdns-hosts.sh new file mode 100644 index 000000000000..8a2b47e52143 --- /dev/null +++ b/nixos/modules/services/networking/cjdns-hosts.sh @@ -0,0 +1,11 @@ +pubs=($pubs) +hosts=($hosts) + +lines="''\n" +for ((i = 0; i < ${#pubs[*]}; i++)); do + addr=$($cjdns/bin/publictoip6 ${pubs[i]}) + lines="${lines}$addr ${hosts[i]}\n" +done +lines="${lines}''" + +echo -ne $lines > $out diff --git a/nixos/modules/services/networking/cjdns.nix b/nixos/modules/services/networking/cjdns.nix index 7192b8b7a0e0..9888419309c1 100644 --- a/nixos/modules/services/networking/cjdns.nix +++ b/nixos/modules/services/networking/cjdns.nix @@ -4,8 +4,46 @@ with lib; let + pkg = pkgs.cjdns; + cfg = config.services.cjdns; + connectToSubmodule = + { options, ... }: + { options = + { password = mkOption { + type = types.str; + description = "Authorized password to the opposite end of the tunnel."; + }; + publicKey = mkOption { + type = types.str; + description = "Public key at the opposite end of the tunnel."; + }; + hostname = mkOption { + default = ""; + example = "foobar.hype"; + type = types.str; + description = "Optional hostname to add to /etc/hosts; prevents reverse lookup failures."; + }; + }; + }; + + peers = mapAttrsToList (n: v: v) (cfg.ETHInterface.connectTo // cfg.UDPInterface.connectTo); + + pubs = toString (map (p: if p.hostname == "" then "" else p.publicKey) peers); + hosts = toString (map (p: if p.hostname == "" then "" else p.hostname) peers); + + cjdnsHosts = + if hosts != "" then + import (pkgs.stdenv.mkDerivation { + name = "cjdns-hosts"; + builder = ./cjdns-hosts.sh; + + inherit (pkgs) cjdns; + inherit pubs hosts; + }) + else ""; + # would be nice to merge 'cfg' with a //, # but the json nesting is wacky. cjdrouteConf = builtins.toJSON ( { @@ -44,7 +82,7 @@ in enable = mkOption { type = types.bool; - default = false; + default = false; description = '' Whether to enable the cjdns network encryption and routing engine. A file at /etc/cjdns.keys will @@ -53,84 +91,80 @@ in ''; }; + confFile = mkOption { + type = types.str; + default = ""; + example = "/etc/cjdroute.conf"; + description = '' + Ignore all other cjdns options and load configuration from this file. + ''; + }; + authorizedPasswords = mkOption { type = types.listOf types.str; - default = [ ]; - example = [ + default = [ ]; + example = [ "snyrfgkqsc98qh1y4s5hbu0j57xw5s0" - "z9md3t4p45mfrjzdjurxn4wuj0d8swv" - "49275fut6tmzu354pq70sr5b95qq0vj" + "z9md3t4p45mfrjzdjurxn4wuj0d8swv" + "49275fut6tmzu354pq70sr5b95qq0vj" ]; - description = '' - Any remote cjdns nodes that offer these passwords on - connection will be allowed to route through this node. + description = '' + Any remote cjdns nodes that offer these passwords on + connection will be allowed to route through this node. ''; }; admin = { bind = mkOption { type = types.string; - default = "127.0.0.1:11234"; - description = '' + default = "127.0.0.1:11234"; + description = '' Bind the administration port to this address and port. - ''; + ''; }; }; UDPInterface = { bind = mkOption { type = types.string; - default = ""; + default = ""; example = "192.168.1.32:43211"; description = '' - Address and port to bind UDP tunnels to. - ''; - }; + Address and port to bind UDP tunnels to. + ''; + }; connectTo = mkOption { - type = types.attrsOf ( types.submodule ( - { options, ... }: - { options = { - # TODO make host an option, and add it to networking.extraHosts - password = mkOption { - type = types.str; - description = "Authorized password to the opposite end of the tunnel."; - }; - publicKey = mkOption { - type = types.str; - description = "Public key at the opposite end of the tunnel."; - }; - }; - } - )); - default = { }; + type = types.attrsOf ( types.submodule ( connectToSubmodule ) ); + default = { }; example = { "192.168.1.1:27313" = { - password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM"; + hostname = "homer.hype"; + password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM"; publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k"; }; }; description = '' - Credentials for making UDP tunnels. - ''; - }; + Credentials for making UDP tunnels. + ''; + }; }; ETHInterface = { bind = mkOption { - default = ""; - example = "eth0"; - description = '' - Bind to this device for native ethernet operation. - ''; - }; + default = ""; + example = "eth0"; + description = '' + Bind to this device for native ethernet operation. + ''; + }; beacon = mkOption { - type = types.int; + type = types.int; default = 2; description = '' Auto-connect to other cjdns nodes on the same network. Options: - 0: Disabled. + 0: Disabled. 1: Accept beacons, this will cause cjdns to accept incoming beacon messages and try connecting to the sender. 2: Accept and send beacons, this will cause cjdns to broadcast @@ -142,32 +176,20 @@ in }; connectTo = mkOption { - type = types.attrsOf ( types.submodule ( - { options, ... }: - { options = { - password = mkOption { - type = types.str; - description = "Authorized password to the opposite end of the tunnel."; - }; - publicKey = mkOption { - type = types.str; - description = "Public key at the opposite end of the tunnel."; - }; - }; - } - )); - default = { }; + type = types.attrsOf ( types.submodule ( connectToSubmodule ) ); + default = { }; example = { "01:02:03:04:05:06" = { - password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM"; + hostname = "homer.hype"; + password = "5kG15EfpdcKNX3f2GSQ0H1HC7yIfxoCoImnO5FHM"; publicKey = "371zpkgs8ss387tmr81q04mp0hg1skb51hw34vk1cq644mjqhup0.k"; }; }; - description = '' - Credentials for connecting look similar to UDP credientials + description = '' + Credentials for connecting look similar to UDP credientials except they begin with the mac address. - ''; - }; + ''; + }; }; }; @@ -185,34 +207,48 @@ in wantedBy = [ "multi-user.target" ]; after = [ "network-interfaces.target" ]; - script = '' - source /etc/cjdns.keys - echo '${cjdrouteConf}' | sed \ - -e "s/@CJDNS_ADMIN_PASSWORD@/$CJDNS_ADMIN_PASSWORD/g" \ - -e "s/@CJDNS_PRIVATE_KEY@/$CJDNS_PRIVATE_KEY/g" \ - | ${pkgs.cjdns}/bin/cjdroute - ''; + script = ( + if cfg.confFile != "" then "${pkg}/bin/cjdroute < ${cfg.confFile}" else + '' + source /etc/cjdns.keys + echo '${cjdrouteConf}' | sed \ + -e "s/@CJDNS_ADMIN_PASSWORD@/$CJDNS_ADMIN_PASSWORD/g" \ + -e "s/@CJDNS_PRIVATE_KEY@/$CJDNS_PRIVATE_KEY/g" \ + | ${pkg}/bin/cjdroute + '' + ); serviceConfig = { Type = "forking"; - Restart = "on-failure"; + Restart = "on-failure"; }; }; - system.activationScripts.cjdns = '' + system.activationScripts.cjdns = if (cfg.confFile == "") then "" else '' + cjdnsWriteKeys() { + private=$1 + ipv6=$2 + public=$3 + + echo "CJDNS_PRIVATE_KEY=$1" >> /etc/cjdns.keys + echo -e "CJDNS_IPV6=$2\nCJDNS_PUBLIC_KEY=$3" > /etc/cjdns.public + + chmod 600 /etc/cjdns.keys + chmod 444 /etc/cjdns.public + } + grep -q "CJDNS_PRIVATE_KEY=" /etc/cjdns.keys || \ - echo "CJDNS_PRIVATE_KEY=$(${pkgs.cjdns}/bin/makekey)" \ - >> /etc/cjdns.keys + cjdnsWriteKeys $(${pkg}/bin/makekeys) grep -q "CJDNS_ADMIN_PASSWORD=" /etc/cjdns.keys || \ - echo "CJDNS_ADMIN_PASSWORD=$(${pkgs.coreutils}/bin/head -c 96 /dev/urandom | ${pkgs.coreutils}/bin/tr -dc A-Za-z0-9)" \ - >> /etc/cjdns.keys - - chmod 600 /etc/cjdns.keys + echo "CJDNS_ADMIN_PASSWORD=$(${pkgs.coreutils}/bin/head -c 96 /dev/urandom | ${pkgs.coreutils}/bin/tr -dc A-Za-z0-9)" \ + >> /etc/cjdns.keys ''; + networking.extraHosts = "${cjdnsHosts}"; + assertions = [ - { assertion = ( cfg.ETHInterface.bind != "" || cfg.UDPInterface.bind != "" ); + { assertion = ( cfg.ETHInterface.bind != "" || cfg.UDPInterface.bind != "" || cfg.confFile == "" ); message = "Neither cjdns.ETHInterface.bind nor cjdns.UDPInterface.bind defined."; } { assertion = config.networking.enableIPv6; diff --git a/pkgs/tools/networking/cjdns/default.nix b/pkgs/tools/networking/cjdns/default.nix index c56faac690c3..9f734f2f6d0f 100644 --- a/pkgs/tools/networking/cjdns/default.nix +++ b/pkgs/tools/networking/cjdns/default.nix @@ -1,30 +1,31 @@ -{ stdenv, fetchgit, nodejs, which, python27 }: +{ stdenv, fetchFromGitHub, nodejs, which, python27 }: let - date = "20140922"; - rev = "5ebca772b0582173127e8c1e61ee235c5ab3fb50"; + date = "20140928"; + rev = "e2b673698e471dbc82b4e9dbc04cb9e16f1f06a6"; in stdenv.mkDerivation { name = "cjdns-${date}-${stdenv.lib.strings.substring 0 7 rev}"; - src = fetchgit { - url = "https://github.com/cjdelisle/cjdns.git"; + src = fetchFromGitHub { + owner = "cjdelisle"; + repo = "cjdns"; inherit rev; - sha256 = "04abf73f4aede12c35b70ae09a367b3d6352a63f818185f788ed13356d06197a"; + sha256 = "0ql51845rni6678dda03zr18ary7xlqcs3khva9x80x815h1sy8v"; }; + patches = [ ./rfc5952.patch ]; + buildInputs = [ which python27 nodejs]; - patches = [ ./makekey.patch ]; - buildPhase = "bash do"; - installPhase = "installBin cjdroute makekey"; + installPhase = "installBin cjdroute makekeys privatetopublic publictoip6"; - meta = { + meta = with stdenv.lib; { homepage = https://github.com/cjdelisle/cjdns; description = "Encrypted networking for regular people"; - license = stdenv.lib.licenses.gpl3; - maintainers = with stdenv.lib.maintainers; [ viric emery ]; - platforms = stdenv.lib.platforms.linux; + license = licenses.gpl3; + maintainers = with maintainers; [ viric emery ]; + platforms = platforms.unix; }; } diff --git a/pkgs/tools/networking/cjdns/makekey.patch b/pkgs/tools/networking/cjdns/makekey.patch deleted file mode 100644 index fcce5e3e728e..000000000000 --- a/pkgs/tools/networking/cjdns/makekey.patch +++ /dev/null @@ -1,64 +0,0 @@ -diff --git a/contrib/c/makekey.c b/contrib/c/makekey.c -new file mode 100644 -index 0000000..c7184e5 ---- /dev/null -+++ b/contrib/c/makekey.c -@@ -0,0 +1,46 @@ -+/* vim: set expandtab ts=4 sw=4: */ -+/* -+ * You may redistribute this program and/or modify it under the terms of -+ * the GNU General Public License as published by the Free Software Foundation, -+ * either version 3 of the License, or (at your option) any later version. -+ * -+ * This program is distributed in the hope that it will be useful, -+ * but WITHOUT ANY WARRANTY; without even the implied warranty of -+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -+ * GNU General Public License for more details. -+ * -+ * You should have received a copy of the GNU General Public License -+ * along with this program. If not, see . -+ */ -+#include "crypto/random/Random.h" -+#include "memory/MallocAllocator.h" -+#include "crypto/AddressCalc.h" -+#include "util/AddrTools.h" -+#include "util/Hex.h" -+ -+#include "crypto_scalarmult_curve25519.h" -+ -+#include -+ -+int main(int argc, char** argv) -+{ -+ struct Allocator* alloc = MallocAllocator_new(1<<22); -+ struct Random* rand = Random_new(alloc, NULL, NULL); -+ -+ uint8_t privateKey[32]; -+ uint8_t publicKey[32]; -+ uint8_t ip[16]; -+ uint8_t hexPrivateKey[65]; -+ -+ for (;;) { -+ Random_bytes(rand, privateKey, 32); -+ crypto_scalarmult_curve25519_base(publicKey, privateKey); -+ if (AddressCalc_addressForPublicKey(ip, publicKey)) { -+ Hex_encode(hexPrivateKey, 65, privateKey, 32); -+ printf(hexPrivateKey); -+ return 0; -+ } -+ } -+ return 0; -+} -+ -diff --git a/node_build/make.js b/node_build/make.js -index 5e51645..11465e3 100644 ---- a/node_build/make.js -+++ b/node_build/make.js -@@ -339,6 +339,7 @@ Builder.configure({ - builder.buildExecutable('contrib/c/privatetopublic.c'); - builder.buildExecutable('contrib/c/sybilsim.c'); - builder.buildExecutable('contrib/c/makekeys.c'); -+ builder.buildExecutable('contrib/c/makekey.c'); - - builder.buildExecutable('crypto/random/randombytes.c'); - diff --git a/pkgs/tools/networking/cjdns/rfc5952.patch b/pkgs/tools/networking/cjdns/rfc5952.patch new file mode 100644 index 000000000000..2152d192b53a --- /dev/null +++ b/pkgs/tools/networking/cjdns/rfc5952.patch @@ -0,0 +1,286 @@ +diff --git a/admin/angel/cjdroute2.c b/admin/angel/cjdroute2.c +index dfce6c6..77954a7 100644 +--- a/admin/angel/cjdroute2.c ++++ b/admin/angel/cjdroute2.c +@@ -80,7 +80,7 @@ static int genAddress(uint8_t addressOut[40], + if (AddressCalc_addressForPublicKey(address.ip6.bytes, address.key)) { + Hex_encode(privateKeyHexOut, 65, privateKey, 32); + Base32_encode(publicKeyBase32Out, 53, address.key, 32); +- Address_printIp(addressOut, &address); ++ Address_printShortIp(addressOut, &address); + return 0; + } + } +diff --git a/contrib/c/makekeys.c b/contrib/c/makekeys.c +index 3727fff..29582f1 100644 +--- a/contrib/c/makekeys.c ++++ b/contrib/c/makekeys.c +@@ -41,7 +41,7 @@ int main(int argc, char** argv) + if (AddressCalc_addressForPublicKey(ip, publicKey)) { + Hex_encode(hexPrivateKey, 65, privateKey, 32); + Base32_encode(publicKeyBase32, 53, publicKey, 32); +- AddrTools_printIp(printedIp, ip); ++ AddrTools_printShortIp(printedIp, ip); + printf("%s %s %s.k\n", hexPrivateKey, printedIp, publicKeyBase32); + } + } +diff --git a/contrib/c/privatetopublic.c b/contrib/c/privatetopublic.c +index 7f5f967..dc98f1c 100644 +--- a/contrib/c/privatetopublic.c ++++ b/contrib/c/privatetopublic.c +@@ -73,7 +73,7 @@ int main(int argc, char** argv) + AddressCalc_addressForPublicKey(address.ip6.bytes, address.key); + if (address.ip6.bytes[0] == 0xFC) { + Base32_encode(publicKeyBase32Out, 53, address.key, 32); +- Address_printIp(addressOut, &address); ++ Address_printShortIp(addressOut, &address); + printf( "Input privkey: %s\n" + "Matching pubkey: %s.k\n" + "Resulting address: %s\n" +diff --git a/contrib/c/publictoip6.c b/contrib/c/publictoip6.c +index fc92f7e..99afc4c 100644 +--- a/contrib/c/publictoip6.c ++++ b/contrib/c/publictoip6.c +@@ -48,7 +48,7 @@ int main(int argc, char** argv) + } + + uint8_t output[40] = {0}; +- AddrTools_printIp(output, ip6Bytes); ++ AddrTools_printShortIp(output, ip6Bytes); + printf("%s\n", output); + return 0; + } +diff --git a/dht/Address.c b/dht/Address.c +index e4c2dba..ba77cad 100644 +--- a/dht/Address.c ++++ b/dht/Address.c +@@ -102,6 +102,12 @@ void Address_printIp(uint8_t output[40], struct Address* addr) + AddrTools_printIp(output, addr->ip6.bytes); + } + ++void Address_printShortIp(uint8_t output[40], struct Address* addr) ++{ ++ Address_getPrefix(addr); ++ AddrTools_printShortIp(output, addr->ip6.bytes); ++} ++ + void Address_print(uint8_t output[60], struct Address* addr) + { + Address_printIp(output, addr); +diff --git a/dht/Address.h b/dht/Address.h +index 43c6f05..f200b40 100644 +--- a/dht/Address.h ++++ b/dht/Address.h +@@ -94,6 +94,8 @@ void Address_forKey(struct Address* out, const uint8_t key[Address_KEY_SIZE]); + + void Address_printIp(uint8_t output[40], struct Address* addr); + ++void Address_printShortIp(uint8_t output[40], struct Address* addr); ++ + void Address_print(uint8_t output[60], struct Address* addr); + + String* Address_toString(struct Address* addr, struct Allocator* alloc); +diff --git a/net/Ducttape.c b/net/Ducttape.c +index 84597d0..1813f3f 100644 +--- a/net/Ducttape.c ++++ b/net/Ducttape.c +@@ -227,7 +227,7 @@ static inline bool isRouterTraffic(struct Message* message, struct Headers_IP6He + #define debugHandles(logger, session, message, ...) \ + do { \ + uint8_t ip[40]; \ +- AddrTools_printIp(ip, session->ip6); \ ++ AddrTools_printIp(ip, session->ip6); \ + Log_debug(logger, "ver[%u] send[%d] recv[%u] ip[%s] " message, \ + session->version, \ + Endian_hostToBigEndian32(session->sendHandle_be), \ +@@ -271,10 +271,10 @@ static inline uint8_t incomingForMe(struct Message* message, + if (Bits_memcmp(addr.ip6.bytes, dtHeader->ip6Header->sourceAddr, 16)) { + #ifdef Log_DEBUG + uint8_t keyAddr[40]; +- Address_printIp(keyAddr, &addr); ++ Address_printShortIp(keyAddr, &addr); + Bits_memcpyConst(addr.ip6.bytes, dtHeader->ip6Header->sourceAddr, 16); + uint8_t srcAddr[40]; +- Address_printIp(srcAddr, &addr); ++ Address_printShortIp(srcAddr, &addr); + Log_debug(context->logger, + "DROP packet because source address is not same as key.\n" + " %s source addr\n" +@@ -292,7 +292,7 @@ static inline uint8_t incomingForMe(struct Message* message, + if (Checksum_udpIp6(dtHeader->ip6Header->sourceAddr, (uint8_t*)uh, message->length)) { + #ifdef Log_DEBUG + uint8_t keyAddr[40]; +- Address_printIp(keyAddr, &addr); ++ Address_printShortIp(keyAddr, &addr); + Log_debug(context->logger, + "DROP Router packet with incorrect checksum, from [%s]", keyAddr); + #endif +@@ -708,7 +708,7 @@ static inline int core(struct Message* message, + struct Address destination; + Bits_memcpyConst(destination.ip6.bytes, ip6Header->destinationAddr, 16); + uint8_t ipAddr[40]; +- Address_printIp(ipAddr, &destination); ++ Address_printShortIp(ipAddr, &destination); + Log_debug(context->logger, "Forwarding data to %s via %s\n", ipAddr, nhAddr); + #endif */ + } else { +@@ -723,7 +723,7 @@ static inline int core(struct Message* message, + struct Address destination; + Bits_memcpyConst(destination.ip6.bytes, ip6Header->destinationAddr, 16); + uint8_t ipAddr[40]; +- Address_printIp(ipAddr, &destination); ++ Address_printShortIp(ipAddr, &destination); + Log_info(context->logger, "DROP message because this node is the closest known " + "node to the destination %s.", ipAddr); + #endif +diff --git a/test/printIp_test.c b/test/printIp_test.c +new file mode 100644 +index 0000000..75d7427 +--- /dev/null ++++ b/test/printIp_test.c +@@ -0,0 +1,54 @@ ++/* vim: set expandtab ts=4 sw=4: */ ++/* ++ * You may redistribute this program and/or modify it under the terms of ++ * the GNU General Public License as published by the Free Software Foundation, ++ * either version 3 of the License, or (at your option) any later version. ++ * ++ * This program is distributed in the hope that it will be useful, ++ * but WITHOUT ANY WARRANTY; without even the implied warranty of ++ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ++ * GNU General Public License for more details. ++ * ++ * You should have received a copy of the GNU General Public License ++ * along with this program. If not, see . ++ */ ++ ++#include "crypto/random/Random.h" ++#include "memory/MallocAllocator.h" ++#include "util/AddrTools.h" ++#include "util/Assert.h" ++ ++#include ++ ++int main() ++{ ++ struct Allocator* alloc = MallocAllocator_new(1<<22); ++ struct Random* rand = Random_new(alloc, NULL, NULL); ++ ++ uint8_t ip[16]; ++ uint8_t printedIp[40]; ++ uint8_t printedShortIp[40]; ++ uint8_t ipFromFull[16]; ++ uint8_t ipFromShort[16]; ++ ++ for (int i = 0; i < 1024; ++i) { ++ Random_bytes(rand, ip, 16); ++ ++ for (int j = 0; j < 16; j++) { ++ // make the random result have lots of zeros since that's what we're looking for. ++ ip[j] = (ip[j] % 2) ? 0 : ip[j]; ++ } ++ ++ AddrTools_printIp(printedIp, ip); ++ AddrTools_printShortIp(printedShortIp, ip); ++ //printf("%s\n%s\n\n", printedIp, printedShortIp); ++ ++ AddrTools_parseIp(ipFromFull, printedIp); ++ AddrTools_parseIp(ipFromShort, printedShortIp); ++ ++ Assert_true(0 == Bits_memcmp(ip, ipFromFull, 16)); ++ Assert_true(0 == Bits_memcmp(ipFromFull, ipFromShort, 16)); ++ } ++ ++ return 0; ++} +diff --git a/util/AddrTools.h b/util/AddrTools.h +index 858ced4..d59544d 100644 +--- a/util/AddrTools.h ++++ b/util/AddrTools.h +@@ -143,6 +143,44 @@ static inline void AddrTools_printIp(uint8_t output[40], const uint8_t binIp[16] + output[39] = '\0'; + } + ++static inline void AddrTools_printShortIp(uint8_t output[40], const uint8_t binIp[16]) ++{ ++ /* The chances of hitting :0:0: and breaking ++ * RFC5952 are 1 in (1 / (2^16))^2 * 6. ++ * E. Siler ++ */ ++ ++ char *p = output; ++ int i = 0; ++ for (; i < 16;) { ++ if ((size_t)p != (size_t)output) { ++ *p++= ':'; ++ } ++ ++ if (binIp[i] > 0x0F) { ++ Hex_encode(p, 2, &binIp[i++], 1); ++ p += 2; ++ } else if (binIp[i] > 0x00) { ++ *p++ = Hex_encodeLowNibble(binIp[i++]); ++ } else { ++ ++i; ++ if (binIp[i] > 0x0F) { ++ Hex_encode(p, 2, &binIp[i++], 1); ++ p += 2; ++ } else { ++ *p++ = Hex_encodeLowNibble(binIp[i++]); ++ } ++ continue; ++ } ++ Hex_encode(p, 2, &binIp[i++], 1); ++ p += 2; ++ } ++ *p = '\0'; ++ ++ Assert_true((size_t)p <= ((size_t)output + 40)); ++ Assert_true(i <= 16); ++} ++ + /** + * Parse out an address. + * +diff --git a/util/Hex.c b/util/Hex.c +index e3e3c4d..b9bce57 100644 +--- a/util/Hex.c ++++ b/util/Hex.c +@@ -29,6 +29,8 @@ static const uint8_t numForAscii[] = + 99,99,99,99,99,99,99,99,99,99,99,99,99,99,99,99, + }; + ++static const char* hexEntities = "0123456789abcdef"; ++ + int Hex_encode(uint8_t* output, + const uint32_t outputLength, + const uint8_t* in, +@@ -40,8 +42,6 @@ int Hex_encode(uint8_t* output, + output[inputLength * 2] = '\0'; + } + +- static const char* hexEntities = "0123456789abcdef"; +- + for (uint32_t i = 0; i < inputLength; i++) { + output[i * 2] = hexEntities[in[i] >> 4]; + output[i * 2 + 1] = hexEntities[in[i] & 15]; +@@ -88,3 +88,8 @@ int Hex_decode(uint8_t* output, + + return length / 2; + } ++ ++uint8_t Hex_encodeLowNibble(const uint8_t nibble) ++{ ++ return hexEntities[nibble & 15]; ++} +diff --git a/util/Hex.h b/util/Hex.h +index 4570c3e..a12e402 100644 +--- a/util/Hex.h ++++ b/util/Hex.h +@@ -41,4 +41,6 @@ bool Hex_isHexEntity(const uint8_t character); + + int Hex_decodeByte(const uint8_t highNibble, const uint8_t lowNibble); + ++uint8_t Hex_encodeLowNibble(const uint8_t nibble); ++ + #endif