├── .gitignore ├── .gitmodules ├── crouton ├── README.md ├── SecretHandshakeStream.hh └── SecretHandshakeStream.cc ├── .github └── workflows │ └── build-test.yml ├── LICENSE ├── capnproto ├── README.md ├── SecretConnection.hh ├── SecretRPC.hh ├── SecretConnection.cc └── SecretRPC.cc ├── src ├── SecretHandshake_Internal.hh ├── shs.cc ├── SecretHandshake.cc └── SecretStream.cc ├── CMakeLists.txt ├── tests ├── shsCroutonTests.cc ├── SecretRPCTests.cc ├── SecretHandshakeTests.c ├── shsTests.cc └── SecretHandshakeTests.cc ├── CheatSheet.md ├── include ├── SecretHandshakeTypes.hh ├── SecretStream.h ├── shs.hh ├── SecretHandshake.hh ├── SecretHandshake.h └── SecretStream.hh └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | build_cmake 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/shs1-c"] 2 | path = vendor/shs1-c 3 | url = https://github.com/sunrise-choir/shs1-c.git 4 | [submodule "vendor/monocypher-cpp"] 5 | path = vendor/monocypher-cpp 6 | url = https://github.com/snej/monocypher-cpp.git 7 | -------------------------------------------------------------------------------- /crouton/README.md: -------------------------------------------------------------------------------- 1 | # SecretHandshake For Crouton 2 | 3 | These source files let you use the SecretHandshake protocol ([see parent directory](../README.md)) 4 | with the awesome [Crouton](https://github.com/couchbaselabs/crouton) coroutine & I/O library. 5 | 6 | * The `SecretHandshake` class simply runs the handshake over a Crouton `IStream`. 7 | * `SecretHandshakeStream` is an `IStream` subclass that wraps another stream, typically from a 8 | `TCPSocket`, and transparently runs the handshake and then encrypts/decrypts traffic. 9 | 10 | They're both pretty easy to use. See [shsCroutonTests.cc](../tests/shsCroutonTests.cc) for an example. 11 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | name: Build+Test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | name: Build and test 12 | 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macOS-latest] # TODO: Restore windows-latest (how to install libsodium?) 16 | runs-on: ${{ matrix.os }} 17 | 18 | steps: 19 | - name: Get Package 20 | uses: mstksg/get-package@v1 21 | with: 22 | brew: libsodium 23 | apt-get: libsodium-dev 24 | - name: checkout 25 | uses: actions/checkout@v2 26 | - name: checkout submodules 27 | run: git submodule update --init --recursive 28 | - name: build and test 29 | run: ./build_and_test.sh 30 | shell: bash 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Licensed under the MIT License: 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | The above copyright notice and this permission notice shall be included in 10 | all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 18 | THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /capnproto/README.md: -------------------------------------------------------------------------------- 1 | # SecretHandshake For Cap’n Proto 2 | 3 | These source files let you use the SecretHandshake protocol ([see parent directory](../README.md)) with the awesome [Cap’n Proto](https://capnproto.org/) RPC library. 4 | 5 | **SecretRPC** provides high-level RPC client and server classes that mimic Cap’n Proto’s `EzRpcClient`/`Server` classes, but use `SecretConnection` (q.v.) 6 | 7 | If you currently use Cap'n Proto `EzRpc` you should be able to drop in `SecretRPC` pretty easily. You’ll just need to use the `SecretKey` class to generate a key-pair, and persist it somehow. (Hint: put the secret key someplace secure, like the Mac/iOS Keychain.) 8 | 9 | **SecretConnection** is lower-level: it exposes a `StreamWrapper` class that takes a Cap’n Proto `AsyncIoStream` and returns a new `AsyncIoStream` that internally performs the SecretHandshake and the `SecretStream` encryption. 10 | 11 | If you use lower-level Cap’n Proto classes to create connections, you’ll need to use the classes in SecretConnection to wrap your plain-TCP `AsyncIOStream` with the secure one. You can look at the code in `SecretRPC.cc` for clues. -------------------------------------------------------------------------------- /src/SecretHandshake_Internal.hh: -------------------------------------------------------------------------------- 1 | // 2 | // SecretHandshake_Internal.hh 3 | // 4 | // Copyright © 2024 Jens Alfke. All rights reserved. 5 | // 6 | // Licensed under the MIT License: 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | 26 | #pragma once 27 | #include "SecretHandshakeTypes.hh" 28 | 29 | namespace snej::shs { 30 | 31 | void _Log(LogLevel, const char* format, ...) noexcept shs_printflike(2,3); 32 | 33 | #define Log(LEVEL, FMT, ...) \ 34 | do { if (LogCallback) _Log(LogLevel::LEVEL, FMT, ## __VA_ARGS__); } while (false) 35 | 36 | } 37 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | project(SecretHandshakeCpp) 4 | 5 | ## NOTE: libSodium is required for building the tests, but not the library itself. 6 | 7 | add_subdirectory(vendor/monocypher-cpp) 8 | 9 | set(CMAKE_CXX_STANDARD 17) 10 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 11 | set(CMAKE_C_STANDARD 11) 12 | set(CMAKE_C_STANDARD_REQUIRED ON) 13 | 14 | if (MSVC) 15 | add_compile_options(/W4) # TODO: Add /WX 16 | else() 17 | add_compile_options( 18 | -Wall 19 | -Wpedantic 20 | -Werror 21 | -Wno-unknown-pragmas 22 | -Wno-gnu-zero-variadic-macro-arguments # token pasting of ',' and __VA_ARGS__ 23 | ) 24 | add_compile_definitions( 25 | _LIBCPP_REMOVE_TRANSITIVE_INCLUDES # stops libc++ headers from including extra headers. 26 | ) 27 | endif() 28 | 29 | 30 | include_directories( 31 | include 32 | vendor/monocypher-cpp/include 33 | ) 34 | 35 | 36 | #### LIBRARY 37 | 38 | add_library( SecretHandshakeCpp STATIC 39 | src/shs.cc 40 | src/SecretHandshake.cc 41 | src/SecretStream.cc 42 | ) 43 | target_link_libraries( SecretHandshakeCpp INTERFACE 44 | MonocypherCpp 45 | ) 46 | 47 | 48 | #### TESTS 49 | 50 | if(APPLE) 51 | # Just for libSodium, used by shs1.c in the tests 52 | include_directories(SYSTEM PUBLIC 53 | /opt/homebrew/include 54 | /usr/local/include 55 | ) 56 | link_directories( 57 | /opt/homebrew/lib 58 | /usr/local/lib 59 | ) 60 | endif() 61 | 62 | add_executable( SecretHandshakeTests 63 | vendor/monocypher-cpp/tests/tests_main.cc # Include the Monocypher-cpp tests too 64 | vendor/monocypher-cpp/tests/MonocypherCppTests.cc 65 | tests/shsTests.cc 66 | tests/SecretHandshakeTests.cc 67 | tests/SecretHandshakeTests.c 68 | vendor/shs1-c/src/shs1.c 69 | ) 70 | 71 | if (CMAKE_COMPILER_IS_GNUCC) 72 | set_source_files_properties( 73 | vendor/shs1-c/src/shs1.c PROPERTIES COMPILE_OPTIONS "-Wno-array-parameter" 74 | ) 75 | endif() 76 | 77 | target_include_directories( SecretHandshakeTests PRIVATE 78 | vendor/shs1-c/src/ 79 | vendor/catch2 80 | vendor/monocypher-cpp/tests/ 81 | ) 82 | target_link_libraries( SecretHandshakeTests PRIVATE 83 | SecretHandshakeCpp 84 | sodium # used by shs1-c 85 | ) 86 | -------------------------------------------------------------------------------- /tests/shsCroutonTests.cc: -------------------------------------------------------------------------------- 1 | // 2 | // shsCroutonTests.cc 3 | // 4 | // Copyright © 2023 Jens Alfke. All rights reserved. 5 | // 6 | // Licensed under the MIT License: 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | #include "SecretHandshakeStream.hh" 28 | #include "crouton/Crouton.hh" 29 | #include 30 | 31 | #include "catch.hpp" 32 | 33 | using namespace std; 34 | using namespace snej::shs; 35 | using namespace ::crouton; 36 | 37 | 38 | static void RunCoroutine(function()> test) { 39 | Future f = test(); 40 | Scheduler::current().runUntil([&]{return f.hasResult();}); 41 | f.result(); // check exception 42 | } 43 | 44 | 45 | TEST_CASE("SecretHandshakeStream", "[SecretHandshake]") { 46 | using SecretHandshakeStream = snej::shs::crouton::SecretHandshakeStream; 47 | 48 | RunCoroutine([&]() -> Future { 49 | AppID app = Context::appIDFromString("SecretHandshakeStream"); 50 | KeyPair clientKeys = KeyPair::generate(); 51 | KeyPair serverKeys = KeyPair::generate(); 52 | Context clientCtx {app, clientKeys}; 53 | Context serverCtx {app, serverKeys}; 54 | auto [clientSock, serverSock] = io::LocalSocket::createPair(); 55 | 56 | SecretHandshakeStream clientStream(clientSock, clientCtx, &serverKeys.publicKey); 57 | SecretHandshakeStream serverStream(serverSock, serverCtx, nullptr); 58 | 59 | auto f1 = clientStream.open(); 60 | auto f2 = serverStream.open(); 61 | AWAIT f1; 62 | AWAIT f2; 63 | 64 | CHECK(clientStream.peerPublicKey() == serverKeys.publicKey); 65 | CHECK(serverStream.peerPublicKey() == clientKeys.publicKey); 66 | 67 | AWAIT clientStream.write(ConstBytes("Howdy neighbor")); 68 | string gotString = AWAIT serverStream.readString(14); 69 | CHECK(gotString == "Howdy neighbor"); 70 | 71 | AWAIT clientStream.close(); 72 | AWAIT serverStream.close(); 73 | RETURN noerror; 74 | }); 75 | } 76 | 77 | 78 | -------------------------------------------------------------------------------- /tests/SecretRPCTests.cc: -------------------------------------------------------------------------------- 1 | // 2 | // SecretRPCTests.cc 3 | // 4 | // Copyright © 2022 Jens Alfke. All rights reserved. 5 | // 6 | // Licensed under the MIT License: 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | // NOTE: This tests the Cap'n Proto support code. The build scripts in this repo do not build 28 | // or run this code, to avoid dragging in more dependencies that many users won't need. 29 | 30 | #include "SecretConnection.hh" 31 | #include "SecretRPC.hh" 32 | #include 33 | #include 34 | 35 | #include "catch.hpp" 36 | 37 | using namespace std; 38 | using namespace snej::shs; 39 | 40 | 41 | class TestExceptionCallback : public kj::ExceptionCallback { 42 | virtual void onFatalException(kj::Exception&& exception) override { 43 | cerr << "FATAL: " << exception.getDescription().cStr() << endl; 44 | } 45 | }; 46 | 47 | 48 | TEST_CASE("SecretConnection", "[SecretHandshake]") { 49 | bool successfulTest = GENERATE(true, false); 50 | cerr << (successfulTest ? "---- Successful connection\n" : "---- Failed connection\n"); 51 | 52 | kj::_::Debug::setLogLevel(kj::LogSeverity::INFO); 53 | TestExceptionCallback xcb; 54 | 55 | static AppID kAppID = Context::appIDFromString("SecretRPCTests"); 56 | Context clientContext{kAppID, KeyPair::generate()}; 57 | Context serverContext{kAppID, KeyPair::generate()}; 58 | auto serverKey = serverContext.keyPair.publicKey; 59 | if (!successfulTest) 60 | serverKey[8] ^= 0x40; 61 | ClientWrapper clientWrapper(clientContext, serverKey); 62 | ServerWrapper serverWrapper(serverContext, nullptr); 63 | clientWrapper.setIsSocket(false); 64 | serverWrapper.setIsSocket(false); 65 | 66 | kj::EventLoop loop; 67 | kj::WaitScope waitScope(loop); 68 | kj::TwoWayPipe pipe = kj::newTwoWayPipe(); 69 | 70 | kj::Own clientStream, serverStream; 71 | auto clientConn = clientWrapper.wrap(kj::mv(pipe.ends[0])).then([&](auto &&stream) { 72 | clientStream = kj::mv(stream); 73 | cerr << "Writing!\n"; 74 | return clientStream->write("HELLO", 5); 75 | }); 76 | 77 | char readBuf[100] = {}; 78 | 79 | auto serverConn = serverWrapper.wrap(kj::mv(pipe.ends[1])).then([&](auto &&stream) { 80 | serverStream = kj::mv(stream); 81 | cerr<< "Reading...\n"; 82 | return serverStream->read(readBuf, 5, 5); 83 | }) .then([&](size_t len) { 84 | CHECK(len == 5); 85 | CHECK(string(readBuf) == "HELLO"); 86 | }); 87 | 88 | if (successfulTest) { 89 | clientConn.wait(waitScope); 90 | serverConn.wait(waitScope); 91 | CHECK(string(readBuf) == "HELLO"); 92 | } else { 93 | auto result = clientConn.then([] {return true;}, [](kj::Exception) {return false;}); 94 | CHECK( result.wait(waitScope) == false ); 95 | result = serverConn.then([] {return true;}, [](kj::Exception) {return false;}); 96 | CHECK( result.wait(waitScope) == false ); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /CheatSheet.md: -------------------------------------------------------------------------------- 1 | # Secret Handshake Cheat Sheet 2 | 3 | This information comes from the [Secret Handshake paper](http://dominictarr.github.io/secret-handshake-paper/shs.pdf), p.11, but where the implementations differ (I mainly followed [the C one](https://github.com/sunrise-choir/shs1-c)) I've gone with them, because compatibility. 4 | 5 | There's also a [visual tutorial](https://ssbc.github.io/scuttlebutt-protocol-guide/#handshake) in the Scuttlebutt Protocol Guide. 6 | 7 | For the C++ code implementing this, look at `shs.cc` in this repo. 8 | 9 | ## Terminology: 10 | 11 | "A" is Alice, the peer who's initiating the connection, usually called the "client"; 12 | "B" is Bob, the peer accepting the connection, usually called the "server". 13 | 14 | | Name | Description | 15 | | --------- | ------------------------------------ | 16 | | *K* | application ID, 256-bit shared value | 17 | | *(A, Ap)* | client's long-term Ed25519 key pair | 18 | | *(B, Bp)* | server's long-term Ed25519 key pair | 19 | | *(a, ap)* | client's ephemeral X25519 key pair | 20 | | *(b, bp)* | server's ephemeral X25519 key pair | 21 | 22 | ## Functions: 23 | 24 | | Name | Description | 25 | |--------------|--------------------------------------| 26 | |*x \| y* | concatenation | 27 | |*x · y* | Curve25519 scalar multiplication, i.e. _x · yp_, which is the same as _y · xp_ | 28 | |*hmac\[k](d)* | HMAC-SHA-512-256 of data _d_ with key _k_ | 29 | |*hash(d)* | SHA-256 hash of _d_ | 30 | |*sign\[k](d)* | Ed25519 digital signature of _d_ with key _k_ | 31 | |*box\[k](d)* | "Secret box" as in libSodium or RFC8439, i.e. *poly1305(d) \| xsalsa20\[k](d)*. | 32 | 33 | > Note: HMAC-SHA-512-256 is just HMAC-SHA-512 with output truncated to 256 bits. 34 | 35 | > Note: XSalsa20 is used here with an all-zeroes nonce, since each key is only used once. 36 | 37 | ## The Secret Handshake 38 | 39 | ### Preconditions: 40 | 41 | - Client knows: *K, A, Ap, Bp* 42 | - Server knows: *K, B, Bp* 43 | 44 | ### Protocol: 45 | 46 | 0. **ephemeral keys** 47 | - client generates *(a, ap)*, server generates *(b, bp)* 48 | 1. **client challenge** 49 | - client sends ⟹ *hmac\[K](ap) | ap* 50 | 2. **server challenge** 51 | - server verifies client challenge; learns _ap_ 52 | - server sends ⟹ *hmac\[K](bp) | bp* 53 | 3. **client auth** 54 | - client verifies server challenge; learns _bp_ 55 | - client sends ⟹ *box\[K | a·b | a·B](H)* 56 | - where *H = sign\[A](K | Bp | hash(a·b)) | Ap* 57 | 4. **server ack** 58 | - server decrypts client auth, verifies signature; learns _Ap_ 59 | - server sends ⟹ *box\[K | a·b | a·B | A·b](sign\[B](K | H | hash(a·b)))* 60 | 5. **client validates ack** 61 | - client decrypts server ack, verifies signature 62 | 63 | If any verification fails, that peer immediately terminates the connection. 64 | 65 | ### Postconditions: 66 | 67 | - Client now knows: *b*p 68 | - Server now knows: *ap, Ap* 69 | 70 | ### Afterwards: 71 | 72 | Both compute the shared secret *SS = K | a·b | a·B | A·b* 73 | 74 | Both derive the following keys & nonces: 75 | 76 | - Client encryption key: *hash(hash(hash(SS)) | Bp)* 77 | - Client nonce: *hmac\[K](bp)* [only 1st 24 bytes needed] 78 | - Server encryption key: *hash(hash(hash(SS)) | Ap)* 79 | - Server nonce: *hmac\[K](ap)* [only 1st 24 bytes needed] 80 | 81 | They can now communicate using these. Any 256-bit symmetric cipher will work; the Scuttlebutt “box-stream” protocol uses the same secret-box as before, i.e. XSalsa20 prefixed with a Poly1305 MAC. 82 | 83 | ## Appendix: Compatibility Notes 84 | 85 | There are several discrepancies between the the original protocol design published in the paper and the existing implementations. This documentation and code follow the implementations. 86 | 87 | * Step 1: The paper has the client send _ap | hmac\[K](ap)_, i.e. public key first, not last. 88 | * Step 2: The paper has the server send _bp | hmac\[K | a·b](bp)_. 89 | "I was reluctant to change it since it didn't have security implications, and also changing things was hard." —[Dominic Tarr](https://github.com/auditdrivencrypto/secret-handshake/issues/7) 90 | * Step 3: The paper defines H as _Ap | sign\[A](K | Bp | hash(a·b))_, i.e. putting the public key before the signature, not after it. 91 | -------------------------------------------------------------------------------- /include/SecretHandshakeTypes.hh: -------------------------------------------------------------------------------- 1 | // 2 | // SecretHandshakeTypes.hh 3 | // 4 | // Copyright © 2021 Jens Alfke. All rights reserved. 5 | // 6 | 7 | #pragma once 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #if __has_attribute(format) 14 | # define shs_printflike(FMT,DOT) __attribute__((__format__ (__printf__, FMT, DOT))) 15 | #endif 16 | 17 | 18 | namespace snej::shs { 19 | 20 | /// This is an idiomatic C++ wrapper implementation of the 21 | /// ["Secret Handshake"](https://github.com/auditdrivencrypto/secret-handshake) protocol. 22 | /// This allows a client and server, each with a longterm key-pair, to form a secure 23 | /// authenticated connection, with a session key for encrypting subsequent traffic. 24 | /// To connect, the client must already know the server's public key. 25 | 26 | 27 | /// An arbitrary 32-byte value identifying your higher-level application protocol. 28 | /// Client and server must both use the same AppID to connect. 29 | /// The AppID is usually not secret, unless you want to add a layer of "secrecy through 30 | /// obscurity" to your protocol. 31 | using AppID = std::array; 32 | 33 | /// A 256-bit Ed25519 public key. 34 | /// Data layout is the same as the public key in Sodium's `crypto_sign_` API 35 | /// and Monocypher's `crypto_ed25519_` API. 36 | using PublicKey = std::array; 37 | 38 | /// A secret "seed" value that can be extracted from an Ed25519 key-pair and reused to 39 | /// reconstitute it. 40 | using SigningKey = std::array; 41 | 42 | /// A 256-bit symmetrical session key. 43 | /// The Secret Handshake algorithm derives this key, but doesn't care what you do with it. 44 | /// Scuttlebutt uses it with Sodium's `crypto_box` API to encrypt message bodies. 45 | /// You could instead use it and the nonce with a stream cipher like XSalsa20; the Session 46 | /// class provides some utility methods for that. 47 | using SessionKey = std::array; 48 | 49 | using KeyPairBytes = std::array; 50 | 51 | /// A 192-bit nonce for use with a `SessionKey`. 52 | using Nonce = std::array; 53 | 54 | 55 | /// An Ed25519 key-pair, used for authentication. 56 | /// Data layout is the same as the "secret key" in Sodium's `crypto_sign_` API. 57 | struct KeyPair { 58 | SigningKey signingKey; 59 | PublicKey publicKey; 60 | 61 | /// Generates a new key-pair (using Monocypher.) 62 | static KeyPair generate(); 63 | 64 | /// Reconstitutes a key-pair from its private key alone. 65 | explicit KeyPair(SigningKey const&); 66 | 67 | explicit KeyPair(KeyPairBytes const& bytes) { 68 | ::memcpy(&signingKey, &bytes, sizeof(bytes)); 69 | } 70 | 71 | KeyPairBytes data() const { 72 | return *reinterpret_cast(&signingKey); 73 | } 74 | 75 | ~KeyPair(); 76 | private: 77 | KeyPair() = default; 78 | }; 79 | 80 | static inline bool operator==(KeyPair const& kp1, KeyPair const& kp2) { 81 | return kp1.signingKey == kp2.signingKey && kp1.publicKey == kp2.publicKey; 82 | } 83 | 84 | 85 | 86 | /// The local state needed to start a handshake: AppID and key-pair. 87 | struct Context { 88 | Context(AppID const& a, KeyPair const& sk) :appID(a), keyPair(sk) { } 89 | Context(char const* str, KeyPair const& sk) :appID(appIDFromString(str)), keyPair(sk) { } 90 | 91 | AppID const appID; ///< Arbitrary 32-byte value identifying the app/protocol 92 | KeyPair const keyPair; ///< Ed25519 key-pair for authentication 93 | 94 | /// Simple transformation of an ASCII string to an AppID. 95 | /// Up to 32 bytes of the string are copied to the AppID, and the rest is padded with 00. 96 | static AppID appIDFromString(const char *str); 97 | }; 98 | 99 | 100 | 101 | /// Result of the secret handshake: 102 | /// * session encryption / decryption keys with nonces, 103 | /// * and the peer's long-term public key (which is news to the server, but not to the client.) 104 | struct Session { 105 | SessionKey encryptionKey; ///< The session encryption key 106 | Nonce encryptionNonce; ///< Nonce to use with the encryption key 107 | SessionKey decryptionKey; ///< The session decryption key 108 | Nonce decryptionNonce; ///< Nonce to use with the decryption key 109 | 110 | PublicKey peerPublicKey; ///< The peer's authenticated public key 111 | 112 | ~Session(); 113 | }; 114 | 115 | 116 | /// Log levels; same values as spdlog or Crouton. 117 | enum class LogLevel { trace, debug, info, warn, err, critical, off }; 118 | 119 | /// Optional callback to receive log messages created by SecretHandshake. 120 | extern void (*LogCallback)(LogLevel, const char* format, va_list args) shs_printflike(2,0); 121 | 122 | } 123 | -------------------------------------------------------------------------------- /tests/SecretHandshakeTests.c: -------------------------------------------------------------------------------- 1 | // 2 | // SecretHandshakeTests.c 3 | // 4 | // Copyright © 2022 Jens Alfke. All rights reserved. 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | #include "SecretHandshake.h" 20 | #include "SecretStream.h" 21 | #include 22 | #include 23 | 24 | bool test_C_Handshake(void); 25 | bool test_C_HandshakeWrongServerKey(void); 26 | 27 | 28 | static bool sTestResult; 29 | 30 | #define CHECK(X) if (!(X)) {sTestResult = false; \ 31 | fprintf(stderr, "CHECK failed: %s at line %d\n", #X, __LINE__);} 32 | #define REQUIRE(X) if (!(X)) {CHECK(X); return false;} 33 | 34 | #define EqualStructs(A, B) (0 == memcmp(&(A), &(B), sizeof(A))) 35 | 36 | 37 | typedef struct HandshakeTest { 38 | SHSAppID appID; 39 | SHSKeyPair serverKey, clientKey; 40 | SHSHandshake* server; 41 | SHSHandshake* client; 42 | } HandshakeTest; 43 | 44 | 45 | static void initHandshakeTest(HandshakeTest *test) { 46 | test->appID = SHSAppID_FromString("App"); 47 | test->serverKey = SHSKeyPair_Generate(); 48 | test->clientKey = SHSKeyPair_Generate(); 49 | test->server = SHSHandshake_CreateServer(&test->appID, &test->serverKey); 50 | test->client = SHSHandshake_CreateClient(&test->appID, &test->clientKey, &test->serverKey.publicKey); 51 | } 52 | 53 | static void freeHandshakeTest(HandshakeTest *test) { 54 | SHSKeyPair_Erase(&test->serverKey); 55 | SHSKeyPair_Erase(&test->clientKey); 56 | SHSHandshake_Free(test->server); 57 | SHSHandshake_Free(test->client); 58 | } 59 | 60 | static bool sendFromTo(SHSHandshake *src, SHSHandshake *dst, size_t expectedCount) { 61 | // One step of the handshake: 62 | CHECK(SHSHandshake_GetBytesNeeded(src) == 0); 63 | CHECK(SHSHandshake_GetBytesToSend(dst).size == 0); 64 | SHSInputBuffer toSend = SHSHandshake_GetBytesToSend(src); 65 | CHECK(toSend.size == expectedCount); 66 | SHSOutputBuffer toRead = SHSHandshake_GetBytesToRead(dst); 67 | CHECK(toRead.size == toSend.size); 68 | memcpy(toRead.dst, toSend.src, toSend.size); 69 | SHSHandshake_ReadCompleted(dst); 70 | SHSHandshake_SendCompleted(src); 71 | return !SHSHandshake_GetError(src) && !SHSHandshake_GetError(dst); 72 | } 73 | 74 | 75 | bool test_C_Handshake(void) { 76 | sTestResult = true; 77 | HandshakeTest test; 78 | initHandshakeTest(&test); 79 | 80 | // Run the handshake: 81 | REQUIRE(sendFromTo(test.client, test.server, 64)); 82 | REQUIRE(sendFromTo(test.server, test.client, 64)); 83 | REQUIRE(sendFromTo(test.client, test.server, 112)); 84 | REQUIRE(sendFromTo(test.server, test.client, 80)); 85 | 86 | REQUIRE(SHSHandshake_Finished(test.server)); 87 | REQUIRE(SHSHandshake_Finished(test.client)); 88 | 89 | // Check that they ended up with matching session keys, and each other's public keys: 90 | SHSSession clientSession = SHSHandshake_GetSession(test.client); 91 | SHSSession serverSession = SHSHandshake_GetSession(test.server); 92 | CHECK(EqualStructs(clientSession.encryptionKey , serverSession.decryptionKey)); 93 | CHECK(EqualStructs(clientSession.encryptionNonce , serverSession.decryptionNonce)); 94 | CHECK(EqualStructs(clientSession.decryptionKey , serverSession.encryptionKey)); 95 | CHECK(EqualStructs(clientSession.decryptionNonce , serverSession.encryptionNonce)); 96 | 97 | CHECK(EqualStructs(serverSession.peerPublicKey , test.clientKey.publicKey)); 98 | CHECK(EqualStructs(clientSession.peerPublicKey , test.serverKey.publicKey)); 99 | 100 | SHSSession_Erase(&serverSession); 101 | SHSSession_Erase(&clientSession); 102 | freeHandshakeTest(&test); 103 | return sTestResult; 104 | } 105 | 106 | 107 | bool test_C_HandshakeWrongServerKey(void) { 108 | sTestResult = true; 109 | HandshakeTest test; 110 | initHandshakeTest(&test); 111 | 112 | // Create a client that has the wrong server public key: 113 | SHSPublicKey badServerKey = test.serverKey.publicKey; 114 | badServerKey.bytes[17]++; 115 | SHSHandshake* badClient = SHSHandshake_CreateClient(&test.appID, &test.clientKey, &badServerKey); 116 | 117 | // Run the handshake: 118 | REQUIRE(sendFromTo(badClient, test.server, 64)); 119 | REQUIRE(sendFromTo(test.server, badClient, 64)); 120 | REQUIRE(!sendFromTo(badClient, test.server, 112)); 121 | REQUIRE(SHSHandshake_GetError(test.server) == SHSAuthError); 122 | 123 | SHSHandshake_Free(badClient); 124 | freeHandshakeTest(&test); 125 | return sTestResult; 126 | } 127 | -------------------------------------------------------------------------------- /crouton/SecretHandshakeStream.hh: -------------------------------------------------------------------------------- 1 | // 2 | // SecretHandshakeStream.hh 3 | // 4 | // Copyright © 2023 Jens Alfke. All rights reserved. 5 | // 6 | 7 | #pragma once 8 | #include "../include/SecretHandshakeTypes.hh" 9 | #include "crouton/io/IStream.hh" 10 | #include "crouton/io/ISocket.hh" 11 | 12 | namespace snej::shs { 13 | class DecryptionStream; 14 | class EncryptionStream; 15 | class Handshake; 16 | } 17 | namespace snej::shs::crouton { 18 | using namespace ::crouton; 19 | 20 | /** Error enum for SecretHandshake failures. */ 21 | enum class SecretHandshakeError : errorcode_t { 22 | ProtocolError = 1, // Handshake failed due to bad data 23 | AuthError, // Handshake failed because peer rejected public key 24 | DataError, // Invalid data received after handshake 25 | }; 26 | 27 | 28 | /** Runs the SecretHandshake protocol over a Crouton IStream. 29 | After the handshake completes, it's up to you how to send/receive data. 30 | You probably want to use SecretHandshakeStream instead. */ 31 | class SecretHandshake { 32 | public: 33 | 34 | /// Constructs a SecretHandshake. 35 | /// @param context Contains your key-pair and the app ID. 36 | /// @param serverKey For a client connection, pass the server's known public key. 37 | /// For a server connection, pass nullptr. 38 | SecretHandshake(shs::Context const& context, PublicKey const* serverKey); 39 | 40 | /// Registers a callback that determines whether a client should be allowed to connect. 41 | /// It takes the client public key as a parameter, and returns true to allow connection. 42 | /// If this is not called, the default is to allow any client. 43 | void setClientAuthorizer(std::function); 44 | 45 | /// Performs the handshake. 46 | /// Upon successful completion, returns the Session struct with the sesssion keys. 47 | /// On failure, returns a SecretHandshakeError. 48 | ASYNC handshake(std::shared_ptr); 49 | 50 | private: 51 | std::unique_ptr _handshake; 52 | }; 53 | 54 | 55 | 56 | /** Optional delegate interface for a SecretHandshakeStream. */ 57 | struct SecretHandshakeStreamDelegate { 58 | virtual bool authorizeSecretHandshake(PublicKey const&) {return true;} 59 | virtual void secretHandshakeStreamClosed() { } 60 | virtual ~SecretHandshakeStreamDelegate() = default; 61 | }; 62 | 63 | 64 | /** A Crouton IStream implementation that wraps another IStream, probably an ISocket's. 65 | It runs the SecretHandshake protocol, and on success, encrypts & decrypts stream data.*/ 66 | class SecretHandshakeStream : public io::IStream { 67 | public: 68 | SecretHandshakeStream(std::shared_ptr stream, 69 | Context const&, 70 | PublicKey const* serverKey); 71 | ~SecretHandshakeStream(); 72 | 73 | bool isOpen() const override; 74 | ASYNC open() override; 75 | ASYNC close() override; 76 | ASYNC closeWrite() override; 77 | 78 | ASYNC readNoCopy(size_t maxLen = 65536) override; 79 | ASYNC peekNoCopy() override; 80 | 81 | ASYNC write(ConstBytes) override; 82 | ASYNC write(const ConstBytes buffers[], size_t nBuffers) override; 83 | 84 | /// The connected peer's public key. Stream MUST be open. 85 | PublicKey const& peerPublicKey() const; 86 | 87 | using Delegate = SecretHandshakeStreamDelegate; 88 | void setDelegate(Delegate*); 89 | 90 | protected: 91 | friend class SecretHandshakeSocket; 92 | void setRawStream(std::shared_ptr); 93 | 94 | private: 95 | void notifyClosed(); 96 | 97 | std::shared_ptr _stream; 98 | SecretHandshake _handshake; 99 | Delegate* _delegate = nullptr; 100 | shs::PublicKey _peerPublicKey; 101 | std::unique_ptr _writer; 102 | std::unique_ptr _reader; 103 | size_t _lastReadSize = 0; 104 | size_t _lastWriteSize = 0; 105 | bool _open = false; 106 | }; 107 | 108 | 109 | 110 | /** An ISocket implementation that opens a TCPSocket and wraps its stream with a 111 | SecretHandshakeStream. */ 112 | class SecretHandshakeSocket : public io::ISocket { 113 | public: 114 | SecretHandshakeSocket(Context const&, PublicKey const& serverKey); 115 | ASYNC open() override; 116 | std::shared_ptr stream() override; 117 | 118 | private: 119 | std::shared_ptr _stream; 120 | }; 121 | 122 | } 123 | 124 | namespace crouton { 125 | template <> struct ErrorDomainInfo { 126 | static constexpr string_view name = "SecretHandshake"; 127 | static string description(errorcode_t); 128 | }; 129 | } 130 | -------------------------------------------------------------------------------- /capnproto/SecretConnection.hh: -------------------------------------------------------------------------------- 1 | // 2 | // SecretConnection.hh 3 | // 4 | // Copyright © 2021 Jens Alfke. All rights reserved. 5 | // 6 | 7 | #pragma once 8 | #include "SecretHandshake.hh" 9 | #include 10 | #include 11 | 12 | namespace snej::shs { 13 | 14 | /// Cap'n Proto AsyncStream wrapper factory for SecretHandshake connections. 15 | /// This is an abstract class; use `ServerWrapper` or `ClientWrapper`. 16 | class StreamWrapper { 17 | public: 18 | /// A server-side callback that accepts or rejects a client given its public key. 19 | using Authorizer = std::function; 20 | 21 | explicit StreamWrapper(Context const& context) :_context(context) { } 22 | virtual ~StreamWrapper() = default; 23 | 24 | void setConnectTimeout(kj::Duration timeout, kj::Timer &timer); 25 | 26 | void setIsSocket(bool isSocket) {_isSocket = isSocket;} 27 | bool isSocket() const {return _isSocket;} 28 | 29 | /// Upgrades a regular network stream to use SecretHandshake. 30 | /// The returned promise resolves when the handshake has completed successfully. 31 | kj::Promise> wrap(kj::Own); 32 | 33 | /// Upgrade a regular authenticated network stream to use SecretHandshake. 34 | /// The returned promise resolves when the handshake has completed successfully. 35 | /// @note The stream's `peerIdentity` will be a `SHSPeerIdentity`. 36 | kj::Promise wrap(kj::AuthenticatedStream stream); 37 | 38 | 39 | /// Async version of `wrap` that takes a promised stream. 40 | /// The wrapper is passed as a parameter so that it can be NULL, which is a no-op. 41 | template 42 | static kj::Promise asyncWrap(StreamWrapper *wrapper, 43 | kj::Promise streamPromise) 44 | { 45 | if (!wrapper) 46 | return streamPromise; 47 | return streamPromise.then([=](auto && stream) mutable { 48 | return wrapper->wrap(kj::mv(stream)); 49 | }); 50 | } 51 | 52 | protected: 53 | virtual kj::Own newHandshake() =0; 54 | 55 | Context _context; 56 | Authorizer _authorizer; 57 | kj::Maybe _connectTimeout; 58 | kj::Maybe _connectTimer; 59 | bool _isSocket = true; 60 | }; 61 | 62 | 63 | /// Cap'n Proto AsyncStream wrapper factory for SecretHandshake server (incoming) connections. 64 | class ServerWrapper final : public StreamWrapper { 65 | public: 66 | /// Constructs a ServerWrapper. 67 | /// @param context The app ID and key-pair. 68 | /// @param auth Callback that accepts or rejects a client given its public key. 69 | explicit ServerWrapper(Context const& context, 70 | Authorizer auth) 71 | :StreamWrapper(context) {_authorizer = kj::mv(auth);} 72 | 73 | Authorizer const& authorizer() const {return _authorizer;} 74 | 75 | private: 76 | kj::Own newHandshake() override; 77 | }; 78 | 79 | 80 | 81 | /// Cap'n Proto AsyncStream wrapper factory for SecretHandshake client (outgoing) connections. 82 | class ClientWrapper final : public StreamWrapper { 83 | public: 84 | /// Constructs a ClientWrapper. 85 | /// @param context The app ID and key-pair. 86 | /// @param serverKey The server's public key. The handshake will verify this. 87 | ClientWrapper(Context const& context, 88 | PublicKey const& serverKey) 89 | :StreamWrapper(context) ,_serverPublicKey(serverKey) { } 90 | 91 | PublicKey const& serverPublicKey() const {return _serverPublicKey;} 92 | 93 | private: 94 | kj::Own newHandshake() override; 95 | PublicKey const _serverPublicKey; 96 | }; 97 | 98 | 99 | 100 | /// PeerIdentity of an AuthenticatedStream produced by a SecretHandshake Context. 101 | /// Reveals the peer's public key. This is useful for the server, but not for the client 102 | /// (which had to know the server's public key already, to make the handshake.) 103 | class SHSPeerIdentity final : public kj::PeerIdentity { 104 | public: 105 | SHSPeerIdentity(PublicKey const& key, 106 | kj::Own inner) 107 | :_publicKey(key), _inner(kj::mv(inner)) { } 108 | 109 | kj::String toString() override; 110 | 111 | /// The peer's public key. 112 | PublicKey publicKey() const {return _publicKey;} 113 | 114 | /// The identity information of the underlying transport. 115 | kj::PeerIdentity const* inner() const {return _inner.get();} 116 | 117 | private: 118 | PublicKey const _publicKey; 119 | kj::Own _inner; 120 | }; 121 | 122 | 123 | /// Utility function to get the human-readable IP address of the peer. 124 | std::string getPeerName(kj::AsyncIoStream& stream); 125 | } 126 | -------------------------------------------------------------------------------- /include/SecretStream.h: -------------------------------------------------------------------------------- 1 | // 2 | // SecretStream.h 3 | // 4 | // Copyright © 2022 Jens Alfke. All rights reserved. 5 | // 6 | 7 | #ifndef SecretStream_h 8 | #define SecretStream_h 9 | #include "SecretHandshake.h" 10 | 11 | #ifdef __cplusplus 12 | extern "C" { 13 | #endif 14 | 15 | 16 | typedef enum { 17 | Compact, ///< Less overhead, but message lengths are eavesdroppable. 18 | BoxStream ///< Scuttlebutt-compatible. More overhead, but msg lengths are encrypted. 19 | } SHSCryptoBoxProtocol; 20 | 21 | typedef enum { 22 | Success, ///< Encryption/decryption succeeded 23 | OutTooSmall, ///< The output's capacity is too small 24 | IncompleteInput, ///< Need more input data to decrypt 25 | CorruptData ///< The encrypted data is corrupted 26 | } SHSStatus; 27 | 28 | 29 | //-------- ENCRYPTION: 30 | 31 | /// Message-oriented encryption using keys & nonces from a Session. 32 | /// Encrypts a sequence of arbitrary-size blocks of data ("messages"), to be written to a stream. 33 | /// Each encrypted message is prefixed with its size and a MAC. 34 | /// The nonce is incremented after each message. 35 | typedef struct SHSEncryptoBox SHSEncryptoBox; 36 | 37 | /// Constructs an `SHSEncryptoBox` from the encryption key and nonce of a SHSSession. 38 | SHSEncryptoBox* SHSEncryptoBox_Create(const SHSSession *session, SHSCryptoBoxProtocol); 39 | 40 | void SHSEncryptoBox_Free(SHSEncryptoBox*); 41 | 42 | /// Returns the encrypted size of a message. (It will be somewhat larger than the input.) 43 | size_t SHSEncryptoBox_GetEncryptedSize(SHSEncryptoBox*, size_t inputSize); 44 | 45 | /// Encrypts an outgoing message, attaching the MAC and size. 46 | /// @note Currently the maximum size message is 65535 bytes. 47 | /// @param in The message to be sent. 48 | /// @param out Where to write the encrypted message. 49 | /// On entry `out.data` must be set and `out.size` must be the maximum capacity. 50 | /// On success, `out.size` will be set to the encrypted size. 51 | /// @return The status, either `Success` or `OutTooSmall`. 52 | SHSStatus SHSEncryptoBox_Encrypt(SHSEncryptoBox*, SHSInputBuffer in, SHSOutputBuffer* out); 53 | 54 | 55 | //-------- DECRYPTION: 56 | 57 | 58 | /// Message-oriented decryption using keys & nonces from a Session: 59 | /// Reads & decrypts _entire messages_ created by `EncryptoBox`, in the same order they were created. 60 | /// Reads the message size so it knows how big the full message is, and indicates whether the 61 | /// message is incomplete. 62 | typedef struct SHSDecryptoBox SHSDecryptoBox; 63 | 64 | typedef struct SHSPeekResult { 65 | SHSStatus status; 66 | size_t decryptedSize; 67 | size_t encryptedSize; 68 | } SHSPeekResult; 69 | 70 | /// Constructs an `SHSDecryptoBox` from the decryption key and nonce of a SHSSession. 71 | SHSDecryptoBox* SHSDecryptoBox_Create(const SHSSession *session, SHSCryptoBoxProtocol); 72 | 73 | void SHSDecryptoBox_Free(SHSDecryptoBox*); 74 | 75 | /// Returns the minimum number of input bytes needed for `SHSDecryptoBox_Peek` to succeed. 76 | size_t SHSDecryptoBox_MinPeekSize(SHSDecryptoBox*); 77 | 78 | /// Looks at the input data (which must start on a message boundary but can be incomplete) 79 | /// and returns the length of the encrypted and decrypted messages. 80 | /// The `status` value will be: 81 | /// - `Success` if the size is known; `encryptedSize` and `decryptedSize` will be accurate. 82 | /// - `IncompleteInput` if there's not enough input to determine the size; `decryptedSize` 83 | /// will be set to the length of input needed to determine it. 84 | /// - `CorruptData` if the input data is corrupted 85 | SHSPeekResult SHSDecryptoBox_Peek(SHSDecryptoBox*, SHSInputBuffer); 86 | 87 | /// Decrypts incoming data from the encrypted stream, reading the next message if it's 88 | /// completely available. This always reads one entire message, as passed to `encrypt` 89 | /// on the other end. 90 | /// 91 | /// If the input data is incomplete (doesn't contain the entire message), nothing is 92 | /// consumed and `IncompleteInput` is returned. 93 | /// 94 | /// If the output buffer is too small to hold the entire message, nothing is consumed 95 | /// and `OutTooSmall` is returned. You can then call `getDecryptedSize` to find out how big 96 | /// a buffer you need. 97 | /// 98 | /// If the input data contains more than just one message, the extra data is not 99 | /// consumed; `in.data` will be adjusted to point to the remaining data. 100 | /// 101 | /// After `Success` is returned, there could be another complete message remaining in the 102 | /// buffer, so you should call `decrypt` again (potentially multiple times.) 103 | /// 104 | /// @param in Data from the stream. On success, **this will be adjusted** to account for 105 | /// the bytes consumed: `data` will point to the first unread byte, and `size` 106 | /// will be set to the number of remaining bytes. 107 | /// @param out Where to write the decrypted message. 108 | /// On input, its `data` must be set, and `size` must be the maximum capacity. 109 | /// On success, its `size` will be set to the decrypted message's size. 110 | /// @return The status; see the description of the 'status' enum values. 111 | SHSStatus SHSDecryptoBox_Decrypt(SHSDecryptoBox*, SHSInputBuffer *in, SHSOutputBuffer *out); 112 | 113 | 114 | #ifdef __cplusplus 115 | } 116 | #endif 117 | 118 | #endif /* SecretStream_h */ 119 | -------------------------------------------------------------------------------- /include/shs.hh: -------------------------------------------------------------------------------- 1 | // 2 | // shs.hh 3 | // 4 | // Copyright © 2022 Jens Alfke. All rights reserved. 5 | // 6 | 7 | #pragma once 8 | #include "monocypher/encryption.hh" 9 | #include "monocypher/signatures.hh" 10 | #include "monocypher/ext/ed25519.hh" 11 | #include "monocypher/ext/sha256.hh" 12 | #include 13 | 14 | #ifndef SHS_SCUTTLEBUTT_COMPATIBLE 15 | /// If this is true, use XSalsa20 instead of XChaCha20, for compatibility with other 16 | /// implementations of SecretHandshake. 17 | #define SHS_SCUTTLEBUTT_COMPATIBLE 1 18 | #endif 19 | 20 | #if SHS_SCUTTLEBUTT_COMPATIBLE 21 | #include "monocypher/ext/xsalsa20.hh" 22 | #endif 23 | 24 | 25 | namespace snej::shs::impl { 26 | template using byte_array = monocypher::byte_array; 27 | 28 | // Types used by the handshake: 29 | 30 | using app_id = byte_array<32>; 31 | 32 | using signing_key = monocypher::signing_key; 33 | using public_key = monocypher::public_key; 34 | using key_pair = monocypher::key_pair; 35 | using signature = key_pair::signature; 36 | 37 | using key_exchange = monocypher::key_exchange; 38 | 39 | using ChallengeData = byte_array<64>; 40 | using ClientAuthData = byte_array<112>; 41 | using ServerAckData = byte_array<80>; 42 | 43 | using session_key = monocypher::secret_byte_array<32>; 44 | using nonce = byte_array<24>; 45 | 46 | #if SHS_SCUTTLEBUTT_COMPATIBLE 47 | using box_key = monocypher::session::encryption_key; 48 | #else 49 | using box_key = monocypher::session::key; 50 | #endif 51 | 52 | 53 | /// Low-level implementation of SecretHandshake crypto operations. 54 | /// You will probably prefer to use the API in SecretHandshake.hh! 55 | class handshake { 56 | public: 57 | /// Initialize with the app ID (which both peers must know), and your long-term Ed25519 58 | /// key-pair. 59 | /// If you're the client (initiator), you must next call `setServerPublicKey`. 60 | handshake(app_id const& appID, 61 | signing_key const& longTermSigningKey, 62 | public_key const& longTermPublicKey); 63 | 64 | /// Setting custom ephemeral keys is optional; typically only done by unit tests. 65 | void setEphemeralKeys(key_exchange const&); 66 | 67 | // The client must call these in order: 68 | 69 | void setServerPublicKey(public_key const&); 70 | ChallengeData createClientChallenge() {return createChallenge();} 71 | bool verifyServerChallenge(ChallengeData const& c) {return verifyChallenge(c);} 72 | ClientAuthData createClientAuth(); 73 | bool verifyServerAck(ServerAckData const&); 74 | 75 | // The server must call these in order: 76 | 77 | bool verifyClientChallenge(ChallengeData const& c) {return verifyChallenge(c);} 78 | ChallengeData createServerChallenge() {return createChallenge();} 79 | bool verifyClientAuth(ClientAuthData const&); 80 | ServerAckData createServerAck(); 81 | 82 | /// Returns the peer's public key. May be called after `verifyClientAuth` returns true. 83 | public_key const& getPeerPublicKey() const {return _Yp.value();} 84 | 85 | // Both client and server call this last, to get the session keys: 86 | void getOutcome(session_key &encryptionKey, 87 | nonce &encryptionNonce, 88 | session_key &decryptionKey, 89 | nonce &decryptionNonce, 90 | public_key &peerPublicKey); 91 | 92 | 93 | // optional non-denominational names: 94 | ChallengeData createChallenge(); 95 | bool verifyChallenge(ChallengeData const&); 96 | 97 | using kx_public_key = key_exchange::public_key; 98 | using kx_secret_key = key_exchange::secret_key; 99 | using kx_shared_secret = key_exchange::shared_secret; 100 | using sha256 = monocypher::ext::sha256; 101 | 102 | private: 103 | box_key clientAuthKey(); 104 | box_key serverAckKey(); 105 | 106 | // Input data. Here, 'x' means 'me' and 'y' means 'the peer'. 107 | app_id const _K; // Application ID 108 | signing_key _X; // My signing key (A or B) 109 | public_key _Xp; // My public key (Ap or Bp) 110 | key_exchange _x; // My ephemeral key-pair (a or b) 111 | kx_public_key _xp; // My ephemeral public key (ap or bp) 112 | 113 | // These get set as the challenge progresses: 114 | std::optional _Yp; // Peer's public key (Bp or Ap) 115 | std::optional _yp; // Peer's ephemeral public key (bp or ap) 116 | std::optional _ab; // _x * _yp, which is a·b on both sides 117 | std::optional _hashab; // SHA256(a·b) 118 | std::optional _aB; // a·Bp, which is also B·ap 119 | std::optional _Ab; // A·bp, which is also b·Ap 120 | std::optional _serverAckKey; // hash(K | a·b | a·B | A·b) 121 | std::optional> _H; // sign[A](K | Bp | hash(a·b)) | Ap 122 | }; 123 | 124 | } 125 | -------------------------------------------------------------------------------- /capnproto/SecretRPC.hh: -------------------------------------------------------------------------------- 1 | // 2 | // SecretRPC.hh 3 | // 4 | // Copyright © 2021 Jens Alfke. All rights reserved. 5 | // 6 | 7 | #pragma once 8 | #include "SecretConnection.hh" 9 | #include 10 | #include 11 | #include 12 | 13 | namespace snej::shs { 14 | 15 | /// Easy Cap'n Proto RPC server using the Secret Handshake protocol. 16 | class SecretRPCServer { 17 | public: 18 | /// Factory function for the server's main/root capability. 19 | /// This is called when a new client connection opens, and is passed the peer identity, 20 | /// from which you can get the peer's authenticated public key. (If secrecy is disabled for 21 | /// this server, this parameter will be nullptr.) 22 | /// You can either ignore the identity and return a singleton object, as with EzRpcServer, 23 | /// or you can create a new capability per connection and pass it the connection's public 24 | /// key, so it can authorize calls to its API. 25 | using MainInterfaceFactory = std::function; 26 | 27 | /// Initializes & starts the server, asynchronously. 28 | /// @param shsWrapper The server's SecretHandshake info, or empty to disable secrecy. 29 | /// @param mainInterface The root object to be served. 30 | /// @param bindAddress The address of the interface to bind to, or "*" for all interfaces. 31 | /// @param defaultPort The TCP port to listen on, or 0 to pick a random port. 32 | /// @param readerOpts Options for reading incoming serialized messages. 33 | SecretRPCServer(kj::Own shsWrapper, 34 | MainInterfaceFactory mainInterface, 35 | kj::StringPtr bindAddress, 36 | uint16_t defaultPort, 37 | capnp::ReaderOptions readerOpts); 38 | 39 | ~SecretRPCServer() noexcept(false); 40 | 41 | kj::Promise getPort(); 42 | 43 | kj::WaitScope& getWaitScope(); 44 | 45 | kj::AsyncIoProvider& getIoProvider(); 46 | 47 | kj::LowLevelAsyncIoProvider& getLowLevelIoProvider(); 48 | 49 | 50 | /// Constructor that doesn't open a listening socket. 51 | /// Instead, you have to call `acceptStream` to connect streams to it. Used for testing. 52 | SecretRPCServer(kj::Own shsContext, 53 | MainInterfaceFactory mainInterfaceFactory, 54 | capnp::ReaderOptions readerOpts); 55 | 56 | /// Connects a (promised) stream to the server, as though a client had connected. 57 | /// Used for testing. 58 | void acceptStream(kj::Promise streamPromise, 59 | capnp::ReaderOptions readerOpts = {}); 60 | 61 | private: 62 | struct Impl; 63 | kj::Own _impl; 64 | }; 65 | 66 | 67 | 68 | /// Easy Cap'n Proto RPC client using the Secret Handshake protocol. 69 | class SecretRPCClient { 70 | public: 71 | /// Initializes the client and connects asynchronously. 72 | /// @param shsWrapper The client's SecretHandshake info, or empty to disable secrecy. 73 | /// @param serverAddress The address to connect to. 74 | /// @param serverPort The TCP port to connect to. 75 | /// @param readerOpts RPC options controlling how data is read. 76 | SecretRPCClient(kj::Own shsWrapper, 77 | kj::StringPtr serverAddress, 78 | uint16_t serverPort, 79 | capnp::ReaderOptions readerOpts = {}); 80 | 81 | /// Initializes the client on a (promised) stream. 82 | /// @param shsWrapper The client's SecretHandshake info, or empty to disable secrecy. 83 | /// @param streamPromise The promised AsyncIoStream. 84 | /// @param readerOpts RPC options controlling how data is read. 85 | SecretRPCClient(kj::Own shsWrapper, 86 | kj::Promise> streamPromise, 87 | capnp::ReaderOptions readerOpts = {}); 88 | 89 | SecretRPCClient(SecretRPCClient &&other); 90 | 91 | ~SecretRPCClient() noexcept(false); 92 | 93 | template 94 | typename Type::Client getMain(); 95 | 96 | capnp::Capability::Client getMain(); 97 | 98 | kj::WaitScope& getWaitScope(); 99 | 100 | kj::AsyncIoProvider& getIoProvider(); 101 | 102 | kj::LowLevelAsyncIoProvider& getLowLevelIoProvider(); 103 | 104 | private: 105 | struct Impl; 106 | kj::Own _impl; 107 | }; 108 | 109 | 110 | 111 | template 112 | inline typename Type::Client SecretRPCClient::getMain() { 113 | return getMain().castAs(); 114 | } 115 | 116 | 117 | /** Automatically manages an AsyncIoContext per thread. 118 | Just call RPCContext::getThreadLocal. Hang onto the reference for as long as 119 | you need to do RPC stuff. */ 120 | class RPCContext: public kj::Refcounted { 121 | public: 122 | static kj::Own getThreadLocal(); 123 | 124 | kj::AsyncIoContext& getIoContext() {return ioContext;} 125 | kj::WaitScope& getWaitScope() {return ioContext.waitScope;} 126 | kj::AsyncIoProvider& getIoProvider() {return *ioContext.provider;} 127 | kj::LowLevelAsyncIoProvider& getLowLevelIoProvider() {return *ioContext.lowLevelProvider;} 128 | 129 | // do not call these 130 | RPCContext(); 131 | ~RPCContext() noexcept(false); 132 | 133 | private: 134 | kj::AsyncIoContext ioContext; 135 | }; 136 | } 137 | -------------------------------------------------------------------------------- /include/SecretHandshake.hh: -------------------------------------------------------------------------------- 1 | // 2 | // SecretHandshake.hh 3 | // 4 | // Copyright © 2021 Jens Alfke. All rights reserved. 5 | // 6 | 7 | #pragma once 8 | #include "SecretHandshakeTypes.hh" 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | namespace snej::shs { 16 | namespace impl { class handshake; } 17 | 18 | 19 | /// This is an idiomatic C++ wrapper implementation of the 20 | /// ["Secret Handshake"](https://github.com/auditdrivencrypto/secret-handshake) protocol. 21 | /// This allows a client and server, each with a longterm key-pair, to form a secure 22 | /// authenticated connection, with a session key for encrypting subsequent traffic. 23 | /// To connect, the client must already know the server's public key. 24 | 25 | 26 | /// Abstract base class of Secret Handshake protocol. 27 | /// Superclass of ClientHandshake and ServerHandshake. 28 | class Handshake { 29 | public: 30 | /// Returns the number of bytes the handshake wants to read. 31 | virtual size_t byteCountNeeded() =0; 32 | 33 | /// Returns the number of bytes the handshake wants to read, and a buffer to put them in. 34 | /// The returned size may be 0, if nothing needs to be read. 35 | std::pair bytesToRead(); 36 | 37 | /// Call this after all bytes have been copied into the buffer returned by `bytesToRead`. 38 | /// @return True if the data is valid, false if the handshake has failed. 39 | bool readCompleted(); 40 | 41 | /// Call this if the input stream was closed by the peer before requested bytes could be 42 | /// read. At this point the handshake has of course failed; calling this method will set 43 | /// the `error` property appropriately. 44 | void readFailed(); 45 | 46 | /// Alternative read API; use instead of `bytesToRead` and `readCompleted`. 47 | /// Call this when data is received from the peer. 48 | /// @param src The received data. 49 | /// @param count The number of bytes received. 50 | /// @return The number of bytes consumed. -1 on error. 51 | intptr_t receivedBytes(const void *src, size_t count); 52 | 53 | /// Returns the current bytes to send, as a pointer and length. 54 | /// Call after constructor, and after calling `receivedBytes`. 55 | /// The length will be 0 if there is nothing to send. 56 | std::pair bytesToSend(); 57 | 58 | /// Call this after fully sending the bytes returned by bytesToSend(). 59 | void sendCompleted(); 60 | 61 | /// Alternative sending API; use instead of `bytesToSend` and `sendCompleted`. 62 | /// Pass it a buffer and the buffer's size. It will copy any output to the buffer, and return the 63 | /// number of bytes to send. 64 | /// @param dst The buffer to copy the bytes to. 65 | /// @param maxCount The size of the buffer. 66 | /// @return The number of bytes written to the buffer. -1 on error. 67 | intptr_t copyBytesToSend(void *dst, size_t maxCount); 68 | 69 | enum Error { 70 | NoError, ///< No error yet 71 | ProtocolError, ///< The peer does not use SecretHandshake, or a different AppID. 72 | AuthError, ///< Server has different public key, or doesn't like the client's. 73 | }; 74 | 75 | /// Current error; if not None, the handshake has failed and you should close the socket. 76 | /// Call this after `receivedBytes` and `bytesToSend`. 77 | Error error() const {return _error;} 78 | 79 | /// Becomes true when the handshake is complete. 80 | /// Call this after `receivedBytes` and `bytesToSend`. 81 | bool finished() const {return _step == Finished;} 82 | 83 | /// After the handshake is finished, this returns the results to use for communication. 84 | Session session(); 85 | 86 | virtual ~Handshake(); 87 | 88 | protected: 89 | enum Step { 90 | Failed = 0, 91 | ClientChallenge, // start here 92 | ServerChallenge, 93 | ClientAuth, 94 | ServerAck, 95 | Finished 96 | }; 97 | 98 | explicit Handshake(Context const&); 99 | void nextStep(); 100 | void failed(); 101 | virtual bool _receivedBytes(const uint8_t*) =0; // process received bytes 102 | virtual void _fillOutputBuffer(std::vector&) =0;// Resize & fill vector with output 103 | 104 | Context _context; // App ID and local key-pair 105 | Step _step = ClientChallenge; // Current step in protocol, or Failed 106 | Error _error = NoError; // Current error 107 | std::unique_ptr _impl; // Crypto implementation object 108 | private: 109 | std::vector _inputBuffer; // Unread bytes 110 | std::vector _outputBuffer; // Unsent bytes 111 | }; 112 | 113 | 114 | 115 | /// Client (active) side of Secret Handshake protocol. 116 | class ClientHandshake final : public Handshake { 117 | public: 118 | /// Constructs a Client for making a connection to a Server. 119 | /// @param context The application ID and the client's key-pair. 120 | /// @param serverPublicKey The server's identity. If this is incorrect the handshake fails. 121 | ClientHandshake(Context const& context, 122 | PublicKey const& serverPublicKey); 123 | 124 | size_t byteCountNeeded() override; 125 | protected: 126 | bool _receivedBytes(const uint8_t *bytes) override; 127 | void _fillOutputBuffer(std::vector&) override; 128 | }; 129 | 130 | 131 | 132 | /// Server (passive) side of Secret Handshake protocol. 133 | class ServerHandshake final : public Handshake { 134 | public: 135 | /// Constructs a Server for accepting a connection from a Client. 136 | /// @param context The application ID and the server's key-pair. 137 | explicit ServerHandshake(Context const& context); 138 | 139 | using ClientAuthorizer = std::function; 140 | 141 | /// Registers a callback that determines whether a client should be allowed to connect. 142 | /// It takes the client public key as a parameter, and returns true to allow connection. 143 | void setClientAuthorizer(ClientAuthorizer a) {_clientAuth = std::move(a);} 144 | 145 | size_t byteCountNeeded() override; 146 | protected: 147 | bool _receivedBytes(const uint8_t *bytes) override; 148 | void _fillOutputBuffer(std::vector&) override; 149 | 150 | ClientAuthorizer _clientAuth; 151 | }; 152 | 153 | } 154 | -------------------------------------------------------------------------------- /include/SecretHandshake.h: -------------------------------------------------------------------------------- 1 | // 2 | // SecretHandshake.h 3 | // 4 | // Copyright © 2022 Jens Alfke. All rights reserved. 5 | // 6 | 7 | #pragma once 8 | #ifndef SecretHandshake_h 9 | #define SecretHandshake_h 10 | #include 11 | #include 12 | #include 13 | 14 | #ifdef __cplusplus 15 | extern "C" { 16 | #endif 17 | 18 | 19 | typedef struct { uint8_t bytes[32]; } SHSAppID; ///< Arbitrary ID for your protocol 20 | typedef struct { uint8_t bytes[32]; } SHSPublicKey; ///< A 256-bit Ed25519 public key. 21 | typedef struct { uint8_t bytes[32]; } SHSSigningKey; ///< A 256-bit Ed25519 private key. 22 | typedef struct { uint8_t bytes[32]; } SHSSessionKey; ///< A 256-bit random value for use as a key. 23 | typedef struct { uint8_t bytes[24]; } SHSNonce; ///< A 192-bit random value for use as a nonce. 24 | 25 | void SHSErase(void *dst, size_t size); 26 | 27 | #define SHSKey_Erase(KEY) SHSErase(&(KEY).bytes, sizeof(KEY)) 28 | 29 | static inline void SHSSigningKey_Erase(SHSSigningKey *key) {SHSKey_Erase(*key);} 30 | 31 | typedef struct { 32 | SHSSigningKey signingKey; 33 | SHSPublicKey publicKey; 34 | } SHSKeyPair; 35 | 36 | /// Randomly generates a new key pair. 37 | SHSKeyPair SHSKeyPair_Generate(void); 38 | 39 | /// Reconstructs the public key of a KeyPair given the private signing key. 40 | SHSKeyPair SHSKeyPair_Regenerate(const SHSSigningKey*); 41 | 42 | /// Securely erases the private key of a key-pair. 43 | static inline void SHSKeyPair_Erase(SHSKeyPair *kp) {SHSKey_Erase(kp->signingKey);} 44 | 45 | 46 | /// Initializes an AppID by copying up to the first 32 bytes of a string to it; 47 | /// if the string is shorter than 32 bytes, the rest is zeroed. 48 | SHSAppID SHSAppID_FromString(const char* str); 49 | 50 | 51 | /// Result of the secret handshake: 52 | /// * session encryption / decryption keys with nonces, 53 | /// * and the peer's long-term public key (which is news to the server, but not to the client.) 54 | /// You can use the keys and nonces with whatever symmetric cipher you want; they're effectively 55 | /// random data that happens to be known by both you and the peer after a successful handshake. 56 | typedef struct { 57 | SHSSessionKey encryptionKey; ///< The session encryption key 58 | SHSNonce encryptionNonce; ///< Nonce to use with the encryption key 59 | SHSSessionKey decryptionKey; ///< The session decryption key 60 | SHSNonce decryptionNonce; ///< Nonce to use with the decryption key 61 | SHSPublicKey peerPublicKey; ///< The peer's authenticated public key 62 | } SHSSession; 63 | 64 | /// Securely erases the memory occupied by a SHSSession. Call this when finished with it. 65 | static inline void SHSSession_Erase(SHSSession *s) {SHSErase(s, sizeof(*s));} 66 | 67 | 68 | /// Points to immutable data to be sent, encrypted or decrypted. 69 | typedef struct { 70 | const void* src; ///< The bytes received 71 | size_t size; ///< Number of bytes received 72 | } SHSInputBuffer; 73 | 74 | /// Points to a mutable buffer for data received, encrypted or decrypted to be written to. 75 | typedef struct { 76 | void* dst; ///< Where to write the bytes to send 77 | size_t size; ///< Number of bytes available at `dst` 78 | } SHSOutputBuffer; 79 | 80 | typedef enum { 81 | SHSNoError, ///< No error yet 82 | SHSProtocolError, ///< The peer does not use SecretHandshake, or a different AppID. 83 | SHSAuthError, ///< Server has different public key, or doesn't like the client's. 84 | } SHSError; 85 | 86 | 87 | /// Opaque reference to an object that runs the SecretHandshake protocol. 88 | typedef struct SHSHandshake SHSHandshake; 89 | 90 | /// Constructs a client-side handshake for making a connection to a server. 91 | /// @param appID The application ID. Both server and client must use the same appID. 92 | /// @param keyPair The client's public and private key. 93 | /// @param serverPublicKey The server's identity. If this is incorrect the handshake fails. 94 | SHSHandshake* SHSHandshake_CreateClient(const SHSAppID *appID, 95 | const SHSKeyPair *keyPair, 96 | const SHSPublicKey *serverPublicKey); 97 | 98 | /// Constructs a server-side handshake for accepting a connection from a client. 99 | /// @param appID The application ID. Both server and client must use the same appID. 100 | /// @param keyPair The server's public and private key. 101 | SHSHandshake* SHSHandshake_CreateServer(const SHSAppID *appID, 102 | const SHSKeyPair *keyPair); 103 | 104 | /// Frees memory allocated by the handshake, and securely erases private keys. 105 | void SHSHandshake_Free(SHSHandshake*); 106 | 107 | /// Returns the number of bytes the handshake wants to read. May be 0. 108 | size_t SHSHandshake_GetBytesNeeded(SHSHandshake*); 109 | 110 | /// Returns a buffer to copy bytes received to. Its size is `GetBytesNeeded`. 111 | void* SHSHandshake_GetInputBuffer(SHSHandshake*); 112 | 113 | static inline SHSOutputBuffer SHSHandshake_GetBytesToRead(SHSHandshake *h) { 114 | SHSOutputBuffer buf = {SHSHandshake_GetInputBuffer(h), SHSHandshake_GetBytesNeeded(h)}; 115 | return buf; 116 | } 117 | 118 | /// Call this after all bytes have been copied into the buffer returned by `GetInputBuffer`. 119 | /// @return True if the data is valid, false if the handshake has failed. 120 | bool SHSHandshake_ReadCompleted(SHSHandshake*); 121 | 122 | /// Alternative read API; use instead of `GetInputBuffer`. 123 | /// Call this when data is received from the peer. 124 | /// @param src The received data. 125 | /// @param count The number of bytes received. 126 | /// @return The number of bytes consumed by the protocol, or -1 on error. 127 | intptr_t SHSHandshake_ReceivedBytes(SHSHandshake*, const void *src, size_t count); 128 | 129 | /// Returns the current bytes to send, as a pointer and length. 130 | /// Call after creating the handshake, and after calling `receivedBytes`. 131 | /// The length will be 0 if there is nothing to send. 132 | SHSInputBuffer SHSHandshake_GetBytesToSend(SHSHandshake*); 133 | 134 | /// Call this after fully sending the bytes returned by bytesToSend(). 135 | void SHSHandshake_SendCompleted(SHSHandshake*); 136 | 137 | /// Alternative sending API; use instead of `GetBytesToSend` and `SendCompleted`. 138 | /// Pass it a buffer and the buffer's size. It will copy any output to the buffer, and return the 139 | /// number of bytes to send. 140 | /// @param dst The buffer to copy the bytes to. 141 | /// @param capacity The number of bytes that can be written to your buffer. 142 | /// @return The number of bytes written to the buffer. -1 on error. 143 | intptr_t SHSHandshake_CopyBytesToSend(SHSHandshake*, void *dst, size_t capacity); 144 | 145 | /// The current error. (If not `SHSNoError`, you should close the socket.) 146 | /// Check this after sending and receiving data. 147 | SHSError SHSHandshake_GetError(SHSHandshake*); 148 | 149 | /// True if the handshake is complete and successful. 150 | /// Check this after sending and receiving data. 151 | bool SHSHandshake_Finished(SHSHandshake*); 152 | 153 | /// After the handshake is finished, this returns the results to use for communication. 154 | SHSSession SHSHandshake_GetSession(SHSHandshake*); 155 | 156 | #ifdef __cplusplus 157 | } 158 | #endif 159 | 160 | #endif /* SecretHandshake_h */ 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SecretHandshake Protocol Implementation In C++ 2 | 3 | This is a C++ implementation of the [SecretHandshake](https://github.com/auditdrivencrypto/secret-handshake) protocol. SecretHandshake upgrades your network connections with encryption and _mutual_ authentication, without all the overhead of using TLS. 4 | 5 | An (incomplete) C API is provided, for the use of clients written in C and for binding to other languages. 6 | 7 | There is also some glue code to use SecretHandshake with the [capnproto](capnproto/README.md) and [Crouton](crouton/README.md) networking libraries. 8 | 9 | ## 1. About SecretHandshake 10 | 11 | **SecretHandshake** is “a mutually authenticating key agreement handshake, with forward secure identity metadata.” It was designed by Dominic Tarr and is used in the [Secure Scuttlebutt](https://scuttlebutt.nz) P2P social network. There’s a [design paper](http://dominictarr.github.io/secret-handshake-paper/shs.pdf) that presents the algorithm in detail. 12 | 13 | It’s based on 256-bit Ed25519 key-pairs. Each peer needs to maintain a long-term key pair, whose public key serves as its global identifier. The peer making the connection (the “client”) must know the public key of the other peer (“server”) to be able to connect. 14 | 15 | The handshake happens when the socket opens. The peers alternate sending and receiving four cryptographic blobs of about 100 bytes each. The server learns the client’s public key during the handshake, and each peer proves to the other that it knows its matching private key. 16 | 17 | The handshake also produces two 256-bit session keys and 192-bit nonces, known to both peers but otherwise secret, which are then used to encrypt the two TCP streams. (This is not strictly speaking part of the SecretHandshake protocol, which ends after key agreement.) 18 | 19 | The API in `SecretStream.hh` provides stream encryption using those keys. It supports both Scuttlebutt's "box-stream" protocol based on XSalsa20, and a more compact custom protocol using XChaCha20. 20 | 21 | The crypto primitives themselves come from [Monocypher](https://monocypher.org), a small C crypto library, as wrapped by my own [MonocypherCpp](https://github.com/snej/monocypher-cpp) C++ API. 22 | 23 | ## 3. Building The Library 24 | 25 | *Make sure to check out submodules. Recursively. Otherwise you will get mucho build errors.* 26 | 27 | A simple CMake build file is supplied. Or you can use your own build system: just compile the files in `src` and `vendor/monocypher-cpp/src`, and add `include` and `vendor/monocypher/include` to the preprocessor's header path. 28 | 29 | There are some unit tests in `SecretHandshakeTests.cc`. They use the [Catch2](https://github.com/catchorg/Catch2) unit test framework. Some of the tests use an existing C implementation of SecretHandshake for validation; that code in turn requires libSodium, so to run the tests you'll need to [install libSodium](https://libsodium.gitbook.io/doc/installation) and make sure it’s in the system header search path. But that's not necessary if you only want to build the library. 30 | 31 | ## 4. Using SecretHandshake 32 | 33 | *None of the code here implements networking!* It expects you to open sockets, and tell it the data you read; it will tell you what to send, and whether the handshake succeeded or failed. 34 | 35 | ### Ahead of time 36 | 37 | Come up with an “AppID”, an arbitrary 32-byte value specific to your own application protocol, which will be known to both clients and servers. It’s most convenient to treat this as a zero-padded string, like “MyGame v1”. `Context::appIDFromString()` will convert a C string to a binary AppID struct. 38 | 39 | Usually the AppID is hardcoded into both the server and the client. 40 | 41 | The purpose of the AppID is to prevent accidentally connecting to a different application that also happens to use SecretHandshake but runs an incompatible protocol after the handshake. 42 | 43 | ### Steps to run a server/listener 44 | 45 | 1. The first time the server starts up, call `KeyPair::generate()` to create a key-pair for it. Save the private signing key in a secure place, like the Keychain on mac/iOS. 46 | 2. Log or otherwise display the public key: clients will need to know it in order to connect. You may want to construct a URL containing the key in base64 encoding. 47 | 3. Listen for TCP connections. 48 | 4. After accepting a TCP connection, create a `ServerHandshake` instance for the connection. 49 | 5. Proceed to “**The handshake**”, below. 50 | 51 | ### Steps to run a client 52 | 53 | 1. The first time the client launches, call `KeyPair::generate()` to create a key-pair for the user. Save the private signing key in a secure place, like the Keychain on mac/iOS. 54 | 2. Open a TCP connection to the server’s address & port. 55 | 3. Create a `ClientHandshake` instance for the connection. You’ll need to know the *server’s* public key. 56 | 4. Proceed to “**The handshake**”, below… 57 | 58 | ### The handshake 59 | 60 | 1. Call `handshake.bytesToSend()`. If it returns a non-zero byte count then: 61 | 1. send those bytes over the socket. 62 | 2. Call `handshake.sendCompleted()`. 63 | 2. Else: 64 | 1. Call handshake.bytesToReceive(), which returns a byte count and a buffer pointer. 65 | 2. Wait to receive that many bytes, and copy them into the buffer. (Or if the peer drops the connection, the handshake of course fails.) 66 | 3. Call `handshake.readCompleted()`. 67 | 3. If `handshake.failed()` is true, exit the loop and close the socket. 68 | 4. If `handshake.finished()` is false, start the loop over again… 69 | 70 | The actual traffic you’ll see during a successful handshake is: 71 | 72 | * Client sends 64 bytes 73 | * Server reads those, then sends 64 bytes 74 | * Client reads those, then sends 112 bytes 75 | * Server reads those, then sends 80 bytes; then the server’s handshake is finished. 76 | * Client reads those, then its handshake is finished. 77 | 78 | ### After a successful handshake 79 | 80 | 1. Call `handshake.session()`. The returned `Session` struct contains the symmetric session keys and nonces. 81 | - If you’re the server, the Session also contains the client’s authenticated public key, which you can use as a persistent identifier instead of requiring a login. If your server only allows registered users to connect, you should close the socket now if the key isn’t known. 82 | 2. You can now use `CryptoBox` or `CryptoStream` to send and receive encrypted data over the socket; consult the documentation comments in SecretStream.hh for details. Or you can use whatever other symmetric encryption you want: the keys and nonces in the Session are just random secrets known to both client and server. 83 | 84 | ### After a failed handshake 85 | 86 | Just close the socket. If you’re the client, report that the connection failed. 87 | 88 | There are many reasons a client connection can fail: 89 | 90 | - The server you connected to doesn’t actually use SecretHandshake 91 | - The server’s appID doesn’t match yours, i.e. it’s for a different application 92 | - The server’s public key isn’t the one you expected 93 | - The server wasn’t able to send a valid signature proving it owns the matching private key 94 | - The server only allows known users to connect, and it didn’t recognize your public key 95 | - The server sent invalid data, maybe because of a man-in-the-middle attack, maybe just a rare network glitch 96 | 97 | The library doesn’t currently let you distinguish between these, so all you can do is tell the user that the connection failed. 98 | 99 | ## 5. Status 100 | 101 | I’ve been using this code since February 2022. It works correctly in an app I’m developing, and has basic unit tests, including a test that the network data it sends is identical to that of an established SecretHandshake implementation. But it has not been used in released software, and hasn’t gone through an audit. 102 | 103 | It builds with Clang 12+ and recent GCC, and is run & tested on macOS and Ubuntu by Github CI. 104 | 105 | ## 6. License 106 | 107 | The code in this repo is provided under the MIT license. 108 | 109 | Monocypher uses the 2-clause BSD license. 110 | 111 | (The code in the `shs-1` submodule is LGPL-licensed, but since it is only used in the tests (`shsTests.cc`) it has no effect on the licensing of the library itself.) 112 | -------------------------------------------------------------------------------- /tests/shsTests.cc: -------------------------------------------------------------------------------- 1 | // 2 | // shsTests.cc 3 | // 4 | // Copyright © 2022 Jens Alfke. All rights reserved. 5 | // 6 | // Licensed under the Apache License, Version 2.0 (the "License"); 7 | // you may not use this file except in compliance with the License. 8 | // You may obtain a copy of the License at 9 | // 10 | // http://www.apache.org/licenses/LICENSE-2.0 11 | // 12 | // Unless required by applicable law or agreed to in writing, software 13 | // distributed under the License is distributed on an "AS IS" BASIS, 14 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | // See the License for the specific language governing permissions and 16 | // limitations under the License. 17 | // 18 | 19 | #include "shs.hh" 20 | #include "hexString.hh" 21 | #include 22 | #include "catch.hpp" // https://github.com/catchorg/Catch2 23 | 24 | // `SHS_TESTS_COMPARE_C` controls whether to test that shs.cc produces the same results as an 25 | // existing implementation, shs.c. It's enabled if predefined to 1, or if we can determine that 26 | // libSodium is installed (since shs.c requires libSodium.) 27 | #if !defined(SHS_TESTS_COMPARE_C) 28 | # define SHS_TESTS_COMPARE_C 0 29 | # if defined(__has_include) 30 | # if __has_include() 31 | # undef SHS_TESTS_COMPARE_C 32 | # define SHS_TESTS_COMPARE_C 1 33 | # endif 34 | # endif 35 | #endif 36 | 37 | #if SHS_TESTS_COMPARE_C 38 | extern "C" { 39 | #include "shs1.h" 40 | } 41 | #endif 42 | 43 | 44 | using namespace std; 45 | using namespace snej::shs::impl; 46 | 47 | 48 | TEST_CASE("shs Cpp vs C", "[SecretHandshake]") { 49 | key_pair clientKP = key_pair::generate(), serverKP = key_pair::generate(); 50 | signing_key clientSK = clientKP.get_seed(), serverSK = serverKP.get_seed(); 51 | public_key clientPK = clientKP.get_public_key(), serverPK = serverKP.get_public_key(); 52 | key_exchange clientEph, serverEph; 53 | 54 | #if SHS_TESTS_COMPARE_C 55 | REQUIRE(::sodium_init() == 0); 56 | uint8_t clientKeyPair[64]; 57 | uint8_t serverKeyPair[64]; 58 | memcpy(&clientKeyPair[0], clientSK.data(), 32); 59 | memcpy(&clientKeyPair[32], clientPK.data(), 32); 60 | memcpy(&serverKeyPair[0], serverSK.data(), 32); 61 | memcpy(&serverKeyPair[32], serverPK.data(), 32); 62 | #else 63 | cout << "** NOTE: Not comparing with shs-1 C implementation **\n"; 64 | #endif 65 | 66 | app_id appID; 67 | strcpy((char*)&appID, "shsTests"); 68 | 69 | cout << "Client pub key: " << hexString(clientPK) << endl; 70 | cout << "Client sec key: " << hexString(clientSK) << endl; 71 | cout << "Client eph pub key: " << hexString(clientEph.get_public_key()) << endl; 72 | cout << "Client eph sec key: " << hexString(clientEph.get_secret_key()) << endl; 73 | cout << "Server pub key: " << hexString(serverPK) << endl; 74 | cout << "Server sec key: " << hexString(serverSK) << endl; 75 | cout << "Server eph pub key: " << hexString(serverEph.get_public_key()) << endl; 76 | cout << "Server eph sec key: " << hexString(serverEph.get_secret_key()) << endl; 77 | 78 | 79 | handshake client(appID, clientSK, clientPK); 80 | client.setServerPublicKey(serverPK); 81 | client.setEphemeralKeys(clientEph); 82 | handshake server(appID, serverSK, serverPK); 83 | server.setEphemeralKeys(serverEph); 84 | 85 | #if SHS_TESTS_COMPARE_C 86 | auto clientEphPub = clientEph.get_public_key(), serverEphPub = serverEph.get_public_key(); 87 | auto clientEphSec = clientEph.get_secret_key(), serverEphSec = serverEph.get_secret_key(); 88 | SHS1_Client cClient; 89 | shs1_init_client(&cClient, appID.data(), clientPK.data(), clientKeyPair, 90 | clientEphPub.data(), clientEphSec.data(), serverPK.data()); 91 | SHS1_Server cServer; 92 | shs1_init_server(&cServer, appID.data(), serverPK.data(), serverKeyPair, 93 | serverEphPub.data(), serverEphSec.data()); 94 | #endif 95 | 96 | cout << "\n1. Client Challenge\n"; 97 | ChallengeData clientCh; 98 | clientCh = client.createChallenge(); 99 | cout << "C++ client challenge: " << hexString(clientCh) << endl; 100 | #if SHS_TESTS_COMPARE_C 101 | ChallengeData cClientCh; 102 | shs1_create_client_challenge(cClientCh.data(), &cClient); 103 | cout << "C client challenge: " << hexString(cClientCh) << endl; 104 | CHECK(clientCh == cClientCh); 105 | #endif 106 | 107 | cout << "\n2. Verify Client Challenge\n"; 108 | REQUIRE(server.verifyChallenge(clientCh)); 109 | #if SHS_TESTS_COMPARE_C 110 | REQUIRE(shs1_verify_client_challenge(cClientCh.data(), &cServer)); 111 | #endif 112 | 113 | cout << "\n3. Server Challenge\n"; 114 | ChallengeData serverCh; 115 | serverCh = server.createChallenge(); 116 | cout << "C++ server challenge: " << hexString(serverCh) << endl; 117 | #if SHS_TESTS_COMPARE_C 118 | ChallengeData cServerCh; 119 | shs1_create_server_challenge(cServerCh.data(), &cServer); 120 | cout << "C server challenge: " << hexString(cServerCh) << endl; 121 | CHECK(serverCh == cServerCh); 122 | #endif 123 | 124 | cout << "\n4. Verify Server Challenge\n"; 125 | REQUIRE(client.verifyChallenge(serverCh)); 126 | #if SHS_TESTS_COMPARE_C 127 | REQUIRE(shs1_verify_server_challenge(cServerCh.data(), &cClient)); 128 | #endif 129 | 130 | cout << "\n4. Client Auth\n"; 131 | ClientAuthData clientAuth; 132 | clientAuth = client.createClientAuth(); 133 | cout << "C++ client auth: " << hexString(clientAuth) << endl; 134 | #if SHS_TESTS_COMPARE_C 135 | ClientAuthData cClientAuth; 136 | shs1_create_client_auth(cClientAuth.data(), &cClient); 137 | cout << "C client auth: " << hexString(cClientAuth) << endl; 138 | CHECK(clientAuth == cClientAuth); 139 | #endif 140 | 141 | cout << "\n4. Verify Client Auth\n"; 142 | REQUIRE(server.verifyClientAuth(clientAuth)); 143 | #if SHS_TESTS_COMPARE_C 144 | REQUIRE(shs1_verify_client_auth(cClientAuth.data(), &cServer)); 145 | #endif 146 | 147 | cout << "\n5. Server Ack\n"; 148 | ServerAckData serverAck; 149 | serverAck = server.createServerAck(); 150 | cout << "C++ server ack: " << hexString(serverAck) << endl; 151 | #if SHS_TESTS_COMPARE_C 152 | ServerAckData cServerAck; 153 | shs1_create_server_ack(cServerAck.data(), &cServer); 154 | cout << "C server ack: " << hexString(cServerAck) << endl; 155 | CHECK(serverAck == cServerAck); 156 | #endif 157 | 158 | cout << "\n5. Verify Server Ack\n"; 159 | REQUIRE(client.verifyServerAck(serverAck)); 160 | #if SHS_TESTS_COMPARE_C 161 | REQUIRE(shs1_verify_server_ack(cServerAck.data(), &cClient)); 162 | #endif 163 | 164 | cout << "\n6. Outcomes\n"; 165 | session_key clientEncKey, clientDecKey, serverEncKey, serverDecKey; 166 | nonce clientEncNonce, clientDecNonce, serverEncNonce, serverDecNonce; 167 | public_key clientPeerPubKey, serverPeerPubKey; 168 | 169 | client.getOutcome(clientEncKey, clientEncNonce, clientDecKey, clientDecNonce, clientPeerPubKey); 170 | server.getOutcome(serverEncKey, serverEncNonce, serverDecKey, serverDecNonce, serverPeerPubKey); 171 | 172 | cout << "C++ client enc key: " << hexString(clientEncKey) << endl; 173 | cout << "C++ server dec key: " << hexString(serverDecKey) << endl; 174 | cout << "C++ client enc non: " << hexString(clientEncNonce) << endl; 175 | cout << "C++ server dec non: " << hexString(serverDecNonce) << endl; 176 | cout << "C++ client dec key: " << hexString(clientDecKey) << endl; 177 | cout << "C++ server enc key: " << hexString(serverEncKey) << endl; 178 | cout << "C++ client dec non: " << hexString(clientDecNonce) << endl; 179 | cout << "C++ server enc non: " << hexString(serverEncNonce) << endl; 180 | 181 | #if SHS_TESTS_COMPARE_C 182 | SHS1_Outcome cClientOut, cServerOut; 183 | shs1_client_outcome(&cClientOut, &cClient); 184 | shs1_server_outcome(&cServerOut, &cServer); 185 | 186 | auto &cClientEncKey = *(session_key*)cClientOut.encryption_key; 187 | auto &cClientDecKey = *(session_key*)cClientOut.decryption_key; 188 | auto &cClientEncNonce = *(nonce*)cClientOut.encryption_nonce; 189 | auto &cClientDecNonce = *(nonce*)cClientOut.decryption_nonce; 190 | 191 | cout << endl; 192 | cout << "C client enc key: " << hexString(cClientEncKey) << endl; 193 | cout << "C client enc non: " << hexString(cClientEncNonce) << endl; 194 | cout << "C client dec key: " << hexString(cClientDecKey) << endl; 195 | cout << "C client dec non: " << hexString(cClientDecNonce) << endl; 196 | 197 | CHECK(clientEncKey == cClientEncKey); 198 | CHECK(clientEncNonce == cClientEncNonce); 199 | CHECK(clientDecKey == cClientDecKey); 200 | CHECK(clientDecNonce == cClientDecNonce); 201 | #endif 202 | 203 | REQUIRE(clientEncKey == serverDecKey); 204 | REQUIRE(clientEncNonce == serverDecNonce); 205 | REQUIRE(clientDecKey == serverEncKey); 206 | REQUIRE(clientDecNonce == serverEncNonce); 207 | REQUIRE(clientPK == serverPeerPubKey); 208 | } 209 | -------------------------------------------------------------------------------- /src/shs.cc: -------------------------------------------------------------------------------- 1 | // 2 | // shs.cc 3 | // 4 | // Copyright © 2022 Jens Alfke. All rights reserved. 5 | // 6 | // Licensed under the MIT License: 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | #include "shs.hh" 28 | #include "monocypher/ext/sha512.hh" 29 | 30 | /* Follow along with CheatSheet.md! The variable names here follow the same terminology. */ 31 | 32 | 33 | #define _UNUSED 34 | #ifdef __has_attribute 35 | # if __has_attribute(unused) 36 | # undef _UNUSED 37 | # define _UNUSED __attribute__((unused)) 38 | # endif 39 | #endif 40 | 41 | 42 | namespace snej::shs::impl { 43 | using namespace std; 44 | 45 | using input_bytes = monocypher::input_bytes; 46 | using kx_shared_secret = handshake::kx_shared_secret; 47 | using kx_public_key = handshake::kx_public_key; 48 | 49 | 50 | // Algorithms, named as in the mathematical description: 51 | 52 | static monocypher::ext::sha256 hash(input_bytes in) { 53 | return monocypher::ext::sha256::create(in); 54 | } 55 | 56 | struct sha512256 : public byte_array<32> { }; 57 | 58 | static inline sha512256 hmac(byte_array<32> const& key, input_bytes in) { 59 | // (HMAC-SHA-512-256 is just the first 256 bits of HMAC-SHA-512.) 60 | auto h = monocypher::hash::createMAC(in, key); 61 | return reinterpret_cast(h.range<0, 32>()); 62 | } 63 | 64 | static inline nonce hashToNonce(sha512256 const& h) { 65 | return h.range<0, sizeof(nonce)>(); 66 | } 67 | 68 | static inline box_key makeBoxKey(input_bytes keyMaterial) { 69 | // The algorithm generates crypto-box keys by running the key material through SHA-256. 70 | return box_key(hash(keyMaterial)); 71 | } 72 | 73 | template 74 | byte_array box(box_key const& key, byte_array const& plaintext) { 75 | // This hardcodes an all-zeroes nonce, which is only safe because the protocol uses each 76 | // key only once! 77 | return key.box(monocypher::session::nonce(0), plaintext); 78 | } 79 | 80 | template 81 | optional> unbox(box_key const& key, 82 | byte_array const& ciphertext) { 83 | byte_array output; 84 | if (!key.unbox(monocypher::session::nonce(0), ciphertext, output)) 85 | return nullopt; 86 | return output; 87 | } 88 | 89 | 90 | // Overload `*` for Curve25519 scalar multiplication with Ed25519 keys: 91 | static inline kx_shared_secret operator* (signing_key const& k, kx_public_key const& pk) { 92 | return k.as_key_exchange() * pk; 93 | } 94 | 95 | static inline kx_shared_secret operator* (key_exchange const& k, public_key const& pk) { 96 | return k * pk.for_key_exchange(); 97 | } 98 | 99 | 100 | #pragma mark - COMMON CODE: 101 | 102 | 103 | handshake::handshake(app_id const& appID, 104 | signing_key const& signingKey, 105 | public_key const& publicKey) 106 | :_K(appID) 107 | ,_X(signingKey) 108 | ,_Xp(publicKey) 109 | ,_x() // randomized 110 | ,_xp(_x.get_public_key()) 111 | { } 112 | 113 | 114 | void handshake::setEphemeralKeys(key_exchange const& kx) { 115 | _x = kx; 116 | _xp = _x.get_public_key(); 117 | } 118 | 119 | 120 | // hmac[K](xp) | xp 121 | ChallengeData handshake::createChallenge() { 122 | return hmac(_K, _xp) | _xp; 123 | } 124 | 125 | 126 | // hmac[K](yp) | yp 127 | bool handshake::verifyChallenge(ChallengeData const& challenge) { 128 | // Unpack hmac[K](yp) and yp: 129 | auto &challengeHmac = challenge.range<0, sizeof(sha512256)>(); 130 | auto &challengePubKey = challenge.range(); 131 | // Verify hmac: 132 | if (challengeHmac != hmac(_K, challengePubKey)) 133 | return false; 134 | // Now we know yp, the peer's ephemeral public key: 135 | _yp = kx_public_key(challengePubKey); 136 | _ab = _x * *_yp; 137 | _hashab = hash(*_ab); 138 | return true; 139 | } 140 | 141 | 142 | void handshake::getOutcome(session_key & encryptionKey, 143 | nonce & encryptionNonce, 144 | session_key & decryptionKey, 145 | nonce & decryptionNonce, 146 | public_key & peerPublicKey) 147 | { 148 | auto boxKeyHash = hash(_serverAckKey.value()); 149 | // hash(hash(hash(K | a_s * b_p | a_s * B_p | A_s * b_p)) | Y_p): 150 | encryptionKey = session_key(hash(boxKeyHash | _Yp.value())); 151 | // hmac_{K}(b_p): 152 | encryptionNonce = hashToNonce(hmac(_K, _yp.value())); 153 | // hash(hash(hash(K | a_s * b_p | a_s * B_p | A_s * b_p)) | X_p): 154 | decryptionKey = session_key(hash(boxKeyHash | _Xp)); 155 | // hmac_{K}(x_p): 156 | decryptionNonce = hashToNonce(hmac(_K, _xp)); 157 | peerPublicKey = _Yp.value(); 158 | } 159 | 160 | 161 | box_key handshake::clientAuthKey() { // [K | a·b | a·B] 162 | return makeBoxKey(_K | *_ab | *_aB); 163 | } 164 | 165 | 166 | box_key handshake::serverAckKey() { // [K | a·b | a·B | A·b] 167 | _serverAckKey = makeBoxKey(_K | _ab.value() | _aB.value() | _Ab.value()); 168 | return *_serverAckKey; 169 | } 170 | 171 | 172 | 173 | #pragma mark - CLIENT: 174 | 175 | 176 | #define WITH_CLIENT_VARS \ 177 | _UNUSED auto &A = _X;\ 178 | _UNUSED auto &Ap = _Xp;\ 179 | _UNUSED auto &Bp = _Yp.value();\ 180 | _UNUSED auto &a = _x;\ 181 | _UNUSED auto &ap = _xp;\ 182 | _UNUSED auto &bp = _yp.value(); 183 | 184 | 185 | void handshake::setServerPublicKey(const public_key &pk) { 186 | _Yp = pk; 187 | } 188 | 189 | 190 | // box[K | a·b | a·B](H) 191 | ClientAuthData handshake::createClientAuth() { 192 | WITH_CLIENT_VARS 193 | // Compute H = sign[A](K | Bp | hash(a·b)) | Ap 194 | _H = A.sign(_K | Bp | _hashab.value()) | Ap; 195 | // Return box[K | a·b | a·B](H) 196 | _Ab = A * bp; 197 | _aB = a * Bp; 198 | auto key = clientAuthKey(); 199 | return box(key, *_H); 200 | } 201 | 202 | 203 | // ack = box[K | a·b | a·B | A·b](sign[B](K | H | hash(a·b))) 204 | bool handshake::verifyServerAck(ServerAckData const& ack) { 205 | WITH_CLIENT_VARS 206 | // Unbox, producing the signature. 207 | // Then verify it's the true signature of K | H | hash(a·b). 208 | if (auto sig = unbox(serverAckKey(), ack)) 209 | return Bp.check((signature&)*sig, (_K | _H.value() | _hashab.value())); 210 | else 211 | return false; 212 | } 213 | 214 | 215 | #pragma mark - SERVER: 216 | 217 | 218 | #define WITH_SERVER_VARS \ 219 | _UNUSED auto &B = _X;\ 220 | _UNUSED auto &Bp = _Xp;\ 221 | _UNUSED auto &Ap = _Yp;\ 222 | _UNUSED auto &b = _x;\ 223 | _UNUSED auto &bp = _xp;\ 224 | _UNUSED auto &ap = _yp.value();\ 225 | 226 | 227 | // auth = box[K | a·b | a·B](H) ... where H = sign[A](K | Bp | hash(a·b)) | Ap 228 | bool handshake::verifyClientAuth(ClientAuthData const& auth) { 229 | WITH_SERVER_VARS 230 | _aB = B * ap; // because a·Bp == ap·B == B·ap 231 | _H = unbox(clientAuthKey(), auth); 232 | if (!_H) 233 | return false; 234 | 235 | // Split H into `Ap` and `sign[A](K | Bp | hash(a·b))` 236 | auto &sig = (signature&)_H->range<0,sizeof(signature)>(); 237 | Ap = public_key(_H->range()); 238 | _Ab = b * *Ap; // because A·bp == Ap·b == b·Ap 239 | // Verify the signature: 240 | return Ap->check(sig, _K | Bp | _hashab.value()); 241 | } 242 | 243 | 244 | // box[K | a·b | a·B | A·b](sign[B](K | H | hash(a·b))) 245 | ServerAckData handshake::createServerAck() { 246 | WITH_SERVER_VARS 247 | return box(serverAckKey(), B.sign(_K | _H.value() | _hashab.value())); 248 | } 249 | 250 | } 251 | -------------------------------------------------------------------------------- /crouton/SecretHandshakeStream.cc: -------------------------------------------------------------------------------- 1 | // 2 | // SecretHandshakeStream.cc 3 | // 4 | // Copyright © 2023 Jens Alfke. All rights reserved. 5 | // 6 | // Licensed under the MIT License: 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | #include "SecretHandshakeStream.hh" 28 | #include "../include/SecretHandshake.hh" 29 | #include "../include/SecretStream.hh" 30 | #include "crouton/Future.hh" 31 | #include "crouton/util/Logging.hh" 32 | #include 33 | 34 | namespace snej::shs::crouton { 35 | using namespace std; 36 | using namespace ::crouton; 37 | 38 | 39 | shs_printflike(2,0) 40 | static void shslog(LogLevel level, const char* format, va_list args) { 41 | char* message = nullptr; 42 | if (vasprintf(&message, format, args) >= 0) 43 | LNet->log(log::level::level_enum(int(level)), "SecretHandshake: {}", message); 44 | free(message); 45 | } 46 | 47 | 48 | SecretHandshake::SecretHandshake(Context const& context, PublicKey const* serverKey) { 49 | static once_flag sOnce; 50 | call_once(sOnce, [] { if (!LogCallback) LogCallback = shslog; }); 51 | 52 | if (serverKey) 53 | _handshake = make_unique(context, *serverKey); 54 | else 55 | _handshake = make_unique(context); 56 | } 57 | 58 | 59 | void SecretHandshake::setClientAuthorizer(ServerHandshake::ClientAuthorizer auth) { 60 | dynamic_cast(*_handshake).setClientAuthorizer(std::move(auth)); 61 | } 62 | 63 | 64 | ASYNC SecretHandshake::handshake(std::shared_ptr stream) { 65 | precondition(stream); 66 | if (!stream->isOpen()) 67 | AWAIT stream->open(); 68 | 69 | // Handshake: 70 | do { 71 | auto [toSend, sizeToSend] = _handshake->bytesToSend(); 72 | if (sizeToSend > 0) { 73 | AWAIT stream->write(ConstBytes{toSend, sizeToSend}); 74 | _handshake->sendCompleted(); 75 | } 76 | auto [toRead, sizeToRead] = _handshake->bytesToRead(); 77 | if (sizeToRead > 0) { 78 | size_t n = AWAIT stream->read(MutableBytes{toRead, sizeToRead}); 79 | if (n == sizeToRead) 80 | _handshake->readCompleted(); 81 | else 82 | _handshake->readFailed(); 83 | } 84 | } while (!_handshake->finished() && !_handshake->error()); 85 | 86 | if (_handshake->error()) { 87 | Error err(SecretHandshakeError(int(_handshake->error()))); 88 | _handshake = nullptr; 89 | RETURN err; 90 | } 91 | 92 | // Handshake succeeded: 93 | auto session = _handshake->session(); 94 | _handshake = nullptr; 95 | RETURN session; 96 | } 97 | 98 | 99 | #pragma mark - SECRET HANDSHAKE STREAM: 100 | 101 | 102 | SecretHandshakeStream::SecretHandshakeStream(std::shared_ptr stream, 103 | Context const& context, 104 | PublicKey const* serverKey) 105 | :_stream(stream) 106 | ,_handshake(context, serverKey) 107 | { } 108 | 109 | SecretHandshakeStream::~SecretHandshakeStream() = default; 110 | 111 | void SecretHandshakeStream::setDelegate(Delegate* d) {_delegate = d;} 112 | bool SecretHandshakeStream::isOpen() const {return _open;} 113 | void SecretHandshakeStream::setRawStream(shared_ptr stream) {_stream = std::move(stream);} 114 | 115 | 116 | ASYNC SecretHandshakeStream::open() { 117 | if (_delegate) { 118 | _handshake.setClientAuthorizer([this](PublicKey const& clientKey) { 119 | bool ok = _delegate->authorizeSecretHandshake(clientKey); 120 | if (!ok) 121 | LNet->error("SecretHandshake delegate rejected peer"); 122 | return ok; 123 | }); 124 | } 125 | 126 | Result session = AWAIT NoThrow(_handshake.handshake(_stream)); 127 | if (session.ok()) { 128 | _peerPublicKey = session->peerPublicKey; 129 | _writer = make_unique(*session); 130 | _reader = make_unique(*session); 131 | _open = true; 132 | } else { 133 | AWAIT _stream->close(); 134 | notifyClosed(); 135 | } 136 | RETURN session.error(); 137 | } 138 | 139 | 140 | void SecretHandshakeStream::notifyClosed() { 141 | if (_delegate) 142 | _delegate->secretHandshakeStreamClosed(); 143 | } 144 | 145 | 146 | PublicKey const& SecretHandshakeStream::peerPublicKey() const { 147 | precondition(_open); 148 | return _peerPublicKey; 149 | } 150 | 151 | 152 | ASYNC SecretHandshakeStream::close() { 153 | _open = false; 154 | if (!_stream) 155 | RETURN noerror; 156 | Result result = AWAIT NoThrow(_stream->close()); 157 | notifyClosed(); 158 | RETURN result.error(); 159 | } 160 | 161 | 162 | ASYNC SecretHandshakeStream::closeWrite() { 163 | assert(_stream); 164 | return _stream->closeWrite(); 165 | } 166 | 167 | 168 | ASYNC SecretHandshakeStream::peekNoCopy() { 169 | if (!_open) 170 | RETURN CroutonError::InvalidState; 171 | if (_lastReadSize > 0) { 172 | _reader->skip(_lastReadSize); 173 | _lastReadSize = 0; 174 | } 175 | while (_reader->bytesAvailable() == 0) { 176 | ConstBytes encBytes = AWAIT _stream->readNoCopy(); 177 | if (encBytes.empty()) { 178 | if (_reader->close()) { 179 | LNet->debug("SecretHandshakeStream {} read EOF", (void*)this); 180 | RETURN ConstBytes{}; 181 | } else { 182 | LNet->error("SecretHandshakeStream {} unexpected EOF!", (void*)this); 183 | (void)close(); 184 | RETURN Error(SecretHandshakeError::DataError); 185 | } 186 | } 187 | LNet->debug("SecretHandshakeStream {} received {} encrypted bytes", (void*)this, encBytes.size()); 188 | if (!_reader->push(encBytes.data(), encBytes.size())) { 189 | (void)close(); 190 | RETURN Error(SecretHandshakeError::DataError); 191 | } 192 | LNet->debug("SecretHandshakeStream {} has {} bytes available", (void*)this, _reader->availableData().size); 193 | } 194 | input_data avail = _reader->availableData(); 195 | RETURN ConstBytes(avail.data, avail.size); 196 | } 197 | 198 | 199 | ASYNC SecretHandshakeStream::readNoCopy(size_t maxLen) { 200 | ConstBytes bytes = AWAIT peekNoCopy(); 201 | bytes = bytes.read(maxLen); 202 | _lastReadSize = bytes.size(); 203 | RETURN bytes; 204 | } 205 | 206 | 207 | ASYNC SecretHandshakeStream::write(ConstBytes bytes) { 208 | return write(&bytes, 1); 209 | } 210 | 211 | ASYNC SecretHandshakeStream::write(const ConstBytes buffers[], size_t nBuffers) { 212 | _writer->skip(_lastWriteSize); 213 | _lastWriteSize = 0; 214 | if (LNet->level() <= crouton::log::level::debug) { 215 | size_t total = 0; 216 | for (size_t i = 0; i < nBuffers; ++i) 217 | total += buffers[i].size(); 218 | LNet->debug("SecretHandshakeStream {} writing {} bytes", (void*)this, total); 219 | } 220 | if (!_open) 221 | return CroutonError::InvalidState; 222 | for (size_t i = 0; i < nBuffers; ++i) 223 | _writer->pushPartial(buffers[i].data(), buffers[i].size()); 224 | _writer->flush(); 225 | auto encBytes = _writer->availableData(); 226 | _lastWriteSize = encBytes.size; 227 | LNet->debug("SecretHandshakeStream {} sending {} encrypted bytes", (void*)this, encBytes.size); 228 | return _stream->write(ConstBytes{encBytes.data, encBytes.size}); 229 | } 230 | 231 | 232 | #pragma mark - SOCKET: 233 | 234 | 235 | SecretHandshakeSocket::SecretHandshakeSocket(Context const& context, 236 | PublicKey const& serverKey) 237 | :_stream(make_shared(nullptr, context, &serverKey)) 238 | { } 239 | 240 | 241 | ASYNC SecretHandshakeSocket::open() { 242 | return stream()->open(); 243 | } 244 | 245 | 246 | shared_ptr SecretHandshakeSocket::stream() { 247 | if (!_stream->_stream) { 248 | auto tcpSocket = io::ISocket::newSocket(false); 249 | tcpSocket->bind(*this->_binding); 250 | _stream->setRawStream(tcpSocket->stream()); 251 | } 252 | return _stream; 253 | } 254 | 255 | } 256 | 257 | 258 | namespace crouton { 259 | string ErrorDomainInfo::description(errorcode_t code) { 260 | static constexpr string_view kErrorNames[] = { 261 | "", "protocol error", "authentication error", "data error" }; 262 | return string(kErrorNames[code]); 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /capnproto/SecretConnection.cc: -------------------------------------------------------------------------------- 1 | // 2 | // SecretConnection.cc 3 | // 4 | // Copyright © 2021 Jens Alfke. All rights reserved. 5 | // 6 | // Licensed under the MIT License: 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | 26 | 27 | // This code is adapted from Cap'n Proto's c++/src/kj/compat/tls.c++ 28 | // 29 | // Copyright (c) 2016 Sandstorm Development Group, Inc. and contributors 30 | 31 | #include "SecretConnection.hh" 32 | #include "SecretHandshake.hh" 33 | #include "SecretStream.hh" 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | #include 40 | #include "assert.h" 41 | 42 | namespace snej::shs { 43 | 44 | #pragma mark - STREAM: 45 | 46 | 47 | std::string getPeerName(kj::AsyncIoStream& stream) { 48 | char nameBuf[INET6_ADDRSTRLEN] = ""; 49 | sockaddr_in6 addr; 50 | unsigned addrLen = sizeof(addr); 51 | stream.getpeername((sockaddr*)&addr, &addrLen); 52 | inet_ntop(addr.sin6_family, &addr.sin6_addr, nameBuf, sizeof(nameBuf)); 53 | return std::string(nameBuf); 54 | } 55 | 56 | 57 | class WrappedStream final: public kj::AsyncIoStream { 58 | public: 59 | WrappedStream(kj::Own stream, 60 | kj::Own handshake, 61 | StreamWrapper::Authorizer authorizer, 62 | bool isSocket) 63 | :WrappedStream(*stream, kj::mv(handshake), kj::mv(authorizer), isSocket) 64 | { 65 | _ownInner = kj::mv(stream); 66 | } 67 | 68 | 69 | WrappedStream(kj::AsyncIoStream& stream, 70 | kj::Own handshake, 71 | StreamWrapper::Authorizer authorizer, 72 | bool isSocket) 73 | :_handshake(kj::mv(handshake)) 74 | ,_authorizer(kj::mv(authorizer)) 75 | ,_inner(stream) 76 | ,_isSocket(isSocket) 77 | { } 78 | 79 | 80 | ~WrappedStream() noexcept(false) { } 81 | 82 | 83 | kj::Promise runHandshake() { 84 | std::string address = getPeerName(); 85 | if (_handshake->finished()) { 86 | auto result = _handshake->session(); 87 | KJ_LOG(INFO, "SecretHandshake completed", address); 88 | _handshake = nullptr; 89 | if (_authorizer && !_authorizer(result.peerPublicKey)) 90 | return KJ_EXCEPTION(DISCONNECTED, "Unauthorized client key"); 91 | return result; 92 | } else if (auto [toSend, sendSize] = _handshake->bytesToSend(); sendSize > 0) { 93 | //KJ_LOG(INFO, "SecretHandshake: sending data", sendSize); 94 | return _inner.write(toSend, sendSize).then([this]() { 95 | _handshake->sendCompleted(); 96 | return runHandshake(); // continue 97 | }); 98 | } else if (auto [toRead, readSize] = _handshake->bytesToRead(); readSize > 0) { 99 | return _inner.read(toRead, readSize).then([this]() { 100 | //KJ_LOG(INFO, "SecretHandshake: received data", bytesRead); 101 | _handshake->readCompleted(); 102 | return runHandshake(); // continue 103 | }); 104 | } else { 105 | KJ_LOG(ERROR, "SecretHandshake failed!", address, _handshake->error()); 106 | assert(_handshake->error()); 107 | return KJ_EXCEPTION(DISCONNECTED, "SecretHandshake protocol failed to connect"); 108 | } 109 | } 110 | 111 | 112 | kj::Promise connect() { 113 | std::string address = getPeerName(); 114 | KJ_LOG(INFO, "Beginning SecretHandshake", address); 115 | 116 | return runHandshake().then([this](Session result) { 117 | _session = result; 118 | _encryptor.emplace(result.encryptionKey, result.encryptionNonce); 119 | _decryptor.emplace(result.decryptionKey, result.decryptionNonce); 120 | }, [this](kj::Exception &&x) { 121 | KJ_LOG(ERROR, "SecretHandshake: Connection error", x.getDescription()); 122 | _inner.shutdownWrite(); 123 | _inner.abortRead(); 124 | return std::move(x); 125 | }); 126 | } 127 | 128 | 129 | kj::Own getIdentity(kj::Own inner) { 130 | KJ_IF_MAYBE(keys, _session) { 131 | return kj::heap(keys->peerPublicKey, kj::mv(inner)); 132 | } else { 133 | return {}; 134 | } 135 | } 136 | 137 | 138 | std::string getPeerName() { 139 | return _isSocket ? snej::shs::getPeerName(*this) : ""; 140 | } 141 | 142 | 143 | kj::Promise tryRead(void* buffer, size_t minBytes, size_t maxBytes) override { 144 | auto &decryptor = KJ_REQUIRE_NONNULL(_decryptor); 145 | if (decryptor.bytesAvailable() >= minBytes) { 146 | return decryptor.pull(buffer, maxBytes); 147 | } else { 148 | return _inner.tryRead(buffer, 1, maxBytes).then([this,buffer,minBytes,maxBytes](size_t nBytes) 149 | -> kj::Promise { 150 | if (nBytes == 0) // this happens when the socket is disconnected 151 | return kj::Promise(size_t(0)); 152 | if (!KJ_REQUIRE_NONNULL(_decryptor).push(buffer, nBytes)) 153 | throw std::runtime_error("Received corrupt input data"); 154 | return tryRead(buffer, minBytes, maxBytes); 155 | }); 156 | } 157 | } 158 | 159 | 160 | kj::Promise write(const void* buffer, size_t size) override { 161 | KJ_REQUIRE_NONNULL(_encryptor).push(buffer, size); 162 | return _endWrite(); 163 | } 164 | 165 | 166 | kj::Promise write(kj::ArrayPtr> pieces) override { 167 | auto &encryptor = KJ_REQUIRE_NONNULL(_encryptor); 168 | for (auto &piece : pieces) 169 | encryptor.pushPartial(piece.begin(), piece.size()); 170 | encryptor.flush(); 171 | return _endWrite(); 172 | } 173 | 174 | 175 | kj::Promise _endWrite() { 176 | auto avail = KJ_REQUIRE_NONNULL(_encryptor).availableData(); 177 | return _inner.write(avail.data, avail.size).then([this,avail] { 178 | KJ_REQUIRE_NONNULL(_encryptor).skip(avail.size); 179 | }); 180 | } 181 | 182 | 183 | void shutdownWrite() override { 184 | _inner.shutdownWrite(); 185 | } 186 | kj::Promise whenWriteDisconnected() override { 187 | return _inner.whenWriteDisconnected(); 188 | } 189 | void abortRead() override { 190 | _inner.abortRead(); 191 | } 192 | void getsockopt(int level, int option, void* value, kj::uint* length) override { 193 | _inner.getsockopt(level, option, value, length); 194 | } 195 | void setsockopt(int level, int option, const void* value, kj::uint length) override { 196 | _inner.setsockopt(level, option, value, length); 197 | } 198 | void getsockname(struct sockaddr* addr, kj::uint* length) override { 199 | _inner.getsockname(addr, length); 200 | } 201 | void getpeername(struct sockaddr* addr, kj::uint* length) override { 202 | _inner.getpeername(addr, length); 203 | } 204 | kj::Maybe getFd() const override { 205 | return _inner.getFd(); 206 | } 207 | 208 | private: 209 | kj::Own _handshake; 210 | StreamWrapper::Authorizer _authorizer; 211 | kj::AsyncIoStream& _inner; 212 | kj::Own _ownInner; 213 | kj::Maybe> _shutdownTask; 214 | kj::Maybe _session; 215 | kj::Maybe _encryptor; 216 | kj::Maybe _decryptor; 217 | bool _isSocket; 218 | }; 219 | 220 | 221 | #pragma mark - CONTEXT: 222 | 223 | 224 | void StreamWrapper::setConnectTimeout(kj::Duration timeout, kj::Timer &timer) { 225 | _connectTimeout = timeout; 226 | _connectTimer = &timer; 227 | } 228 | 229 | 230 | kj::Promise> StreamWrapper::wrap(kj::Own stream) { 231 | auto conn = kj::heap(kj::mv(stream), newHandshake(), _authorizer, _isSocket); 232 | auto promise = conn->connect(); 233 | return promise.then(kj::mvCapture(conn, [](kj::Own conn) 234 | -> kj::Own { 235 | return kj::mv(conn); 236 | })); 237 | // FIXME: Handle timeout, like the other `wrap` method (in fact, combine the two methods.) 238 | } 239 | 240 | 241 | kj::Promise StreamWrapper::wrap(kj::AuthenticatedStream stream) { 242 | auto conn = kj::heap(kj::mv(stream.stream), newHandshake(), _authorizer, _isSocket); 243 | auto promise = conn->connect(); 244 | KJ_IF_MAYBE(timeout, _connectTimeout) { 245 | promise = KJ_REQUIRE_NONNULL(_connectTimer)->afterDelay(*timeout).then([]() -> kj::Promise { 246 | return KJ_EXCEPTION(DISCONNECTED, "timed out during Secret Handshake"); 247 | }).exclusiveJoin(kj::mv(promise)); 248 | } 249 | return promise.then([conn=kj::mv(conn),innerId=kj::mv(stream.peerIdentity)]() mutable { 250 | auto id = conn->getIdentity(kj::mv(innerId)); 251 | return kj::AuthenticatedStream { kj::mv(conn), kj::mv(id) }; 252 | }); 253 | } 254 | 255 | 256 | kj::Own ServerWrapper::newHandshake() { 257 | return kj::heap(_context); 258 | } 259 | 260 | kj::Own ClientWrapper::newHandshake() { 261 | return kj::heap(_context, _serverPublicKey); 262 | } 263 | 264 | 265 | #pragma mark - PEER IDENTITY: 266 | 267 | 268 | kj::String SHSPeerIdentity::toString() { 269 | return kj::str("(public key)"); //TODO: Write key as hex string 270 | } 271 | 272 | } // namespace 273 | -------------------------------------------------------------------------------- /include/SecretStream.hh: -------------------------------------------------------------------------------- 1 | // 2 | // SecretStream.hh 3 | // 4 | // Copyright © 2022 Jens Alfke. All rights reserved. 5 | // 6 | 7 | #pragma once 8 | #include "SecretHandshakeTypes.hh" 9 | #include 10 | #include 11 | 12 | namespace snej::shs { 13 | 14 | /// Points to immutable data to be encrypted or decrypted. 15 | struct input_data { 16 | const void* data; ///< The address of the input data 17 | size_t size; ///< The length of the input data in bytes 18 | }; 19 | 20 | 21 | /// Points to a mutable buffer for encrypted/decrypted data to be written to. 22 | struct output_buffer { 23 | void* data; ///< The address to write to 24 | size_t size; ///< On input, the capacity of the buffer; on output, the data size. 25 | }; 26 | 27 | 28 | /// Success or failure status of a `CryptoBox` / `CryptoStream` operation. 29 | enum status_t { 30 | Success, ///< Encryption/decryption succeeded 31 | OutTooSmall, ///< The output's capacity is too small 32 | IncompleteInput, ///< Need more input data to decrypt 33 | CorruptData ///< The encrypted data is corrupted 34 | }; 35 | 36 | 37 | 38 | /// Message-oriented encryption using keys & nonces from a Session; 39 | /// abstract base class of `EncryptoBox` and `DecryptoBox`. 40 | class CryptoBox { 41 | public: 42 | /// Data format to use for encrypted messages. 43 | enum Protocol { 44 | Compact, ///< Less overhead, but message lengths are eavesdroppable. 45 | BoxStream ///< Scuttlebutt-compatible. More overhead, but msg lengths are encrypted. 46 | }; 47 | 48 | /// Returns the encrypted size of a message. (It will be somewhat larger than the input.) 49 | size_t encryptedSize(size_t inputSize); 50 | 51 | ~CryptoBox(); 52 | 53 | protected: 54 | CryptoBox(SessionKey const& key, Nonce const& nonce, Protocol protocol =Compact) 55 | :_key(key) 56 | ,_nonce(nonce) 57 | ,_protocol(protocol) 58 | { } 59 | 60 | struct BoxStreamHeader; 61 | 62 | SessionKey const _key; 63 | Nonce _nonce; 64 | Protocol const _protocol; 65 | }; 66 | 67 | 68 | 69 | /// Message-oriented encryption using keys & nonces from a Session. 70 | /// Encrypts a sequence of arbitrary-size blocks of data ("messages"). Each encrypted message 71 | /// is prefixed with its size and a MAC. The nonce is incremented after each message. 72 | /// 73 | /// `EncryptionStream` wraps this to provide a byte-oriented stream API. 74 | class EncryptoBox : public CryptoBox { 75 | public: 76 | /// Constructs an `EncryptoBox` from an encryption key and nonce. 77 | EncryptoBox(SessionKey const& key, Nonce const& nonce, Protocol protocol =Compact) 78 | :CryptoBox(key, nonce, protocol) { } 79 | 80 | explicit EncryptoBox(Session const& session, Protocol p =CryptoBox::Compact) 81 | :EncryptoBox(session.encryptionKey, session.encryptionNonce, p) { } 82 | 83 | /// The maximum byte length of a message, before encryption. 84 | static constexpr size_t kMaxMessageSize = 0xFFFF; 85 | 86 | /// Encrypts an outgoing message, attaching the MAC and size. 87 | /// @note Currently the maximum size message is 65535 bytes. 88 | /// @param in The message to be sent. 89 | /// @param out Where to write the encrypted message. 90 | /// On entry `out.data` must be set and `out.size` must be the maximum capacity. 91 | /// On success, `out.size` will be set to the encrypted size. 92 | /// @return The status, either `Success` or `OutTooSmall`. 93 | status_t encrypt(input_data in, output_buffer &out); 94 | }; 95 | 96 | 97 | /// Message-oriented decryption using keys & nonces from a Session: 98 | /// Decrypts _entire messages_ created by `EncryptoBox`, in the same order they were created. 99 | /// Reads the message size so it knows how big the full message is, and indicates whether the 100 | /// message is incomplete. 101 | /// 102 | /// `DecryptionStream` wraps this to provide a byte-oriented stream API. 103 | class DecryptoBox : public CryptoBox { 104 | public: 105 | /// Constructs a `DecryptoBox` from an encryption key and nonce. 106 | DecryptoBox(SessionKey const& key, Nonce const& nonce, Protocol protocol =Compact) 107 | :CryptoBox(key, nonce, protocol) { } 108 | 109 | explicit DecryptoBox(Session const& session, Protocol p =CryptoBox::Compact) 110 | :DecryptoBox(session.decryptionKey, session.decryptionNonce, p) { } 111 | 112 | /// The minimum number of input bytes needed for peek() to succeed. 113 | size_t minPeekSize() const; 114 | 115 | struct PeekResult { 116 | status_t status; 117 | size_t decryptedSize; 118 | size_t encryptedSize; 119 | }; 120 | 121 | /// Looks at the input data (which must start on a message boundary but can be incomplete) 122 | /// and returns the length of the encrypted and decrypted messages. 123 | /// The `status` value will be: 124 | /// - `Success` if the size is known; `encryptedSize` and `decryptedSize` will be accurate. 125 | /// - `IncompleteInput` if there's not enough input to determine the size; `decryptedSize` 126 | /// will be set to the length of input needed to determine it. 127 | /// - `CorruptData` if the input data is corrupted 128 | PeekResult peek(input_data); 129 | 130 | std::pair getDecryptedSize(input_data); // deprecated; use peek() 131 | 132 | /// Decrypts incoming data from the encrypted stream, reading the next message if it's 133 | /// completely available. This always reads one entire message, as passed to `encrypt` 134 | /// on the other end. 135 | /// 136 | /// If the input data is incomplete (doesn't contain the entire message), nothing is 137 | /// consumed and `IncompleteInput` is returned. 138 | /// 139 | /// If the output buffer is too small to hold the entire message, nothing is consumed 140 | /// and `OutTooSmall` is returned. You can then call `getDecryptedSize` to find out how big 141 | /// a buffer you need. 142 | /// 143 | /// If the input data contains more than just one message, the extra data is not 144 | /// consumed; `in.data` will be adjusted to point to the remaining data. 145 | /// 146 | /// After `Success` is returned, there could be another complete message remaining in the 147 | /// buffer, so you should call `decrypt` again (potentially multiple times.) 148 | /// 149 | /// @param in Data from the stream. On success, **this will be adjusted** to account for 150 | /// the bytes consumed: `data` will point to the first unread byte, and `size` 151 | /// will be set to the number of remaining bytes. 152 | /// @param out Where to write the decrypted message. 153 | /// On input, its `data` must be set, and `size` must be the maximum capacity. 154 | /// On success, its `size` will be set to the decrypted message's size. 155 | /// @return The status; see the description of the 'status' enum values. 156 | status_t decrypt(input_data &in, output_buffer &out); 157 | 158 | private: 159 | PeekResult decryptBoxStreamHeader(input_data in, BoxStreamHeader &header); 160 | }; 161 | 162 | 163 | 164 | /// Byte-oriented stream crypto API; 165 | /// abstract base class of EncryptionStream and DecryptionStream. 166 | class CryptoStream { 167 | public: 168 | using Protocol = CryptoBox::Protocol; 169 | 170 | /// Reads processed (encrypted or decrypted) data, copying it from the internal buffer. 171 | /// @param buffer The address to copy data to 172 | /// @param maxSize The maximum number of bytes to copy 173 | /// @return The number of bytes copied 174 | size_t pull(void *buffer, size_t maxSize); 175 | 176 | /// Similar to `pull` but doesn't copy the data; instead it returns a pointer and size. 177 | /// After you're done with the data, call `skip` to remove it from the buffer. 178 | /// @warning The returned pointer is invalidated if you call `push`. 179 | input_data availableData() const {return {_buffer.data(), _processedBytes};} 180 | 181 | /// Returns the number of bytes available to pull. 182 | size_t bytesAvailable() const {return _processedBytes;} 183 | 184 | /// Removes processed data from the internal buffer. Usually called after `availableData`. 185 | size_t skip(size_t); 186 | 187 | protected: 188 | CryptoStream() = default; 189 | CryptoStream(const CryptoStream&) = delete; 190 | CryptoStream& operator=(const CryptoStream&) = delete; 191 | 192 | std::vector _buffer; // processed followed by unprocessed bytes 193 | size_t _processedBytes = 0; // # of bytes already encrypted/decrypted 194 | }; 195 | 196 | 197 | 198 | /// Stream-oriented adapter for Session-based encryption. 199 | /// You _push_ cleartext bytes into it, and _pull_ encrypted bytes out of it. 200 | /// Pull doesn't have to keep up with push; data will be buffered as needed. 201 | class EncryptionStream : public CryptoStream { 202 | public: 203 | /// Constructs an EncryptionStream. 204 | EncryptionStream(SessionKey const& key, Nonce const& nonce, Protocol p =CryptoBox::Compact) 205 | :_encryptor(key, nonce, p) { } 206 | 207 | explicit EncryptionStream(Session const& session, Protocol p =CryptoBox::Compact) 208 | :_encryptor(session.encryptionKey, session.encryptionNonce, p) { } 209 | 210 | /// Encrypts data. The ciphertext is then available to pull. 211 | /// @param data The address of the cleartext data to add 212 | /// @param size The size of the data 213 | void push(const void *data, size_t size); 214 | 215 | /// Appends cleartext data to the internal buffer, but does not encrypt it yet. 216 | /// You can call this multiple times, then call `flush`. 217 | /// @param data The address of the cleartext data to add 218 | /// @param size The size of the data 219 | void pushPartial(const void *data, size_t size); 220 | 221 | /// Encrypts all data buffered by `pushPartial`, which is then available to pull. 222 | void flush(); 223 | 224 | private: 225 | EncryptoBox _encryptor; 226 | }; 227 | 228 | 229 | 230 | /// Stream-oriented adapter for Session-based decryption. 231 | /// You _push_ encrypted bytes into it from the network (or wherever), 232 | /// and _pull_ decrypted bytes out of it. 233 | /// Push and pull can run at different rates; data will be buffered as needed. 234 | class DecryptionStream : public CryptoStream { 235 | public: 236 | /// Constructs a DecryptionStream. 237 | DecryptionStream(SessionKey const& key, Nonce const& nonce, Protocol p =CryptoBox::Compact) 238 | :_decryptor(key, nonce, p) { } 239 | 240 | explicit DecryptionStream(Session const& session, Protocol p =CryptoBox::Compact) 241 | :_decryptor(session.decryptionKey, session.decryptionNonce, p) { } 242 | 243 | /// Adds encrypted data received from the sender. 244 | /// It will be internally buffered and decrypted. 245 | /// @note Pushing data doesn't guarantee there will be bytes to pull; 246 | /// decryption only occurs when a complete encrypted block is received. 247 | /// @param data The address of the encrypted data to add 248 | /// @param size The size of the encrypted data 249 | /// @return True on success, false if the data is corrupted. 250 | bool push(const void *data, size_t size); 251 | 252 | /// Call this when the stream from the sender ends and there is no more data to push. 253 | /// @return True if this is a clean close, false if there's an incomplete message. 254 | bool close(); 255 | 256 | private: 257 | DecryptoBox _decryptor; 258 | }; 259 | 260 | } 261 | -------------------------------------------------------------------------------- /src/SecretHandshake.cc: -------------------------------------------------------------------------------- 1 | // 2 | // SecretHandshake.cc 3 | // 4 | // Copyright © 2021 Jens Alfke. All rights reserved. 5 | // 6 | // Licensed under the MIT License: 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | 26 | #include "SecretHandshake.hh" 27 | #include "SecretHandshake_Internal.hh" 28 | #include "shs.hh" 29 | #include "monocypher/signatures.hh" 30 | #include 31 | #include 32 | #include 33 | 34 | namespace snej::shs { 35 | 36 | 37 | void (*LogCallback)(LogLevel, const char* format, va_list args); 38 | 39 | void _Log(LogLevel level, const char*format, ...) noexcept { 40 | if (LogCallback) { 41 | va_list args; 42 | va_start(args, format); 43 | LogCallback(level, format, args); 44 | va_end(args); 45 | } 46 | } 47 | 48 | 49 | KeyPair::KeyPair(SigningKey const& sk) 50 | :signingKey(sk) 51 | ,publicKey(impl::signing_key(sk).get_public_key()) 52 | { } 53 | 54 | 55 | KeyPair KeyPair::generate() { 56 | return KeyPair(impl::key_pair::generate()); 57 | } 58 | 59 | 60 | KeyPair::~KeyPair() { 61 | monocypher::wipe(&signingKey, sizeof(signingKey)); 62 | } 63 | 64 | 65 | AppID Context::appIDFromString(const char *str) { 66 | monocypher::byte_array<32> id; 67 | id.fillWithString(str); 68 | return id; 69 | } 70 | 71 | 72 | Session::~Session() { 73 | monocypher::wipe(this, sizeof(*this)); 74 | } 75 | 76 | 77 | #pragma mark - HANDSHAKE: 78 | 79 | 80 | Handshake::Handshake(Context const& context) 81 | :_context(context) 82 | ,_impl(std::make_unique(impl::app_id(context.appID), 83 | impl::signing_key(context.keyPair.signingKey), 84 | impl::public_key(context.keyPair.publicKey))) 85 | { } 86 | 87 | 88 | Handshake::~Handshake() = default; 89 | 90 | 91 | void Handshake::nextStep() { 92 | assert(_step > Failed && _step < Finished); 93 | _step = Step(_step + 1); 94 | if (_step == Finished) 95 | Log(info, "Successful handshake!"); 96 | } 97 | 98 | 99 | std::pair Handshake::bytesToRead() { 100 | size_t needed = byteCountNeeded(); 101 | if (needed > 0) 102 | Log(debug, "Step %d/4: Awaiting %zu bytes...", _step, needed); 103 | _inputBuffer.resize(needed); 104 | return {_inputBuffer.data(), needed}; 105 | } 106 | 107 | 108 | bool Handshake::readCompleted() { 109 | if (_inputBuffer.size() != byteCountNeeded()) 110 | throw std::logic_error("Unexpected call to Handshake::readCompleted"); 111 | if (_receivedBytes(_inputBuffer.data())) { 112 | Log(debug, "Step %d OK", _step); 113 | nextStep(); 114 | _inputBuffer.clear(); 115 | return true; 116 | } else { 117 | Log(err, "Received invalid data in step %d; HANDSHAKE FAILED", _step); 118 | failed(); 119 | return false; 120 | } 121 | } 122 | 123 | 124 | void Handshake::readFailed() { 125 | Log(err, "Unexpected EOF at step %d; HANDSHAKE FAILED", _step); 126 | failed(); 127 | } 128 | 129 | 130 | void Handshake::failed() { 131 | _error = (_step < ClientAuth) ? Error::ProtocolError : Error::AuthError; 132 | _step = Failed; 133 | } 134 | 135 | 136 | intptr_t Handshake::receivedBytes(const void *src, size_t count) { 137 | if (_step == Failed || _step == Finished) 138 | return -1; 139 | size_t needed = byteCountNeeded(); 140 | if (needed == 0) 141 | return 0; 142 | count = std::min(count, needed - _inputBuffer.size()); 143 | _inputBuffer.reserve(needed); 144 | _inputBuffer.insert(_inputBuffer.end(), (uint8_t*)src, (uint8_t*)src + count); 145 | if (_inputBuffer.size() < needed) { 146 | // Wait for more bytes: 147 | Log(debug, "Received %zu bytes; waiting...", count); 148 | } else { 149 | // Buffer has enough bytes, so consume it: 150 | readCompleted(); 151 | } 152 | return _step != Failed ? count : -1; 153 | } 154 | 155 | 156 | std::pair Handshake::bytesToSend() { 157 | if (_step == Failed || _step == Finished) 158 | return {}; 159 | if (_outputBuffer.empty()) 160 | _fillOutputBuffer(_outputBuffer); 161 | if (!_outputBuffer.empty()) 162 | Log(debug, "Step %d/4: Sending %zu bytes...", _step, _outputBuffer.size()); 163 | return {_outputBuffer.data(), _outputBuffer.size()}; 164 | } 165 | 166 | 167 | void Handshake::sendCompleted() { 168 | if (_outputBuffer.empty()) 169 | throw std::logic_error("Unexpected call to Handshake::sendCompleted"); 170 | Log(debug, "Send completed"); 171 | _outputBuffer.clear(); 172 | nextStep(); 173 | } 174 | 175 | 176 | intptr_t Handshake::copyBytesToSend(void *dst, size_t maxCount) { 177 | if (_step == Failed) 178 | return -1; 179 | if (_outputBuffer.empty()) { 180 | // Fill buffer: 181 | if (bytesToSend().second == 0) 182 | return {}; 183 | } 184 | // Copy bytes from buffer to dst: 185 | size_t count = std::min(_outputBuffer.size(), maxCount); 186 | ::memcpy(dst, _outputBuffer.data(), count); 187 | _outputBuffer.erase(_outputBuffer.begin(), _outputBuffer.begin() + count); 188 | if (_outputBuffer.empty()) { 189 | // Write is complete: 190 | Log(debug, "Send completed"); 191 | nextStep(); 192 | } else { 193 | Log(debug, "Sent %zu bytes...", count); 194 | } 195 | return count; 196 | } 197 | 198 | 199 | Session Handshake::session() { 200 | if (_step != Finished) 201 | throw std::logic_error("Secret Handshake protocol isn't complete"); 202 | Session session; 203 | _impl->getOutcome((impl::session_key&)session.encryptionKey, 204 | (impl::nonce&)session.encryptionNonce, 205 | (impl::session_key&)session.decryptionKey, 206 | (impl::nonce&)session.decryptionNonce, 207 | (impl::public_key&)session.peerPublicKey); 208 | return session; 209 | } 210 | 211 | 212 | template 213 | static T& spaceFor(std::vector &output) { 214 | output.resize(sizeof(T)); 215 | return *(T*)output.data(); 216 | } 217 | 218 | 219 | #pragma mark - CLIENT: 220 | 221 | 222 | /* Notes on interpreting client-side failures: 223 | - EOF before receiving the server challenge: 224 | - It's not a SecretHandshake server 225 | - It doesn't use the same AppID 226 | - Invalid server challenge: 227 | - It's not a SecretHandshake server 228 | - EOF before receiving the server ack: 229 | - Server has a different public key 230 | - Server doesn't allow connections from your public key 231 | 232 | Data corruption or a MITM altering data is also possible. 233 | */ 234 | 235 | 236 | ClientHandshake::ClientHandshake(Context const& context, 237 | PublicKey const& theirPublicKey) 238 | :Handshake(context) 239 | { 240 | _impl->setServerPublicKey(impl::public_key(theirPublicKey)); 241 | } 242 | 243 | 244 | size_t ClientHandshake::byteCountNeeded() { 245 | switch (_step) { 246 | case ServerChallenge: return sizeof(impl::ChallengeData); 247 | case ServerAck: return sizeof(impl::ServerAckData); 248 | default: return 0; 249 | } 250 | } 251 | 252 | 253 | bool ClientHandshake::_receivedBytes(const uint8_t *bytes) { 254 | switch (_step) { 255 | case ServerChallenge: return _impl->verifyChallenge(*(impl::ChallengeData*)bytes); 256 | case ServerAck: return _impl->verifyServerAck(*(impl::ServerAckData*)bytes); 257 | default: return false; 258 | } 259 | } 260 | 261 | 262 | void ClientHandshake::_fillOutputBuffer(std::vector &output) { 263 | switch (_step) { 264 | case ClientChallenge: 265 | spaceFor(output) = _impl->createClientChallenge(); 266 | break; 267 | case ClientAuth: 268 | spaceFor(output) = _impl->createClientAuth(); 269 | break; 270 | default: 271 | break; 272 | } 273 | } 274 | 275 | 276 | #pragma mark - SERVER: 277 | 278 | 279 | #define _serverState() (impl::shs_server*)_state.get() 280 | 281 | 282 | ServerHandshake::ServerHandshake(Context const& context) 283 | :Handshake(context) 284 | { } 285 | 286 | 287 | size_t ServerHandshake::byteCountNeeded() { 288 | switch (_step) { 289 | case ClientChallenge: return sizeof(impl::ChallengeData); 290 | case ClientAuth: return sizeof(impl::ClientAuthData); 291 | default: return 0; 292 | } 293 | } 294 | 295 | 296 | bool ServerHandshake::_receivedBytes(const uint8_t *bytes) { 297 | switch (_step) { 298 | case ClientChallenge: 299 | return _impl->verifyChallenge(*(impl::ChallengeData*)bytes); 300 | case ClientAuth: 301 | return _impl->verifyClientAuth(*(impl::ClientAuthData*)bytes) 302 | && (!_clientAuth || _clientAuth(_impl->getPeerPublicKey())); 303 | default: 304 | return false; 305 | } 306 | } 307 | 308 | 309 | void ServerHandshake::_fillOutputBuffer(std::vector &output) { 310 | switch (_step) { 311 | case ServerChallenge: 312 | spaceFor(output) = _impl->createServerChallenge(); 313 | break; 314 | case ServerAck: 315 | spaceFor(output) = _impl->createServerAck(); 316 | break; 317 | default: 318 | break; 319 | } 320 | } 321 | 322 | } 323 | 324 | 325 | #pragma mark - C GLUE: 326 | 327 | 328 | #include "SecretHandshake.h" 329 | 330 | using namespace snej::shs; 331 | 332 | static inline SHSHandshake* external(Handshake* h) {return (SHSHandshake*)h;} 333 | static inline Handshake* internal(SHSHandshake* h) {return (Handshake*)h;} 334 | 335 | 336 | void SHSErase(void *dst, size_t size) { 337 | monocypher::wipe(dst, size); 338 | } 339 | 340 | SHSKeyPair SHSKeyPair_Generate(void) { 341 | auto pair = KeyPair::generate(); 342 | return (const SHSKeyPair&)pair; 343 | } 344 | 345 | SHSKeyPair SHSKeyPair_Regenerate(const SHSSigningKey* signingKey) { 346 | KeyPair pair(*(SigningKey*)signingKey); 347 | return (const SHSKeyPair&)pair; 348 | } 349 | 350 | SHSAppID SHSAppID_FromString(const char* str) { 351 | auto id = Context::appIDFromString(str); 352 | return (const SHSAppID&)id; 353 | } 354 | 355 | SHSHandshake* SHSHandshake_CreateClient(const SHSAppID *appID, 356 | const SHSKeyPair *keyPair, 357 | const SHSPublicKey *serverPublicKey) 358 | { 359 | Context ctx((AppID&)*appID, (KeyPair&)*keyPair); 360 | return external(new ClientHandshake(ctx, (PublicKey&)*serverPublicKey)); 361 | } 362 | 363 | SHSHandshake* SHSHandshake_CreateServer(const SHSAppID *appID, 364 | const SHSKeyPair *keyPair) { 365 | Context ctx((AppID&)*appID, (KeyPair&)*keyPair); 366 | return external(new ServerHandshake(ctx)); 367 | } 368 | 369 | 370 | void SHSHandshake_Free(SHSHandshake *h) { 371 | delete internal(h); 372 | } 373 | 374 | 375 | size_t SHSHandshake_GetBytesNeeded(SHSHandshake *h) { 376 | return internal(h)->byteCountNeeded(); 377 | } 378 | 379 | 380 | void* SHSHandshake_GetInputBuffer(SHSHandshake *h) { 381 | return internal(h)->bytesToRead().first; 382 | } 383 | 384 | 385 | bool SHSHandshake_ReadCompleted(SHSHandshake *h) { 386 | return internal(h)->readCompleted(); 387 | } 388 | 389 | 390 | intptr_t SHSHandshake_ReceivedBytes(SHSHandshake *h, const void *src, size_t count) { 391 | return internal(h)->receivedBytes(src, count); 392 | } 393 | 394 | 395 | SHSInputBuffer SHSHandshake_GetBytesToSend(SHSHandshake *h) { 396 | auto buf = internal(h)->bytesToSend(); 397 | return {buf.first, buf.second}; 398 | } 399 | 400 | 401 | void SHSHandshake_SendCompleted(SHSHandshake *h) { 402 | internal(h)->sendCompleted(); 403 | } 404 | 405 | 406 | intptr_t SHSHandshake_CopyBytesToSend(SHSHandshake *h, void *dst, size_t capacity) { 407 | return internal(h)->copyBytesToSend(dst, capacity); 408 | } 409 | 410 | 411 | SHSError SHSHandshake_GetError(SHSHandshake *h) { 412 | return SHSError(internal(h)->error()); 413 | } 414 | 415 | 416 | bool SHSHandshake_Finished(SHSHandshake *h) { 417 | return internal(h)->finished(); 418 | } 419 | 420 | 421 | SHSSession SHSHandshake_GetSession(SHSHandshake *h) { 422 | Session s = internal(h)->session(); 423 | return (SHSSession&)s; 424 | } 425 | 426 | -------------------------------------------------------------------------------- /src/SecretStream.cc: -------------------------------------------------------------------------------- 1 | // 2 | // SecretStream.cc 3 | // 4 | // Copyright © 2022 Jens Alfke. All rights reserved. 5 | // 6 | // Licensed under the MIT License: 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | #include "SecretStream.hh" 28 | #include "shs.hh" 29 | #include "monocypher/encryption.hh" 30 | #include 31 | #include 32 | #include 33 | 34 | 35 | #define _UNUSED 36 | #ifdef __has_attribute 37 | # if __has_attribute(unused) 38 | # undef _UNUSED 39 | # define _UNUSED __attribute__((unused)) 40 | # endif 41 | #endif 42 | 43 | 44 | namespace snej::shs { 45 | using box_stream_key = monocypher::session::encryption_key; 46 | using compact_key = monocypher::session::key; 47 | using session_nonce = monocypher::session::nonce; 48 | 49 | static_assert(sizeof(SessionKey) == sizeof(box_stream_key)); 50 | static_assert(sizeof(SessionKey) == sizeof(compact_key)); 51 | static_assert(sizeof(Nonce) == sizeof(session_nonce)); 52 | 53 | using MAC = monocypher::session::mac; 54 | 55 | struct CryptoBox::BoxStreamHeader { 56 | uint8_t size_be[2]; 57 | MAC mac; 58 | }; 59 | 60 | 61 | static inline void writeUint16At(uint8_t *dst, size_t size) { 62 | assert (size <= 0xFFFF); 63 | dst[0] = (size >> 8) & 0xFF; 64 | dst[1] = size & 0xFF; 65 | } 66 | 67 | static inline size_t readUint16At(const uint8_t *src) { 68 | return (size_t(src[0]) << 8) | src[1]; 69 | } 70 | 71 | 72 | size_t CryptoBox::encryptedSize(size_t inputSize) { 73 | static_assert(sizeof(CryptoBox::BoxStreamHeader) == 2 + sizeof(MAC)); 74 | 75 | if (_protocol == BoxStream) 76 | return sizeof(BoxStreamHeader) + sizeof(MAC) + inputSize; 77 | else 78 | return 2 + sizeof(MAC) + inputSize; 79 | } 80 | 81 | 82 | CryptoBox::~CryptoBox() { 83 | monocypher::wipe((void*)&_key, sizeof(_key)); 84 | } 85 | 86 | 87 | status_t EncryptoBox::encrypt(input_data in, output_buffer &out) { 88 | if (in.size > 0xFFFF) 89 | throw std::invalid_argument("CryptoBox message too large"); 90 | size_t encSize = encryptedSize(in.size); 91 | if (out.size < encSize) 92 | return OutTooSmall; 93 | 94 | // Encrypt: 95 | out.size = encSize; 96 | auto dst = (uint8_t*)out.data; 97 | auto &nonce = (session_nonce&)_nonce; 98 | if (_protocol == BoxStream) { 99 | // Create a header buffer that starts with the cleartext length: 100 | auto &key = (const box_stream_key&)_key; 101 | BoxStreamHeader header; 102 | writeUint16At(header.size_be, in.size); 103 | // Encrypt the message. Ciphertext goes into `out`, MAC goes into the header: 104 | auto ciphertextp = dst + sizeof(header) + sizeof(MAC); 105 | header.mac = key.lock(nonce, {in.data, in.size}, ciphertextp); 106 | ++nonce; 107 | // Now encrypt the header and put it at the start of the output: 108 | key.box(nonce, {&header, sizeof(header)}, {dst, encSize}); 109 | ++nonce; 110 | } else { 111 | // Simpler protocol -- just plaintext_size + box 112 | auto &key = (const compact_key&)_key; 113 | key.box(nonce, {in.data, in.size}, {dst + 2, encSize - 2}); 114 | ++nonce; 115 | writeUint16At(dst, in.size); 116 | } 117 | return Success; 118 | } 119 | 120 | 121 | DecryptoBox::PeekResult DecryptoBox::decryptBoxStreamHeader(input_data in, 122 | BoxStreamHeader &header) 123 | { 124 | static constexpr size_t kPrefixSize = sizeof(MAC) + sizeof(BoxStreamHeader); 125 | if (in.size < kPrefixSize) 126 | return {IncompleteInput, 0, kPrefixSize}; 127 | // The nonce has to be incremented first, because on the sending side the header was the 128 | // second thing to be encrypted. But leave the session's nonce alone for now. 129 | auto &key = (const box_stream_key&)_key; 130 | auto nonce = (session_nonce&)_nonce; 131 | ++nonce; 132 | auto out = key.unbox(nonce, 133 | {in.data, kPrefixSize}, 134 | {&header, sizeof(header)}); 135 | if (out.size != sizeof(header)) 136 | return {CorruptData, 0, 0}; 137 | size_t decryptedSize = readUint16At(header.size_be); 138 | return {Success, decryptedSize, kPrefixSize + decryptedSize}; 139 | } 140 | 141 | 142 | size_t DecryptoBox::minPeekSize() const { 143 | return _protocol == BoxStream ? sizeof(MAC) + sizeof(BoxStreamHeader) : 2; 144 | } 145 | 146 | 147 | DecryptoBox::PeekResult DecryptoBox::peek(input_data in) { 148 | if (_protocol == BoxStream) { 149 | BoxStreamHeader header; 150 | return decryptBoxStreamHeader(in, header); 151 | } else { 152 | if (in.size < 2) 153 | return {IncompleteInput, 0, 2}; 154 | size_t decryptedSize = readUint16At((const uint8_t*)in.data); 155 | return {Success, decryptedSize, encryptedSize(decryptedSize)}; 156 | } 157 | } 158 | 159 | std::pair DecryptoBox::getDecryptedSize(input_data in) { 160 | auto result = peek(in); 161 | return {result.status, result.decryptedSize}; 162 | } 163 | 164 | 165 | status_t DecryptoBox::decrypt(input_data &in, output_buffer &out) { 166 | auto src = (const uint8_t*)in.data; 167 | PeekResult r; 168 | auto &nonce = (session_nonce&)_nonce; 169 | if (_protocol == BoxStream) { 170 | BoxStreamHeader header; 171 | r = decryptBoxStreamHeader(in, header); 172 | if (r.status != Success) 173 | return r.status; 174 | if (in.size < r.encryptedSize) 175 | return IncompleteInput; 176 | if (out.size < r.decryptedSize) 177 | return OutTooSmall; 178 | 179 | auto &key = (const box_stream_key&)_key; 180 | if (!key.unlock(nonce, header.mac, 181 | {src + sizeof(MAC) + sizeof(header), r.decryptedSize}, // ciphertext 182 | out.data)) // output plaintext 183 | return CorruptData; 184 | ++nonce; // extra increment due to 2nd decryption 185 | } else { 186 | r = peek(in); 187 | if (r.status != Success) 188 | return r.status; 189 | if (in.size < r.encryptedSize) 190 | return IncompleteInput; 191 | if (out.size < r.decryptedSize) 192 | return OutTooSmall; 193 | 194 | auto &key = (const compact_key&)_key; 195 | if (key.unbox(nonce, {src + 2, r.encryptedSize - 2}, {out.data, out.size}).size != r.decryptedSize) 196 | return CorruptData; 197 | } 198 | ++nonce; 199 | out.size = r.decryptedSize; 200 | in.data = src + r.encryptedSize; 201 | in.size -= r.encryptedSize; 202 | return Success; 203 | } 204 | 205 | 206 | #pragma mark - CRYPTOSTREAM: 207 | 208 | 209 | size_t CryptoStream::skip(size_t maxSize) { 210 | size_t n = std::min(maxSize, _processedBytes); 211 | if (n > 0) { 212 | _buffer.erase(_buffer.begin(), _buffer.begin() + n); 213 | _processedBytes -= n; 214 | } 215 | return n; 216 | } 217 | 218 | 219 | size_t CryptoStream::pull(void *dst, size_t dstSize) { 220 | auto avail = availableData(); 221 | avail.size = std::min(avail.size, dstSize); 222 | if (avail.size > 0) { 223 | memcpy(dst, avail.data, avail.size); 224 | skip(avail.size); 225 | } 226 | return avail.size; 227 | } 228 | 229 | 230 | void EncryptionStream::push(const void *data, size_t size) { 231 | pushPartial(data, size); 232 | flush(); 233 | } 234 | 235 | 236 | void EncryptionStream::pushPartial(const void *data, size_t size) { 237 | // Append data to the buffer. The unprocessed data can only grow to 64KB (kMaxMessageSize), 238 | // so if there's more data than that, flush periodically. 239 | auto begin = (const uint8_t*)data; 240 | while (size > 0) { 241 | size_t maxSize = EncryptoBox::kMaxMessageSize - (_buffer.size() - _processedBytes); 242 | size_t chunk = std::min(size, maxSize); 243 | _buffer.insert(_buffer.end(), begin, begin + chunk); 244 | size -= chunk; 245 | if (size > 0) { 246 | begin += chunk; 247 | flush(); 248 | } 249 | } 250 | } 251 | 252 | 253 | void EncryptionStream::flush() { 254 | size_t msgSize = _buffer.size() - _processedBytes; 255 | if (msgSize > 0) { 256 | _buffer.resize(_processedBytes + _encryptor.encryptedSize(msgSize)); 257 | input_data in = {&_buffer[_processedBytes], msgSize}; 258 | output_buffer out = {(void*)in.data, _buffer.size() - _processedBytes}; 259 | _UNUSED auto status = _encryptor.encrypt(in, out); 260 | assert(status == Success); 261 | _processedBytes += out.size; 262 | _buffer.resize(_processedBytes); 263 | } 264 | } 265 | 266 | 267 | bool DecryptionStream::push(const void *data, size_t size) { 268 | // Append data to the buffer: 269 | auto begin = (const uint8_t*)data; 270 | _buffer.insert(_buffer.end(), begin, begin + size); 271 | 272 | while (true) { 273 | // See if there's enough to decrypt: 274 | output_buffer out = {nullptr, _buffer.size() - _processedBytes}; 275 | if (out.size > 0) 276 | out.data = &_buffer[_processedBytes]; 277 | input_data in = {out.data, out.size}; 278 | switch (_decryptor.decrypt(in, out)) { 279 | case Success: 280 | _processedBytes += out.size; 281 | // Decrypting the data shortened it, so cut out the remaining space: 282 | _buffer.erase(_buffer.begin() + _processedBytes, 283 | _buffer.begin() + ((uint8_t*)in.data - _buffer.data())); 284 | // Continue the `while` loop, in case there's another complete message: 285 | break; 286 | case IncompleteInput: 287 | return true; // Done 288 | case CorruptData: 289 | return false; // Failure 290 | case OutTooSmall: 291 | throw std::logic_error("DecryptionStream failure"); // impossible 292 | } 293 | } 294 | } 295 | 296 | 297 | bool DecryptionStream::close() { 298 | bool ok = _buffer.size() == _processedBytes; 299 | _buffer.clear(); 300 | _processedBytes = 0; 301 | return ok; 302 | } 303 | 304 | } 305 | 306 | 307 | #pragma mark - C GLUE: 308 | 309 | 310 | #include "SecretStream.h" 311 | 312 | using namespace snej::shs; 313 | 314 | 315 | static inline auto external(EncryptoBox *box) {return (SHSEncryptoBox*)box;} 316 | static inline auto external(DecryptoBox *box) {return (SHSDecryptoBox*)box;} 317 | 318 | static inline auto internal(SHSEncryptoBox *box) {return (EncryptoBox*)box;} 319 | static inline auto internal(SHSDecryptoBox *box) {return (DecryptoBox*)box;} 320 | 321 | static inline auto& internal(SHSInputBuffer const& buf) {return (input_data const&)buf;} 322 | static inline auto& internal(SHSInputBuffer *buf) {return (input_data&)*buf;} 323 | static inline auto& internal(SHSOutputBuffer *buf) {return (output_buffer&)*buf;} 324 | 325 | 326 | SHSEncryptoBox* SHSEncryptoBox_Create(const SHSSession *session, SHSCryptoBoxProtocol protocol) { 327 | return external( new EncryptoBox(*(Session*)session, (CryptoBox::Protocol)protocol) ); 328 | } 329 | 330 | void SHSEncryptoBox_Free(SHSEncryptoBox *box) { 331 | delete internal(box); 332 | } 333 | 334 | size_t SHSEncryptoBox_GetEncryptedSize(SHSEncryptoBox *box, size_t inputSize) { 335 | return internal(box)->encryptedSize(inputSize); 336 | 337 | } 338 | 339 | SHSStatus SHSEncryptoBox_Encrypt(SHSEncryptoBox *box, SHSInputBuffer in, SHSOutputBuffer* out) { 340 | return (SHSStatus)internal(box)->encrypt(internal(in), internal(out)); 341 | } 342 | 343 | SHSDecryptoBox* SHSDecryptoBox_Create(const SHSSession *session, SHSCryptoBoxProtocol protocol) { 344 | return external( new DecryptoBox(*(Session*)session, (CryptoBox::Protocol)protocol)); 345 | } 346 | 347 | void SHSDecryptoBox_Free(SHSDecryptoBox *box) { 348 | delete internal(box); 349 | } 350 | 351 | size_t SHSDecryptoBox_MinPeekSize(SHSDecryptoBox *box) { 352 | return internal(box)->minPeekSize(); 353 | } 354 | 355 | SHSPeekResult SHSDecryptoBox_Peek(SHSDecryptoBox *box, SHSInputBuffer in) { 356 | DecryptoBox::PeekResult ir = internal(box)->peek(internal(in)); 357 | SHSPeekResult result; 358 | memcpy(&result, &ir, sizeof(result)); 359 | return result; 360 | } 361 | 362 | SHSStatus SHSDecryptoBox_Decrypt(SHSDecryptoBox *box, SHSInputBuffer *in, SHSOutputBuffer *out) { 363 | return (SHSStatus) internal(box)->decrypt(internal(in), internal(out)); 364 | } 365 | -------------------------------------------------------------------------------- /tests/SecretHandshakeTests.cc: -------------------------------------------------------------------------------- 1 | // 2 | // SecretHandshakeTests.cc 3 | // 4 | // Copyright © 2021 Jens Alfke. All rights reserved. 5 | // 6 | // Licensed under the MIT License: 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | #include "SecretHandshake.hh" 28 | #include "SecretStream.hh" 29 | #include "monocypher/base.hh" 30 | #include "hexString.hh" 31 | #include 32 | 33 | #include "catch.hpp" 34 | 35 | using namespace std; 36 | using namespace snej::shs; 37 | 38 | 39 | template 40 | static void randomize(std::array &array) { 41 | monocypher::randomize(array.data(), SIZE); 42 | } 43 | 44 | 45 | TEST_CASE("SecretKey", "[SecretHandshake]") { 46 | KeyPair kp = KeyPair::generate(); 47 | PublicKey pk = kp.publicKey; 48 | SigningKey sk = kp.signingKey; 49 | 50 | KeyPair kp2 = KeyPair(sk); 51 | PublicKey pk2 = kp2.publicKey; 52 | CHECK(kp2 == kp); 53 | CHECK(pk2 == pk); 54 | } 55 | 56 | 57 | TEST_CASE("AppID", "[SecretHandshake]") { 58 | AppID id = Context::appIDFromString(""); 59 | CHECK(hexString(id) == "00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000"); 60 | id = Context::appIDFromString("ABCDEF"); 61 | CHECK(hexString(id) == "41424344 45460000 00000000 00000000 00000000 00000000 00000000 00000000"); 62 | id = Context::appIDFromString("A string that is too long to fit in an AppID"); 63 | CHECK(hexString(id) == "41207374 72696E67 20746861 74206973 20746F6F 206C6F6E 6720746F 20666974"); 64 | } 65 | 66 | 67 | struct HandshakeTest { 68 | KeyPair serverKey, clientKey; 69 | ServerHandshake server; 70 | ClientHandshake client; 71 | 72 | HandshakeTest() 73 | :serverKey(KeyPair::generate()) 74 | ,clientKey(KeyPair::generate()) 75 | ,server({"App", serverKey}) 76 | ,client({"App", clientKey}, serverKey.publicKey) 77 | { } 78 | 79 | bool sendFromTo(Handshake &src, Handshake &dst, size_t expectedCount) { 80 | // One step of the handshake: 81 | CHECK(src.bytesToRead().second == 0); 82 | CHECK(dst.bytesToSend().second == 0); 83 | auto toSend = src.bytesToSend(); 84 | CHECK(toSend.second == expectedCount); 85 | auto toRead = dst.bytesToRead(); 86 | CHECK(toRead.second == toSend.second); 87 | memcpy(toRead.first, toSend.first, toSend.second); 88 | src.sendCompleted(); 89 | CHECK(!src.error()); 90 | bool ok = dst.readCompleted(); 91 | CHECK(ok == (dst.error() == Handshake::NoError)); 92 | return ok; 93 | } 94 | }; 95 | 96 | 97 | TEST_CASE_METHOD(HandshakeTest, "Handshake", "[SecretHandshake]") { 98 | // Run the handshake: 99 | REQUIRE(sendFromTo(client, server, 64)); 100 | REQUIRE(sendFromTo(server, client, 64)); 101 | REQUIRE(sendFromTo(client, server, 112)); 102 | REQUIRE(sendFromTo(server, client, 80)); 103 | 104 | REQUIRE(server.finished()); 105 | REQUIRE(client.finished()); 106 | 107 | // Check that they ended up with matching session keys, and each other's public keys: 108 | Session clientSession = client.session(), serverSession = server.session(); 109 | CHECK(clientSession.encryptionKey == serverSession.decryptionKey); 110 | CHECK(clientSession.encryptionNonce == serverSession.decryptionNonce); 111 | CHECK(clientSession.decryptionKey == serverSession.encryptionKey); 112 | CHECK(clientSession.decryptionNonce == serverSession.encryptionNonce); 113 | 114 | CHECK(serverSession.peerPublicKey == clientKey.publicKey); 115 | CHECK(clientSession.peerPublicKey == serverKey.publicKey); 116 | } 117 | 118 | 119 | TEST_CASE_METHOD(HandshakeTest, "Handshake with wrong server key", "[SecretHandshake]") { 120 | // Create a client that has the wrong server public key: 121 | PublicKey badServerKey = serverKey.publicKey; 122 | badServerKey[17]++; 123 | ClientHandshake badClient({"App", clientKey}, badServerKey); 124 | 125 | // Run the handshake: 126 | CHECK(sendFromTo(badClient, server, 64)); 127 | CHECK(sendFromTo(server, badClient, 64)); 128 | CHECK(!sendFromTo(badClient, server, 112)); 129 | CHECK(server.error() == Handshake::AuthError); 130 | } 131 | 132 | 133 | extern "C" { 134 | bool test_C_Handshake(void); 135 | bool test_C_HandshakeWrongServerKey(void); 136 | } 137 | 138 | TEST_CASE("C Handshake", "[SecretHandshake]") { 139 | CHECK(test_C_Handshake()); 140 | } 141 | 142 | TEST_CASE("C Handshake with wrong server key", "[SecretHandshake]") { 143 | CHECK(test_C_HandshakeWrongServerKey()); 144 | } 145 | 146 | 147 | struct SessionTest { 148 | Session session1, session2; 149 | 150 | SessionTest() { 151 | randomize(session1.encryptionKey); 152 | randomize(session1.encryptionNonce); 153 | randomize(session1.decryptionKey); 154 | randomize(session1.decryptionNonce); 155 | 156 | session2.encryptionKey = session1.decryptionKey; 157 | session2.encryptionNonce = session1.decryptionNonce; 158 | session2.decryptionKey = session1.encryptionKey; 159 | session2.decryptionNonce = session1.encryptionNonce; 160 | } 161 | }; 162 | 163 | 164 | using getSizeResult = std::pair; 165 | 166 | 167 | TEST_CASE_METHOD(SessionTest, "Encrypted Messages", "[SecretHandshake]") { 168 | auto protocol = GENERATE(CryptoBox::Compact, CryptoBox::BoxStream); 169 | EncryptoBox box1(session1, protocol); 170 | DecryptoBox box2(session2, protocol); 171 | cerr << "\t---- protocol=" << int(protocol) << endl; 172 | 173 | // Encrypt a message: 174 | constexpr const char *kCleartext = "Beware the ides of March. We attack at dawn."; 175 | input_data inClear = {kCleartext, strlen(kCleartext)}; 176 | 177 | // Encrypt: 178 | uint8_t cipherBuf[256] = {}; 179 | output_buffer outCipher = {cipherBuf, 0}; 180 | CHECK(box1.encrypt(inClear, outCipher) == OutTooSmall); 181 | outCipher.size = inClear.size; 182 | CHECK(box1.encrypt(inClear, outCipher) == OutTooSmall); 183 | outCipher.size = box1.encryptedSize(inClear.size); 184 | CHECK(box1.encrypt(inClear, outCipher) == Success); 185 | CHECK(outCipher.data == cipherBuf); 186 | CHECK(outCipher.size == box1.encryptedSize(inClear.size)); 187 | 188 | // Decrypt: 189 | uint8_t clearBuf[256] = {}; 190 | CHECK(box2.getDecryptedSize({cipherBuf, 0}) == getSizeResult{IncompleteInput, 0}); 191 | CHECK(box2.getDecryptedSize({cipherBuf, 1}) == getSizeResult{IncompleteInput, 0}); 192 | if (protocol != CryptoBox::BoxStream) { 193 | CHECK(box2.getDecryptedSize({cipherBuf, 2}) == getSizeResult{Success, inClear.size}); 194 | } 195 | CHECK(box2.getDecryptedSize({cipherBuf, sizeof(cipherBuf)}) == getSizeResult{Success, inClear.size}); 196 | 197 | input_data inCipher = {cipherBuf, 0}; 198 | output_buffer outClear = {clearBuf, sizeof(clearBuf)}; 199 | CHECK(box2.decrypt(inCipher, outClear) == IncompleteInput); 200 | inCipher.size = 2; 201 | CHECK(box2.decrypt(inCipher, outClear) == IncompleteInput); 202 | inCipher.size = outCipher.size - 1; 203 | CHECK(box2.decrypt(inCipher, outClear) == IncompleteInput); 204 | inCipher.size = outCipher.size; 205 | CHECK(box2.decrypt(inCipher, outClear) == Success); 206 | CHECK(inCipher.size == 0); 207 | CHECK(inCipher.data == &cipherBuf[outCipher.size]); 208 | CHECK(outClear.data == clearBuf); 209 | CHECK(outClear.size == inClear.size); 210 | CHECK(memcmp(kCleartext, outClear.data, outClear.size) == 0); 211 | 212 | // Encrypt another message: 213 | constexpr const char *kMoreCleartext = "Alea jacta est"; 214 | inClear = {kMoreCleartext, strlen(kMoreCleartext)}; 215 | outCipher = {cipherBuf, sizeof(cipherBuf)}; 216 | CHECK(box1.encrypt(inClear, outCipher) == Success); 217 | CHECK(outCipher.data == cipherBuf); 218 | CHECK(outCipher.size == box1.encryptedSize(inClear.size)); 219 | 220 | // Decrypt it: 221 | inCipher = {cipherBuf, sizeof(cipherBuf)}; 222 | outClear = {clearBuf, sizeof(clearBuf)}; 223 | CHECK(box2.decrypt(inCipher, outClear) == Success); 224 | CHECK(inCipher.size == sizeof(cipherBuf) - outCipher.size); 225 | CHECK(inCipher.data == &cipherBuf[outCipher.size]); 226 | CHECK(outClear.data == clearBuf); 227 | CHECK(outClear.size == inClear.size); 228 | CHECK(memcmp(kMoreCleartext, outClear.data, outClear.size) == 0); 229 | } 230 | 231 | 232 | TEST_CASE_METHOD(SessionTest, "Encrypted Messages Overlapping Buffers", "[SecretHandshake]") { 233 | auto protocol = GENERATE(CryptoBox::Compact, CryptoBox::BoxStream); 234 | EncryptoBox box1(session1, protocol); 235 | DecryptoBox box2(session2, protocol); 236 | cerr << "\t---- protocol=" << int(protocol) << endl; 237 | 238 | // Check that it's OK to use the same buffer for the input and the output: 239 | constexpr const char *kCleartext = "Beware the ides of March. We attack at dawn."; 240 | char buffer[256]; 241 | strcpy(buffer, kCleartext); 242 | input_data inClear = {buffer, strlen(kCleartext)}; 243 | output_buffer outCipher = {buffer, sizeof(buffer)}; 244 | CHECK(box1.encrypt(inClear, outCipher) == Success); 245 | 246 | if (protocol != CryptoBox::BoxStream) { 247 | CHECK(box2.getDecryptedSize({buffer, 2}) == getSizeResult{Success, inClear.size}); 248 | } 249 | 250 | input_data inCipher = {buffer, sizeof(buffer)}; 251 | output_buffer outClear = {buffer, sizeof(buffer)}; 252 | CHECK(box2.decrypt(inCipher, outClear) == Success); 253 | CHECK(inCipher.size == sizeof(buffer) - outCipher.size); 254 | CHECK(inCipher.data == &buffer[outCipher.size]); 255 | CHECK(outClear.data == buffer); 256 | CHECK(outClear.size == inClear.size); 257 | CHECK(memcmp(kCleartext, outClear.data, outClear.size) == 0); 258 | } 259 | 260 | 261 | TEST_CASE_METHOD(SessionTest, "Decryption Stream", "[SecretHandshake]") { 262 | auto protocol = GENERATE(CryptoBox::Compact, CryptoBox::BoxStream); 263 | size_t kEncOverhead = 18 + (protocol == CryptoBox::BoxStream) * 16; 264 | cerr << "\t---- protocol=" << int(protocol) << endl; 265 | 266 | EncryptionStream enc(session1, protocol); 267 | DecryptionStream dec(session2, protocol); 268 | char cipherBuf[256], clearBuf[256]; 269 | 270 | CHECK(dec.pull(clearBuf, sizeof(clearBuf)) == 0); 271 | 272 | auto transfer = [&](size_t nBytes) { 273 | nBytes = enc.pull(cipherBuf, nBytes); 274 | CHECK(dec.push(cipherBuf, nBytes)); 275 | }; 276 | 277 | // Encrypt a message: 278 | enc.pushPartial("Hel", 3); 279 | CHECK(enc.bytesAvailable() == 0); 280 | enc.pushPartial("lo", 2); 281 | CHECK(enc.bytesAvailable() == 0); 282 | enc.flush(); 283 | CHECK(enc.bytesAvailable() == 5 + kEncOverhead); 284 | 285 | // Transfer it in two parts: 286 | transfer(10); 287 | CHECK(enc.bytesAvailable() == 5 + kEncOverhead - 10); 288 | CHECK(dec.bytesAvailable() == 0); 289 | transfer(100); 290 | CHECK(enc.bytesAvailable() == 0); 291 | CHECK(dec.bytesAvailable() == 5); 292 | 293 | // Read it: 294 | size_t bytesRead = dec.pull(clearBuf, sizeof(clearBuf)); 295 | CHECK(bytesRead == 5); 296 | CHECK(memcmp(clearBuf, "Hello", 5) == 0); 297 | 298 | // Now add two encrypted mesages, but only transfer the first: 299 | enc.push(" there", 6); 300 | enc.pushPartial(", world", 7); 301 | transfer(100); 302 | enc.flush(); 303 | CHECK(enc.bytesAvailable() == 7 + kEncOverhead); 304 | 305 | // Now read part of the first: 306 | CHECK(dec.bytesAvailable() == 6); 307 | size_t n = dec.pull(&clearBuf[bytesRead], 3); 308 | CHECK(n == 3); 309 | bytesRead += n; 310 | CHECK(memcmp(clearBuf, "Hello th", bytesRead) == 0); 311 | 312 | // Transfer the second: 313 | transfer(100); 314 | CHECK(enc.bytesAvailable() == 0); 315 | CHECK(dec.bytesAvailable() == 10); 316 | 317 | // Read the rest: 318 | n = dec.pull(&clearBuf[bytesRead], 100); 319 | CHECK(n == 10); 320 | bytesRead += n; 321 | CHECK(memcmp(clearBuf, "Hello there, world", bytesRead) == 0); 322 | CHECK(dec.pull(&clearBuf[bytesRead], 100) == 0); 323 | CHECK(dec.bytesAvailable() == 0); 324 | } 325 | 326 | 327 | TEST_CASE_METHOD(SessionTest, "Decryption Stream large data", "[SecretHandshake]") { 328 | auto protocol = GENERATE(CryptoBox::Compact, CryptoBox::BoxStream); 329 | size_t kEncOverhead = 18 + (protocol == CryptoBox::BoxStream) * 16; 330 | cerr << "\t---- protocol=" << int(protocol) << endl; 331 | 332 | EncryptionStream enc(session1, protocol); 333 | DecryptionStream dec(session2, protocol); 334 | 335 | static constexpr size_t kBufSize = 1000; 336 | array cipherBuf, clearBuf; 337 | 338 | CHECK(dec.pull(clearBuf.data(), sizeof(clearBuf)) == 0); 339 | 340 | auto transfer = [&](size_t nBytes) { 341 | assert(nBytes <= kBufSize); 342 | nBytes = enc.pull(cipherBuf.data(), nBytes); 343 | CHECK(dec.push(cipherBuf.data(), nBytes)); 344 | }; 345 | 346 | static constexpr size_t kMessageSize = 100000; 347 | static_assert(kMessageSize > EncryptoBox::kMaxMessageSize); 348 | auto message = vector(kMessageSize); 349 | monocypher::randomize(message.data(), message.size()); 350 | size_t messagePos = 0; 351 | 352 | // Encrypt a 30,000-byte message: 353 | enc.pushPartial(&message[messagePos], 20000); messagePos += 20000; 354 | CHECK(enc.bytesAvailable() == 0); 355 | enc.pushPartial(&message[messagePos], 10000); messagePos += 10000; 356 | CHECK(enc.bytesAvailable() == 0); 357 | enc.flush(); 358 | CHECK(enc.bytesAvailable() == 30000 + kEncOverhead); 359 | 360 | // Transfer it in two parts: 361 | for (int i = 0; i < 31; ++i) 362 | transfer(1000); 363 | CHECK(enc.bytesAvailable() == 0); 364 | CHECK(dec.bytesAvailable() == 30000); 365 | 366 | // Read it: 367 | auto gotMessage = vector(100000); 368 | size_t bytesRead = dec.pull(gotMessage.data(), gotMessage.size()); 369 | CHECK(bytesRead == 30000); 370 | CHECK(memcmp(gotMessage.data(), message.data(), bytesRead) == 0); 371 | 372 | // Encrypt the remaining 70,000 bytes at once, exceeding the max box size: 373 | static_assert(40000 + 30000 > EncryptoBox::kMaxMessageSize); 374 | enc.pushPartial(&message[messagePos], 40000); messagePos += 40000; 375 | CHECK(enc.bytesAvailable() == 0); 376 | enc.pushPartial(&message[messagePos], 30000); messagePos += 30000; 377 | REQUIRE(messagePos == message.size()); 378 | //CHECK(enc.bytesAvailable() == 0); 379 | enc.flush(); 380 | CHECK(enc.bytesAvailable() == 70000 + 2 * kEncOverhead); 381 | 382 | // Transfer it in parts: 383 | for (int i = 0; i < 71; ++i) 384 | transfer(1000); 385 | CHECK(enc.bytesAvailable() == 0); 386 | CHECK(dec.bytesAvailable() == 70000); 387 | 388 | // Read it: 389 | bytesRead = dec.pull(gotMessage.data(), gotMessage.size()); 390 | CHECK(bytesRead == 70000); 391 | CHECK(memcmp(gotMessage.data(), &message[30000], bytesRead) == 0); 392 | } 393 | -------------------------------------------------------------------------------- /capnproto/SecretRPC.cc: -------------------------------------------------------------------------------- 1 | // 2 | // SecretRPC.cc 3 | // 4 | // Copyright © 2021 Jens Alfke. All rights reserved. 5 | // 6 | // Licensed under the MIT License: 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | 26 | 27 | // Adapted from Cap'n Proto's ez-rpc.c++ 28 | // 29 | // Copyright (c) 2013-2014 Sandstorm Development Group, Inc. and contributors 30 | 31 | #include "SecretRPC.hh" 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | 38 | namespace snej::shs { 39 | using namespace capnp; 40 | 41 | class RPCContext; 42 | 43 | KJ_THREADLOCAL_PTR(RPCContext) threadSecretContext = nullptr; 44 | 45 | RPCContext::RPCContext(): ioContext(kj::setupAsyncIo()) { 46 | threadSecretContext = this; 47 | } 48 | 49 | RPCContext::~RPCContext() noexcept(false) { 50 | KJ_REQUIRE(threadSecretContext == this, 51 | "SecretRPCContext destroyed from different thread than it was created.") { 52 | return; 53 | } 54 | threadSecretContext = nullptr; 55 | } 56 | 57 | kj::Own RPCContext::getThreadLocal() { 58 | RPCContext* existing = threadSecretContext; 59 | if (existing != nullptr) { 60 | return kj::addRef(*existing); 61 | } else { 62 | return kj::refcounted(); 63 | } 64 | } 65 | 66 | 67 | #pragma mark - SERVER IMPL: 68 | 69 | 70 | struct SecretRPCServer::Impl final : public SturdyRefRestorer, 71 | public kj::TaskSet::ErrorHandler 72 | { 73 | struct ExportedCap { 74 | kj::String name; 75 | Capability::Client cap = nullptr; 76 | 77 | ExportedCap(kj::StringPtr name, Capability::Client cap) 78 | : name(kj::heapString(name)), cap(cap) {} 79 | 80 | ExportedCap() = default; 81 | ExportedCap(const ExportedCap&) = delete; 82 | ExportedCap(ExportedCap&&) = default; 83 | ExportedCap& operator=(const ExportedCap&) = delete; 84 | ExportedCap& operator=(ExportedCap&&) = default; 85 | // Make std::map happy... 86 | }; 87 | 88 | 89 | // Represents an incoming client connection. 90 | struct Connection : public SturdyRefRestorer { 91 | #pragma GCC diagnostic push 92 | #pragma GCC diagnostic ignored "-Wdeprecated-declarations" // SturdyRefRestorer is deprecated 93 | Connection(kj::AuthenticatedStream&& authStream, 94 | Capability::Client mainInterface, 95 | SturdyRefRestorer& parentRestorer, 96 | ReaderOptions readerOpts) 97 | :_stream(kj::mv(authStream)) 98 | ,_network(*this->_stream.stream, capnp::rpc::twoparty::Side::SERVER, readerOpts) 99 | ,_mainInterface(mainInterface) 100 | ,_parentRestorer(parentRestorer) 101 | ,_rpcSystem(makeRpcServer(_network, *this)) 102 | { } 103 | 104 | virtual ~Connection() = default; 105 | 106 | Capability::Client restore(AnyPointer::Reader objectId) override { 107 | if (objectId.isNull()) { 108 | return _mainInterface; 109 | } else { 110 | return _parentRestorer.restore(objectId); 111 | } 112 | } 113 | #pragma GCC diagnostic pop 114 | 115 | kj::AuthenticatedStream _stream; 116 | TwoPartyVatNetwork _network; 117 | Capability::Client _mainInterface; 118 | SturdyRefRestorer& _parentRestorer; 119 | RpcSystem _rpcSystem; 120 | }; 121 | 122 | 123 | Impl(MainInterfaceFactory mainInterfaceFactory, 124 | ReaderOptions readerOpts, 125 | kj::Own shsWrapper) 126 | :_mainInterfaceFactory(kj::mv(mainInterfaceFactory)) 127 | ,_context(RPCContext::getThreadLocal()) 128 | ,_portPromise(nullptr) 129 | ,_tasks(*this) 130 | ,_readerOptions(readerOpts) 131 | ,_shsWrapper(kj::mv(shsWrapper)) 132 | { } 133 | 134 | 135 | Impl(MainInterfaceFactory mainInterfaceFactory, 136 | kj::StringPtr bindAddress, 137 | uint defaultPort, 138 | ReaderOptions readerOpts, 139 | kj::Own shsWrapper) 140 | :Impl(mainInterfaceFactory, readerOpts, kj::mv(shsWrapper)) 141 | { 142 | auto paf = kj::newPromiseAndFulfiller(); 143 | _portPromise = paf.promise.fork(); 144 | 145 | _tasks.add(_context->getIoProvider().getNetwork().parseAddress(bindAddress, defaultPort) 146 | .then(kj::mvCapture(paf.fulfiller, 147 | [this](kj::Own>&& portFulfiller, 148 | kj::Own&& addr) { 149 | _listener = addr->listen(); 150 | portFulfiller->fulfill(_listener->getPort()); 151 | acceptLoop(); 152 | }))); 153 | } 154 | 155 | void acceptLoop() { 156 | KJ_LOG(INFO, "SecretRPCServer now accepting connections..."); 157 | _tasks.add(_listener->acceptAuthenticated() 158 | .then([this](kj::AuthenticatedStream&& stream) { 159 | std::string from = getPeerName(*stream.stream); 160 | KJ_LOG(INFO, "SecretRPCServer accepted socket", from); 161 | acceptLoop(); 162 | auto &timer = _context->getIoProvider().getTimer(); 163 | return timer.timeoutAfter(15 * kj::SECONDS, 164 | _shsWrapper->wrap(kj::mv(stream))); 165 | }, [this](kj::Exception &&x) { 166 | KJ_LOG(ERROR, "SecretRPCServer failed to accept socket"); 167 | acceptLoop(); 168 | return nullptr; 169 | }).then([this](kj::AuthenticatedStream&& secretStream) { 170 | startConnection(kj::mv(secretStream), _readerOptions); 171 | })); 172 | } 173 | 174 | void acceptStream(kj::Promise &&streamPromise, 175 | ReaderOptions readerOpts) 176 | { 177 | streamPromise = StreamWrapper::asyncWrap(_shsWrapper.get(), kj::mv(streamPromise)); 178 | _tasks.add(streamPromise.then([this, readerOpts](kj::AuthenticatedStream&& stream) { 179 | startConnection(kj::mv(stream), readerOpts); 180 | })); 181 | } 182 | 183 | void startConnection(kj::AuthenticatedStream&& stream, ReaderOptions readerOpts) { 184 | std::string from = getPeerName(*stream.stream); 185 | KJ_LOG(INFO, "SecretRPCServer starting RPC connection", from); 186 | auto peerID = dynamic_cast(stream.peerIdentity.get()); 187 | auto mainInterface = _mainInterfaceFactory(peerID); 188 | 189 | auto connection = kj::heap(kj::mv(stream), mainInterface, *this, 190 | readerOpts); 191 | // Arrange to destroy the Connection when all references are gone, or when the 192 | // Server is destroyed (which will destroy the TaskSet). 193 | _tasks.add(connection->_network.onDisconnect().attach(kj::mv(connection))); 194 | } 195 | 196 | Capability::Client restore(AnyPointer::Reader objectId) override { 197 | auto name = objectId.getAs(); 198 | auto iter = _exportMap.find(name); 199 | if (iter == _exportMap.end()) { 200 | KJ_FAIL_REQUIRE("Server exports no such capability.", name) { break; } 201 | return nullptr; 202 | } else { 203 | return iter->second.cap; 204 | } 205 | } 206 | 207 | void taskFailed(kj::Exception&& exception) override { 208 | KJ_LOG(ERROR, "SecretRPCServer task failed", exception.getDescription()); 209 | } 210 | 211 | MainInterfaceFactory _mainInterfaceFactory; 212 | kj::Own _context; 213 | kj::ForkedPromise _portPromise; 214 | kj::TaskSet _tasks; 215 | ReaderOptions _readerOptions; 216 | kj::Own _shsWrapper; 217 | std::map _exportMap; 218 | kj::Own _listener; 219 | }; 220 | 221 | 222 | #pragma mark - PUBLIC SERVER API: 223 | 224 | 225 | SecretRPCServer::SecretRPCServer(kj::Own shsContext, 226 | MainInterfaceFactory mainInterfaceFactory, 227 | kj::StringPtr bindAddress, 228 | uint16_t defaultPort, 229 | capnp::ReaderOptions readerOpts) 230 | :_impl(kj::heap(mainInterfaceFactory, bindAddress, defaultPort, readerOpts, 231 | kj::mv(shsContext))) 232 | { } 233 | 234 | SecretRPCServer::SecretRPCServer(kj::Own shsContext, 235 | MainInterfaceFactory mainInterfaceFactory, 236 | capnp::ReaderOptions readerOpts) 237 | :_impl(kj::heap(mainInterfaceFactory, readerOpts, kj::mv(shsContext))) 238 | { } 239 | 240 | SecretRPCServer::~SecretRPCServer() noexcept(false) { } 241 | 242 | kj::Promise SecretRPCServer::getPort() { 243 | return _impl->_portPromise.addBranch(); 244 | } 245 | 246 | kj::WaitScope& SecretRPCServer::getWaitScope() { 247 | return _impl->_context->getWaitScope(); 248 | } 249 | 250 | kj::AsyncIoProvider& SecretRPCServer::getIoProvider() { 251 | return _impl->_context->getIoProvider(); 252 | } 253 | 254 | kj::LowLevelAsyncIoProvider& SecretRPCServer::getLowLevelIoProvider() { 255 | return _impl->_context->getLowLevelIoProvider(); 256 | } 257 | 258 | void SecretRPCServer::acceptStream(kj::Promise streamPromise, 259 | ReaderOptions readerOpts) 260 | { 261 | _impl->acceptStream(kj::mv(streamPromise), readerOpts); 262 | } 263 | 264 | 265 | #pragma mark - CLIENT IMPL: 266 | 267 | 268 | static kj::Promise> connectAttach(kj::Own&& addr) { 269 | return addr->connect().attach(kj::mv(addr)); 270 | } 271 | 272 | struct SecretRPCClient::Impl { 273 | 274 | struct ClientContext { 275 | kj::Own stream; 276 | TwoPartyVatNetwork network; 277 | RpcSystem rpcSystem; 278 | 279 | ClientContext(kj::Own&& stream, 280 | ReaderOptions readerOpts) 281 | :stream(kj::mv(stream)) 282 | ,network(*this->stream, capnp::rpc::twoparty::Side::CLIENT, readerOpts) 283 | ,rpcSystem(makeRpcClient(network)) 284 | { } 285 | 286 | Capability::Client getMain() { 287 | word scratch[4]; 288 | memset(scratch, 0, sizeof(scratch)); 289 | MallocMessageBuilder message(scratch); 290 | auto hostId = message.getRoot(); 291 | hostId.setSide(capnp::rpc::twoparty::Side::SERVER); 292 | return rpcSystem.bootstrap(hostId); 293 | } 294 | 295 | Capability::Client restore(kj::StringPtr name) { 296 | word scratch[64]; 297 | memset(scratch, 0, sizeof(scratch)); 298 | MallocMessageBuilder message(scratch); 299 | 300 | auto hostIdOrphan = message.getOrphanage().newOrphan(); 301 | auto hostId = hostIdOrphan.get(); 302 | hostId.setSide(capnp::rpc::twoparty::Side::SERVER); 303 | 304 | auto objectId = message.getRoot(); 305 | objectId.setAs(name); 306 | #pragma GCC diagnostic push 307 | #pragma GCC diagnostic ignored "-Wdeprecated-declarations" 308 | return rpcSystem.restore(hostId, objectId); 309 | #pragma GCC diagnostic pop 310 | } 311 | }; 312 | 313 | 314 | Impl(kj::Own shsWrapper, 315 | ReaderOptions readerOpts, 316 | kj::Promise> streamPromise) 317 | :_context(RPCContext::getThreadLocal()) 318 | ,_shsWrapper(kj::mv(shsWrapper)) 319 | ,_setupPromise(ClientWrapper::asyncWrap(_shsWrapper.get(), kj::mv(streamPromise)) 320 | .then([this, readerOpts](kj::Own&& stream) { 321 | _clientContext = kj::heap(kj::mv(stream), readerOpts); 322 | }).fork()) 323 | { } 324 | 325 | 326 | Capability::Client getMain() { 327 | KJ_IF_MAYBE(client, _clientContext) { 328 | return client->get()->getMain(); 329 | } else { 330 | return _setupPromise.addBranch().then([this]() { 331 | return KJ_ASSERT_NONNULL(_clientContext)->getMain(); 332 | }); 333 | } 334 | } 335 | 336 | template 337 | inline typename Type::Client getMain() { 338 | return getMain().castAs(); 339 | } 340 | 341 | kj::Own _context; 342 | kj::Own _shsWrapper; 343 | kj::ForkedPromise _setupPromise; 344 | kj::Maybe> _clientContext; // Filled in before `setupPromise` resolves. 345 | }; 346 | 347 | 348 | #pragma mark - PUBLIC CLIENT API: 349 | 350 | 351 | SecretRPCClient::SecretRPCClient(kj::Own shsContext, 352 | kj::StringPtr serverAddress, 353 | uint16_t serverPort, 354 | capnp::ReaderOptions readerOpts) 355 | { 356 | kj::Own context = RPCContext::getThreadLocal(); 357 | auto streamPromise = context->getIoProvider() 358 | .getNetwork() 359 | .parseAddress(serverAddress, serverPort) 360 | .then([](kj::Own&& addr) { 361 | return connectAttach(kj::mv(addr)); 362 | }); 363 | _impl = kj::heap(kj::mv(shsContext), readerOpts, kj::mv(streamPromise)); 364 | } 365 | 366 | SecretRPCClient::SecretRPCClient(kj::Own shsContext, 367 | kj::Promise> streamPromise, 368 | capnp::ReaderOptions readerOpts) 369 | :_impl(kj::heap(kj::mv(shsContext), readerOpts, kj::mv(streamPromise))) 370 | { } 371 | 372 | SecretRPCClient::~SecretRPCClient() noexcept(false) { } 373 | 374 | Capability::Client SecretRPCClient::getMain() { 375 | KJ_IF_MAYBE(client, _impl->_clientContext) { 376 | return client->get()->getMain(); 377 | } else { 378 | return _impl->_setupPromise.addBranch().then([this]() { 379 | return KJ_ASSERT_NONNULL(_impl->_clientContext)->getMain(); 380 | }); 381 | } 382 | } 383 | 384 | SecretRPCClient::SecretRPCClient(SecretRPCClient &&other) 385 | :_impl(kj::mv(other._impl)) 386 | { } 387 | 388 | 389 | kj::WaitScope& SecretRPCClient::getWaitScope() { 390 | return _impl->_context->getWaitScope(); 391 | } 392 | 393 | kj::AsyncIoProvider& SecretRPCClient::getIoProvider() { 394 | return _impl->_context->getIoProvider(); 395 | } 396 | 397 | kj::LowLevelAsyncIoProvider& SecretRPCClient::getLowLevelIoProvider() { 398 | return _impl->_context->getLowLevelIoProvider(); 399 | } 400 | 401 | } 402 | --------------------------------------------------------------------------------