├── .gitignore ├── .gitmodules ├── .onedev-buildspec.yml ├── CMakeLists.txt ├── CMakePresets.json ├── LICENSE ├── README.md ├── include ├── client │ ├── web_socket_client.hpp │ └── websocketpp_client.hpp ├── cryptography │ ├── bech32.hpp │ └── nostr_bech32.hpp ├── data │ └── data.hpp ├── nostr.hpp ├── service │ ├── nostr_service.hpp │ ├── nostr_service_base.hpp │ └── nostr_service_specializations.hpp └── signer │ ├── noscrypt_signer.hpp │ └── signer.hpp ├── maintainers.yaml ├── src ├── client │ └── websocketpp_client.cpp ├── cryptography │ ├── bech32.cpp │ ├── noscrypt_cipher.cpp │ ├── noscrypt_cipher.hpp │ ├── nostr_bech32.cpp │ ├── nostr_secure_rng.cpp │ └── nostr_secure_rng.hpp ├── data │ ├── event.cpp │ ├── filters.cpp │ └── test_scenarios.md ├── internal │ ├── noscrypt_logger.cpp │ └── noscrypt_logger.hpp ├── service │ └── nostr_service_base.cpp └── signer │ ├── noscrypt_signer.cpp │ └── test_scenarios.md ├── test ├── nostr_bech32_test.cpp ├── nostr_event_test.cpp └── nostr_service_base_test.cpp ├── vcpkg-configuration.json └── vcpkg.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # Outputs 35 | build/ 36 | 37 | # VS Code Settings 38 | .vscode/ 39 | 40 | # CMake outputs 41 | _deps/ 42 | CMakeFiles/ 43 | out/ 44 | Testing/ 45 | Makefile 46 | CTestTestfile.cmake 47 | CMakeCache.txt 48 | cmake_install.cmake 49 | 50 | # vcpkg 51 | vcpkg_installed/ 52 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vcpkg"] 2 | path = vcpkg 3 | url = https://github.com/microsoft/vcpkg.git 4 | -------------------------------------------------------------------------------- /.onedev-buildspec.yml: -------------------------------------------------------------------------------- 1 | version: 38 2 | jobs: 3 | - name: Github Push 4 | steps: 5 | - !PushRepository 6 | name: aedile-ndk 7 | remoteUrl: https://github.com/ShadowySupercode/aedile-ndk 8 | passwordSecret: github_access_token 9 | force: false 10 | condition: ALL_PREVIOUS_STEPS_WERE_SUCCESSFUL 11 | triggers: 12 | - !BranchUpdateTrigger {} 13 | retryCondition: never 14 | maxRetries: 3 15 | retryDelay: 30 16 | timeout: 14400 17 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.19) 2 | 3 | if(CMAKE_VERSION VERSION_GREATER "3.24.0") 4 | # FIXME this policy isn't compatible with versions prior to 3.24 5 | cmake_policy(SET CMP0135 NEW) 6 | endif() 7 | project(aedile VERSION 0.0.3) 8 | 9 | include(ExternalProject) 10 | include(FetchContent) 11 | 12 | # Specify the C/C++ standard 13 | set(CMAKE_C_STANDARD 11) 14 | set(CMAKE_CXX_STANDARD 17) 15 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 16 | set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=native") 17 | set(CMAKE_POSITION_INDEPENDENT_CODE ON) 18 | 19 | get_directory_property(HAS_PARENT PARENT_DIRECTORY) 20 | if(HAS_PARENT) 21 | message(STATUS "Configuring as a subproject.") 22 | 23 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/../out/${CMAKE_BUILD_TYPE}/bin/) 24 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/../out/${CMAKE_BUILD_TYPE}/lib/) 25 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/../out/${CMAKE_BUILD_TYPE}/lib/) 26 | set(CMAKE_INSTALL_PREFIX ${CMAKE_SOURCE_DIR}/../env/) 27 | 28 | if(DEFINED ENV{WORKSPACE}) 29 | list(APPEND CMAKE_PREFIX_PATH $ENV{WORKSPACE}/env) 30 | else() 31 | list(APPEND CMAKE_PREFIX_PATH ${CMAKE_SOURCE_DIR}/../env) 32 | endif() 33 | else() 34 | message(STATUS "Configuring as a standalone project.") 35 | 36 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}/bin/) 37 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}/lib/) 38 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/out/${CMAKE_BUILD_TYPE}/lib/) 39 | endif() 40 | 41 | #======== Find dependencies ========# 42 | find_package(nlohmann_json CONFIG REQUIRED) 43 | find_package(OpenSSL REQUIRED) 44 | find_package(plog CONFIG REQUIRED) 45 | find_package(websocketpp CONFIG REQUIRED) 46 | 47 | #======== Configure uuid_v4 ========# 48 | 49 | FetchContent_Declare( 50 | uuid_v4 51 | GIT_REPOSITORY https://github.com/crashoz/uuid_v4.git 52 | GIT_TAG v1.0.0 53 | ) 54 | FetchContent_Populate(uuid_v4) 55 | 56 | FetchContent_GetProperties(uuid_v4) 57 | include_directories(${uuid_v4_SOURCE_DIR}) 58 | 59 | #======== Configure noscrypt ========# 60 | set(CRYPTO_LIB openssl) 61 | set(NC_ENABLE_UTILS ON) 62 | 63 | FetchContent_Declare( 64 | libnoscrypt 65 | GIT_REPOSITORY https://github.com/VnUgE/noscrypt.git 66 | GIT_TAG v0.1.5 67 | ) 68 | FetchContent_MakeAvailable(libnoscrypt) 69 | FetchContent_GetProperties(libnoscrypt) 70 | 71 | #======== Build the project ========# 72 | 73 | set(AEDILE_SOURCES 74 | "src/client/websocketpp_client.cpp" 75 | "src/cryptography/noscrypt_cipher.cpp" 76 | "src/cryptography/nostr_secure_rng.cpp" 77 | "src/cryptography/bech32.cpp" 78 | "src/cryptography/nostr_bech32.cpp" 79 | "src/data/event.cpp" 80 | "src/data/filters.cpp" 81 | "src/internal/noscrypt_logger.cpp" 82 | "src/service/nostr_service_base.cpp" 83 | "src/signer/noscrypt_signer.cpp" 84 | ) 85 | 86 | list(APPEND INCLUDE_DIR ./include) 87 | list(APPEND INCLUDE_DIR ${CMAKE_SOURCE_DIR}/build/linux/_deps/uuid_v4-src/) 88 | list(APPEND INCLUDE_DIR ${libnoscrypt_SOURCE_DIR}/include) 89 | 90 | add_library(aedile ${AEDILE_SOURCES}) 91 | target_link_libraries(aedile PRIVATE 92 | nlohmann_json::nlohmann_json 93 | OpenSSL::SSL 94 | OpenSSL::Crypto 95 | plog::plog 96 | websocketpp::websocketpp 97 | noscrypt 98 | ) 99 | target_include_directories(aedile PUBLIC ${INCLUDE_DIR}) 100 | set_target_properties(aedile PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES) 101 | 102 | #======== Build the tests ========# 103 | if(AEDILE_INCLUDE_TESTS) 104 | message(STATUS "Building unit tests.") 105 | 106 | enable_testing() 107 | include(GoogleTest) 108 | 109 | FetchContent_Declare( 110 | googletest 111 | URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip 112 | ) 113 | 114 | set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) 115 | FetchContent_MakeAvailable(googletest) 116 | 117 | enable_testing() 118 | 119 | set(TEST_SOURCES 120 | "test/nostr_event_test.cpp" 121 | "test/nostr_service_base_test.cpp" 122 | "test/nostr_bech32_test.cpp" 123 | ) 124 | 125 | add_executable(aedile_test ${TEST_SOURCES}) 126 | target_link_libraries(aedile_test PRIVATE 127 | GTest::gmock 128 | GTest::gtest 129 | GTest::gtest_main 130 | aedile 131 | nlohmann_json::nlohmann_json 132 | ) 133 | target_include_directories(aedile_test PUBLIC ${INCLUDE_DIR}) 134 | set_target_properties(aedile_test PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS YES) 135 | 136 | gtest_add_tests(TARGET aedile_test) 137 | endif() 138 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "configurePresets": [ 4 | { 5 | "name": "linux", 6 | "generator": "Unix Makefiles", 7 | "binaryDir": "${sourceDir}/build/linux", 8 | "cacheVariables": { 9 | "CMAKE_BUILD_TYPE": "Debug", 10 | "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake", 11 | "VCPKG_MANIFEST_MODE": "ON", 12 | "VCPKG_TARGET_TRIPLET": "x64-linux" 13 | } 14 | }, 15 | { 16 | "name": "linux tests", 17 | "generator": "Unix Makefiles", 18 | "binaryDir": "${sourceDir}/build/linux", 19 | "cacheVariables": { 20 | "AEDILE_INCLUDE_TESTS": "ON", 21 | "CMAKE_BUILD_TYPE": "Debug", 22 | "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake", 23 | "VCPKG_MANIFEST_MODE": "ON", 24 | "VCPKG_TARGET_TRIPLET": "x64-linux" 25 | } 26 | } 27 | ], 28 | "buildPresets": [ 29 | { 30 | "name": "linux", 31 | "configurePreset": "linux", 32 | "jobs": 4 33 | }, 34 | { 35 | "name": "linux tests", 36 | "configurePreset": "linux tests", 37 | "jobs": 4 38 | } 39 | ], 40 | "testPresets": [ 41 | { 42 | "name": "linux", 43 | "configurePreset": "linux tests" 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aedile 2 | 3 | A Nostr System Development Kit written in C++. 4 | 5 | ## Behind the Name 6 | 7 | In the ancient Roman Republic, the aediles were officials elected from among the plebians and charged with caring for Rome's public infrastructure and ensuring an accurate system of weights and measures. 8 | 9 | The aim of the Aedile SDK is in the spirit of that ancient office: 10 | 11 | - Provide a fast and efficient service for interacting with Nostr relays via WebSocket connections. 12 | - Offer stable, well-tested implementations of commonly-used [Nostr Implementation Possibilities (NIPs)](https://github.com/nostr-protocol/nips/tree/master). 13 | - Open up Nostr development by taking care of the basics so developers can focus on solving problems, rather than reimplementing the protocol. 14 | 15 | ## Building the SDK 16 | 17 | ### Prerequisites 18 | 19 | This project uses CMake as its build system, and vcpkg as its dependency manager. Thus, to build the SDK, you will need the following: 20 | 21 | - CMake 3.19 or later 22 | - A C++17 compiler 23 | - vcpkg 24 | 25 | CMake invokes vcpkg at the start of the configure process to install some of the project's dependencies. For this step to succeed, ensure that the `VCPKG_ROOT` environment variable is set to the path of your vcpkg installation. 26 | 27 | We have included vcpkg as a git submodule that needs to initialized and updated. The `VCPKG_ROOT` is already 28 | set within the `CMakePresets.json` file, so all that needs to be done is simply initializing the submodule 29 | and building the code. 30 | 31 | ```bash 32 | cd aedile-ndk 33 | git submodule init 34 | git submodule update 35 | ``` 36 | 37 | ### Building and Testing 38 | 39 | The SDK aims to support Linux, Windows, and macOS build targets. It currently supplies a CMake preset for Linux. 40 | 41 | #### Linux 42 | 43 | To build the SDK on Linux, run the following commands from the project root: 44 | 45 | ```bash 46 | cmake --build --preset="linux" 47 | ``` 48 | 49 | To run unit tests, use the following commands: 50 | 51 | ```bash 52 | cmake --build --preset="linux tests" 53 | ctest --preset="linux" 54 | ``` 55 | -------------------------------------------------------------------------------- /include/client/web_socket_client.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | namespace nostr 7 | { 8 | namespace client 9 | { 10 | /** 11 | * @brief An interface for a WebSocket client singleton. 12 | */ 13 | class IWebSocketClient 14 | { 15 | public: 16 | virtual ~IWebSocketClient() = default; 17 | 18 | /** 19 | * @brief Starts the client. 20 | * @remark This method must be called before any other client methods. 21 | */ 22 | virtual void start() = 0; 23 | 24 | /** 25 | * @brief Stops the client. 26 | * @remark This method should be called when the client is no longer needed, before it is 27 | * destroyed. 28 | */ 29 | virtual void stop() = 0; 30 | 31 | /** 32 | * @brief Opens a connection to the given server. 33 | */ 34 | virtual void openConnection(std::string uri) = 0; 35 | 36 | /** 37 | * @brief Indicates whether the client is connected to the given server. 38 | * @returns True if the client is connected, false otherwise. 39 | */ 40 | virtual bool isConnected(std::string uri) = 0; 41 | 42 | /** 43 | * @brief Sends the given message to the given server. 44 | * @returns A tuple indicating the server URI and whether the message was successfully 45 | * sent. 46 | */ 47 | virtual std::tuple send(std::string message, std::string uri) = 0; 48 | 49 | /** 50 | * @brief Sends the given message to the given server and sets up a message handler for 51 | * messages received from the server. 52 | * @returns A tuple indicating the server URI and whether the message was successfully 53 | * sent. 54 | * @remark Use this method to send a message and set up a message handler for responses in the 55 | * same call. 56 | */ 57 | virtual std::tuple send( 58 | std::string message, 59 | std::string uri, 60 | std::function messageHandler 61 | ) = 0; 62 | 63 | /** 64 | * @brief Sets up a message handler for the given server. 65 | * @param uri The URI of the server to which the message handler should be attached. 66 | * @param messageHandler A callable object that will be invoked with the payload the client 67 | * receives from the server. 68 | */ 69 | virtual void receive( 70 | std::string uri, 71 | std::function messageHandler 72 | ) = 0; 73 | 74 | /** 75 | * @brief Closes the connection to the given server. 76 | */ 77 | virtual void closeConnection(std::string uri) = 0; 78 | }; 79 | } // namespace client 80 | } // namespace nostr 81 | -------------------------------------------------------------------------------- /include/client/websocketpp_client.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include "client/web_socket_client.hpp" 11 | 12 | namespace nostr 13 | { 14 | namespace client 15 | { 16 | /** 17 | * @brief An implementation of the `IWebSocketClient` interface that uses the WebSocket++ library. 18 | */ 19 | class WebsocketppClient : public IWebSocketClient 20 | { 21 | public: 22 | void start() override; 23 | 24 | void stop() override; 25 | 26 | void openConnection(std::string uri) override; 27 | 28 | bool isConnected(std::string uri) override; 29 | 30 | std::tuple send(std::string message, std::string uri) override; 31 | 32 | std::tuple send( 33 | std::string message, 34 | std::string uri, 35 | std::function messageHandler 36 | ) override; 37 | 38 | void receive(std::string uri, std::function messageHandler) override; 39 | 40 | void closeConnection(std::string uri) override; 41 | 42 | private: 43 | typedef websocketpp::client websocketpp_client; 44 | typedef std::unordered_map::iterator connection_hdl_iterator; 45 | 46 | websocketpp_client _client; 47 | std::unordered_map _connectionHandles; 48 | std::mutex _propertyMutex; 49 | }; 50 | } // namespace client 51 | } // namespace nostr 52 | 53 | -------------------------------------------------------------------------------- /include/cryptography/bech32.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #define MAX_INPUT_LENGTH 256 15 | #define FROM_BITS 8 16 | #define TO_BITS 5 17 | 18 | typedef std::string PubKey, EventId, PrivKey, Tag; 19 | typedef std::vector TlvValues, BytesArray; 20 | typedef std::vector Relays; 21 | 22 | const int8_t bech32CharsetRev[128] = { 23 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 24 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 25 | -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 26 | 15, -1, 10, 17, 21, 20, 26, 30, 7, 5, -1, -1, -1, -1, -1, -1, 27 | -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, 28 | 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1, 29 | -1, 29, -1, 24, 13, 25, 9, 8, 23, -1, 18, 22, 31, 27, 19, -1, 30 | 1, 0, 3, 16, 11, 28, 12, 14, 6, 4, 2, -1, -1, -1, -1, -1 31 | }; 32 | 33 | const char bech32Charset[33] = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"; 34 | 35 | namespace nostr 36 | { 37 | namespace encoding 38 | { 39 | 40 | typedef enum { 41 | BECH32_ENCODING_NONE, 42 | BECH32_ENCODING_BECH32, 43 | BECH32_ENCODING_BECH32M 44 | } Bech32EncodingType; 45 | 46 | // supported protocols 47 | typedef enum { 48 | SEGWIT_BITCOIN, 49 | SEGWIT_NOSTR, 50 | SEGWIT_MAX 51 | } SegwitProtocol; 52 | 53 | class Bech32 54 | { 55 | public: 56 | /** 57 | * @brief Encode a SegWit address while managing the subtleties 58 | * betweem nostr segwit encoding and proper bitcoin segwit encoding. 59 | * 60 | * @param output string that will contain the encoded address. 61 | * @param hrp string containing human readable part to use 62 | * (chain/network specific). 63 | * @param ver Version of the witness program (between 0 and 16 inclusive, 64 | * should default to 0 when encoding a nostr address). 65 | * @param prog Data bytes for the witness program (between 2 and 40 bytes). 66 | * @param protocol An enum to indicate whether the encoding is for bitcoin 67 | * or for nostr, because it is not 100% the same. 68 | * For Nostr, the the witversion variable is not added to the beginning 69 | * of the data array. 70 | * @return int 71 | */ 72 | static int segwitAddrEncode( 73 | std::string &output, 74 | const std::string hrp, 75 | int ver, 76 | const BytesArray &prog, 77 | SegwitProtocol protocol 78 | ); 79 | 80 | /** 81 | * @brief Decode a SegWit address 82 | * 83 | * @param ver Pointer to an int that will be updated to contain the witness 84 | * program version (between 0 and 16 inclusive). 85 | * @param prog Vectorthat will be updated to 86 | * contain the witness program bytes. 87 | * @param hrp String human readable part that is 88 | * expected (chain/network specific). 89 | * @param addr String containing the address. 90 | * @param protocol An enum to indicate whether the encoding is for bitcoin 91 | * or for nostr, because it is not 100% the same. 92 | * For Nostr, the the witversion variable is not added to the beginning 93 | * of the data array. 94 | * @return int 1 if successful, otherwise 0. 95 | */ 96 | static int segwitAddrDecode( 97 | std::shared_ptr ver, 98 | BytesArray &prog, 99 | const std::string hrp, 100 | const std::string addr, 101 | SegwitProtocol protocol 102 | ); 103 | 104 | /** 105 | * @brief Encode a Bech32 or Bech32m string 106 | * 107 | * @param output String that will be updated to 108 | * contain the null-terminated Bech32 string. 109 | * @param hrp String for the human readable part. 110 | * @param data An array of 5-bit values. 111 | * @param max_input_len Maximum valid length of input (90 for segwit usage). 112 | * @param enc Which encoding to use (BECH32_ENCODING_BECH32{,M}). 113 | * @return int: 1 if successful, else 0 114 | */ 115 | static int encode( 116 | std::string &output, 117 | const std::string hrp, 118 | const BytesArray &data, 119 | std::size_t max_input_len, 120 | Bech32EncodingType enc 121 | ); 122 | 123 | 124 | /** 125 | * @brief Decode a Bech32 or Bech32m string. Decoded address 126 | * will be found at the address pointed to by `data`. 127 | * Will call `bech32_decode_len` if `data_len` is less than or equal to `max_input_len` 128 | * 129 | * @param hrp String for that will be 130 | * updated to contain the null-terminated human readable part. 131 | * @param data Vector of size strlen(input) - 8 that will 132 | * hold the encoded 5-bit data values. 133 | * @param input Bech32 string. 134 | * @param max_input_len Maximum valid length of input (90 for segwit usage). 135 | * @return bech32_encoding enum to specify which bech32 was just decoded 136 | */ 137 | static Bech32EncodingType decode( 138 | std::string &hrp, 139 | BytesArray &data, 140 | const std::string input, 141 | std::size_t max_input_len 142 | ); 143 | 144 | /** 145 | * @brief Decode a Bech32 or Bech32m string. Decoded address 146 | * will be found at the address pointed to by `data` if the value 147 | * pointed to by`data_len` is less than or equal to `max_input_len` 148 | * 149 | * @param hrp Pointer to a buffer of size strlen(input) - 6. Will be 150 | * updated to contain the null-terminated human readable part. 151 | * @param data Vector of size strlen(input) - 8 that will 152 | * hold the encoded 5-bit data values. 153 | * @param input Bech32 string. 154 | * @param max_input_len Maximum valid length of input (90 for segwit usage). 155 | * @return bech32_encoding enum to specify which bech32 was just decoded 156 | */ 157 | static Bech32EncodingType decodeLen( 158 | std::string &hrp, 159 | BytesArray &data, 160 | const std::string input, 161 | std::size_t max_input_len 162 | ); 163 | 164 | /* Helper from bech32: translates inbits-bit bytes to outbits-bit bytes. 165 | * @outlen is incremented as bytes are added. 166 | * @pad is true if we're to pad, otherwise truncate last byte if necessary 167 | */ 168 | 169 | /** 170 | * @brief Helper from bech32: translates inbits-bit bytes to outbits-bit bytes. 171 | * `outlen` is incremented as bytes are added. `pad` is true if we're to pad, 172 | * otherwise truncate last byte if necessary 173 | 174 | * @param out output buffer where the converted words are found 175 | * @param outbits the number of desired bits in each word in the converted output 176 | * @param in input buffer where the words to convert are found. 177 | * @param inbits the number of bits in each word in the input 178 | * @param pad boolean flag to see whether or not to pad 179 | * @return int 1 if function was successul, 0 if not 180 | */ 181 | static int convertBits( 182 | BytesArray &out, 183 | int outbits, 184 | const BytesArray &in, 185 | int inbits, 186 | int pad 187 | ); 188 | }; 189 | } 190 | } -------------------------------------------------------------------------------- /include/cryptography/nostr_bech32.hpp: -------------------------------------------------------------------------------- 1 | // adapted for aedile-ndk by Finrod Felagund (finrod.felagund.97@gmail.com) 2 | // a.k.a. npub1ecdlntvjzexlyfale2egzvvncc8tgqsaxkl5hw7xlgjv2cxs705s9qs735 3 | 4 | #pragma once 5 | 6 | #include "bech32.hpp" 7 | 8 | #define MAX_RELAYS 10 9 | 10 | #define MAX_TLVS 16 11 | 12 | #define TLV_SPECIAL 0 13 | #define TLV_RELAY 1 14 | #define TLV_AUTHOR 2 15 | #define TLV_KIND 3 16 | #define TLV_KNOWN_TLVS 4 17 | 18 | #define KEY_LENGTH 32 19 | 20 | #define CHECKSUM_LENGTH 6 21 | 22 | #define MAX_ENCODING_LENGTH 256 23 | 24 | namespace nostr 25 | { 26 | namespace encoding 27 | { 28 | 29 | typedef enum { 30 | NOSTR_BECH32_NOTE = 1, 31 | NOSTR_BECH32_NPUB = 2, 32 | NOSTR_BECH32_NPROFILE = 3, 33 | NOSTR_BECH32_NEVENT = 4, 34 | NOSTR_BECH32_NRELAY = 5, 35 | NOSTR_BECH32_NADDR = 6, 36 | NOSTR_BECH32_NSEC = 7, 37 | } NostrBech32Type; 38 | 39 | 40 | typedef struct Bech32Note 41 | { 42 | EventId event_id; 43 | } Bech32Note; 44 | 45 | typedef struct Bech32Npub 46 | { 47 | PubKey pubkey; 48 | } Bech32Npub; 49 | 50 | typedef struct Bech32Nsec 51 | { 52 | PrivKey privkey; 53 | } Bech32Nsec; 54 | 55 | 56 | typedef struct Bech32Nevent 57 | { 58 | Relays relays; 59 | EventId event_id; 60 | PubKey pubkey; 61 | uint32_t kind; 62 | bool has_kind; 63 | } Bech32Nevent; 64 | 65 | typedef struct Bech32Nprofile 66 | { 67 | Relays relays; 68 | PubKey pubkey; 69 | } Bech32Nprofile; 70 | 71 | typedef struct Bech32Naddr 72 | { 73 | Relays relays; 74 | std::string tag; 75 | PubKey pubkey; 76 | uint32_t kind; 77 | } Bech32Naddr; 78 | 79 | typedef struct 80 | { 81 | uint8_t type; 82 | uint8_t len; 83 | TlvValues value; 84 | } NostrTlv; 85 | 86 | typedef struct { 87 | Bech32Note note; 88 | Bech32Npub npub; 89 | Bech32Nsec nsec; 90 | Bech32Nprofile nprofile; 91 | Bech32Nevent nevent; 92 | Bech32Naddr naddr; 93 | } NostrBech32Data; 94 | 95 | typedef struct NostrBech32Encoding 96 | { 97 | NostrBech32Type type; 98 | NostrBech32Data data; 99 | } NostrBech32Encoding; 100 | 101 | 102 | class NostrBech32 103 | { 104 | public: 105 | /** 106 | * @brief Given an input data structure containing pubkeys or event ids, 107 | * or relays, etc, and a desired encoding type, this function will 108 | * write the bech32 encoding in the string `encoding` 109 | * 110 | * @param input Data structure containing the data to be encoded 111 | * and which type of encoding to use 112 | * @param encoding Output string containing the bech32 encoding of 113 | * the input data structure 114 | * @return true if encoding was successful 115 | */ 116 | bool encodeNostrBech32(NostrBech32Encoding &input, std::string &encoding); 117 | /** 118 | * @brief Given a bech32 nostr encoding, this function will 119 | * parse the encoding to extract the data and populate the `parsed` 120 | * data structure accordingly. 121 | * 122 | * @param encoding string containing the bech32 encoding to be parsed 123 | * @param parsed the data structure that will host the decoded data extracted 124 | * from the encoded bech32 string. 125 | * @return true if the parsing/decoding was successful. 126 | */ 127 | bool parseNostrBech32(std::string &encoding, NostrBech32Encoding &parsed); 128 | 129 | private: 130 | bool encodeNostrBech32Note(NostrBech32Encoding &input, std::string &encoding); 131 | bool encodeNostrBech32Npub(NostrBech32Encoding &input, std::string &encoding); 132 | bool encodeNostrBech32Nsec(NostrBech32Encoding &input, std::string &encoding); 133 | bool encodeNostrBech32Nprofile(NostrBech32Encoding &input, std::string &encoding); 134 | bool encodeNostrBech32Nevent(NostrBech32Encoding &input, std::string &encoding); 135 | bool encodeNostrBech32Naddr(NostrBech32Encoding &input, std::string &encoding); 136 | 137 | bool parseNostrBech32Note(BytesArray &encoding, NostrBech32Encoding &parsed); 138 | bool parseNostrBech32Npub(BytesArray &encoding, NostrBech32Encoding &parsed); 139 | bool parseNostrBech32Nsec(BytesArray &encoding, NostrBech32Encoding &parsed); 140 | bool parseNostrBech32Nprofile(BytesArray &encoding, NostrBech32Encoding &parsed); 141 | bool parseNostrBech32Nevent(BytesArray &encoding, NostrBech32Encoding &parsed); 142 | bool parseNostrBech32Naddr(BytesArray &encoding, NostrBech32Encoding &parsed); 143 | 144 | bool findTlv(std::vector &tlvs, uint8_t type, NostrTlv &found_tlv); 145 | bool tlvToRelays(std::vector &tlvs, Relays &relays); 146 | bool parseTlv(BytesArray &encoding, NostrTlv &tlv, int &cur); 147 | bool parseTlvs(BytesArray &encoding, std::vector &tlvs); 148 | }; 149 | 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /include/data/data.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | namespace nostr 13 | { 14 | namespace data 15 | { 16 | /** 17 | * @brief A Nostr event. 18 | * @remark All data transmitted over the Nostr protocol is encoded in JSON blobs. This struct 19 | * is common to every Nostr event kind. The significance of each event is determined by the 20 | * `tags` and `content` fields. 21 | */ 22 | struct Event 23 | { 24 | std::string id; ///< SHA-256 hash of the event data. 25 | std::string pubkey; ///< Public key of the event creator. 26 | std::time_t createdAt; ///< Unix timestamp of the event creation. 27 | int kind; ///< Event kind. 28 | std::vector> tags; ///< Arbitrary event metadata. 29 | std::string content; ///< Event content. 30 | std::string sig; ///< Event signature created with the private key of the event creator. 31 | 32 | /** 33 | * @brief Serializes the event to a JSON object. 34 | * @returns A stringified JSON object representing the event. 35 | * @throws `std::invalid_argument` if the event object is invalid. 36 | */ 37 | std::string serialize(); 38 | 39 | /** 40 | * @brief Deserializes the event from a JSON string. 41 | * @param jsonString A stringified JSON object representing the event. 42 | * @returns An event instance created from the JSON string. 43 | */ 44 | static Event fromString(std::string jsonString); 45 | 46 | /** 47 | * @brief Deserializes the event from a JSON object. 48 | * @param j A JSON object representing the event. 49 | * @returns An event instance created from the JSON object. 50 | */ 51 | static Event fromJson(nlohmann::json j); 52 | 53 | /** 54 | * @brief Compares two events for equality. 55 | * @remark Two events are considered equal if they have the same ID, since the ID is uniquely 56 | * generated from the event data. If the `id` field is empty for either event, the comparison 57 | * function will throw an exception. 58 | */ 59 | bool operator==(const Event& other) const; 60 | 61 | private: 62 | /** 63 | * @brief Validates the event. 64 | * @throws `std::invalid_argument` if the event object is invalid. 65 | * @remark The `createdAt` field defaults to the present if it is not already set. 66 | */ 67 | void validate(); 68 | 69 | /** 70 | * @brief Generates an ID for the event and assigns it to the event's `id` field. 71 | * @remark The ID is a 32-bytes lowercase hex-encoded sha256 of the serialized event data. 72 | */ 73 | void generateId(); 74 | }; 75 | 76 | /** 77 | * @brief A set of filters for querying Nostr relays. 78 | * @remark The `limit` field should always be included to keep the response size reasonable. The 79 | * `since` field is not required, and the `until` field will default to the present. At least one 80 | * of the other fields must be set for a valid filter. 81 | */ 82 | struct Filters 83 | { 84 | std::vector ids; ///< Event IDs. 85 | std::vector authors; ///< Event author npubs. 86 | std::vector kinds; ///< Kind numbers. 87 | std::unordered_map> tags; ///< Tag names mapped to lists of tag values. 88 | std::time_t since; ///< Unix timestamp. Matching events must be newer than this. 89 | std::time_t until; ///< Unix timestamp. Matching events must be older than this. 90 | int limit; ///< The maximum number of events the relay should return on the initial query. 91 | 92 | /** 93 | * @brief Serializes the filters to a JSON object. 94 | * @param subscriptionId A string up to 64 chars in length that is unique per relay connection. 95 | * @returns A stringified JSON object representing the filters. 96 | * @throws `std::invalid_argument` if the filter object is invalid. 97 | * @remarks The Nostr client is responsible for managing subscription IDs. Responses from the 98 | * relay will be organized by subscription ID. 99 | */ 100 | std::string serialize(std::string& subscriptionId); 101 | 102 | private: 103 | /** 104 | * @brief Validates the filters. 105 | * @throws `std::invalid_argument` if the filter object is invalid. 106 | * @remark The `until` field defaults to the present if it is not already set. 107 | */ 108 | void validate(); 109 | }; 110 | 111 | class NostrEvent 112 | { 113 | public: 114 | NostrEvent(); 115 | NostrEvent(std::string jsonString); 116 | NostrEvent(nlohmann::json j); 117 | NostrEvent(std::shared_ptr e); 118 | bool operator==(const NostrEvent& other) const; 119 | 120 | std::string toNote(); 121 | std::string toNevent(); 122 | std::string toNaddr(); 123 | 124 | void fromNote(std::string &encoding); 125 | void fromNevent(std::string &encoding); 126 | void fromNaddr(std::string &encoding); 127 | 128 | // class members 129 | std::shared_ptr data; 130 | std::vector relays; ///< relay in which the entity the event is more likely to be found 131 | }; 132 | 133 | } // namespace data 134 | } // namespace nostr 135 | 136 | namespace nlohmann 137 | { 138 | template <> 139 | struct adl_serializer 140 | { 141 | static void to_json(json& j, const nostr::data::Event& event); 142 | static void from_json(const json& j, nostr::data::Event& event); 143 | }; 144 | 145 | template<> 146 | struct adl_serializer 147 | { 148 | static void to_json(json& j, const nostr::data::Filters& filters); 149 | }; 150 | } // namespace nlohmann 151 | -------------------------------------------------------------------------------- /include/nostr.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "data/data.hpp" 4 | #include "client/web_socket_client.hpp" 5 | #include "service/nostr_service_base.hpp" 6 | #include "signer/signer.hpp" 7 | #include 8 | #include 9 | 10 | // namespace nostr 11 | // { 12 | // // TODO: Create custom exception types for the nostr namespace. 13 | // } // namespace nostr 14 | -------------------------------------------------------------------------------- /include/service/nostr_service.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "signer/signer.hpp" 4 | 5 | #include "nostr_service_base.hpp" 6 | 7 | #define NSERVICE_DECL template 8 | #define NSERVICE_DECL2 template 9 | #define NSERVICE_DEF NostrService 10 | #define NSERVICE_DEF2 NostrService 11 | 12 | namespace nostr 13 | { 14 | namespace service 15 | { 16 | /** 17 | * A template for a singleton class that implements Nostr features. 18 | */ 19 | NSERVICE_DECL 20 | class NostrService 21 | { 22 | public: 23 | NostrService(NostrService &other) = delete; 24 | 25 | ~NostrService(); 26 | 27 | void operator=(const NostrService &) = delete; 28 | 29 | /** 30 | * Returns a singleton instance of `NostrService`, initializing the instance if it has not 31 | * been initialized already. 32 | */ 33 | static std::shared_ptr instance(); 34 | 35 | protected: 36 | NostrService( 37 | std::shared_ptr appender, 38 | std::shared_ptr client, 39 | std::shared_ptr signer); 40 | 41 | private: 42 | std::shared_ptr _appender; 43 | std::shared_ptr _client; 44 | std::shared_ptr _base; 45 | std::shared_ptr _signer; 46 | 47 | static std::shared_ptr _instance; 48 | static std::mutex _mutex; 49 | }; 50 | 51 | /** 52 | * The singleton instance. 53 | */ 54 | NSERVICE_DECL 55 | std::shared_ptr NSERVICE_DEF::_instance = nullptr; 56 | 57 | /** 58 | * Enables thread-safe access to the singleton instance. 59 | */ 60 | NSERVICE_DECL 61 | std::mutex NSERVICE_DEF::_mutex; 62 | } // namespace service 63 | } // namespace nostr 64 | -------------------------------------------------------------------------------- /include/service/nostr_service_base.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | 14 | #include "data/data.hpp" 15 | #include "client/web_socket_client.hpp" 16 | 17 | namespace nostr 18 | { 19 | namespace service 20 | { 21 | class INostrServiceBase 22 | { 23 | public: 24 | virtual ~INostrServiceBase() = default; 25 | 26 | /** 27 | * @brief Opens connections to the default Nostr relays of the instance, as specified in 28 | * the constructor. 29 | * @return A list of the relay URLs to which connections were successfully opened. 30 | */ 31 | virtual std::vector openRelayConnections() = 0; 32 | 33 | /** 34 | * @brief Opens connections to the specified Nostr relays. 35 | * @returns A list of the relay URLs to which connections were successfully opened. 36 | */ 37 | virtual std::vector openRelayConnections(std::vector relays) = 0; 38 | 39 | /** 40 | * @brief Closes all open relay connections. 41 | */ 42 | virtual void closeRelayConnections() = 0; 43 | 44 | /** 45 | * @brief Closes any open connections to the specified Nostr relays. 46 | */ 47 | virtual void closeRelayConnections(std::vector relays) = 0; 48 | 49 | /** 50 | * @brief Publishes a Nostr event to all open relay connections. 51 | * @returns A tuple of `std::vector` objects, of the form ``, indicating 52 | * to which relays the event was published successfully, and to which relays the event failed 53 | * to publish. 54 | */ 55 | virtual std::tuple, std::vector> publishEvent( 56 | std::shared_ptr event 57 | ) = 0; 58 | 59 | /** 60 | * @brief Queries all open relay connections for events matching the given set of filters, and 61 | * returns all stored matching events returned by the relays. 62 | * @param filters The filters to use for the query. 63 | * @returns A std::future that will eventually hold a vector of all events matching the filters 64 | * from all open relay connections. 65 | * @remark This method runs until the relays send an EOSE message, indicating they have no more 66 | * stored events matching the given filters. When the EOSE message is received, the method 67 | * will close the subscription for each relay and return the received events. 68 | * @remark Use this method to fetch a batch of events from the relays. A `limit` value must be 69 | * set on the filters in the range 1-64, inclusive. If no valid limit is given, it will be 70 | * defaulted to 16. 71 | */ 72 | virtual std::future>> queryRelays( 73 | std::shared_ptr filters 74 | ) = 0; 75 | 76 | /** 77 | * @brief Queries all open relay connections for events matching the given set of filters. 78 | * @param filters The filters to use for the query. 79 | * @param eventHandler A callable object that will be invoked each time the client receives 80 | * an event matching the filters. 81 | * @param eoseHandler A callable object that will be invoked when the relay sends an EOSE 82 | * message. 83 | * @param closeHandler A callable object that will be invoked when the relay sends a CLOSE 84 | * message. 85 | * @returns The ID of the subscription created for the query. 86 | * @remark By providing a response handler, the caller assumes responsibility for handling all 87 | * events returned from the relay for the given filters. The service will not store the 88 | * events, and they will not be accessible via `getNewEvents`. 89 | */ 90 | virtual std::string queryRelays( 91 | std::shared_ptr filters, 92 | std::function)> eventHandler, 93 | std::function eoseHandler, 94 | std::function closeHandler 95 | ) = 0; 96 | 97 | /** 98 | * @brief Closes the subscription with the given ID on all open relay connections. 99 | * @returns A tuple of `std::vector` objects, of the form ``, indicating 100 | * to which relays the message was sent successfully, and which relays failed to receive the 101 | * message. 102 | */ 103 | virtual std::tuple, std::vector> closeSubscription( 104 | std::string subscriptionId 105 | ) = 0; 106 | 107 | /** 108 | * @brief Closes the subscription with the given ID on the given relay. 109 | * @returns True if the relay received the CLOSE message, false otherwise. 110 | * @remark If the subscription does not exist on the given relay, or if the relay is not 111 | * connected, the method will do nothing and return false. 112 | */ 113 | virtual bool closeSubscription(std::string subscriptionId, std::string relay) = 0; 114 | 115 | /** 116 | * @brief Closes all open subscriptions on all open relay connections. 117 | * @returns A list of any subscription IDs that failed to close. 118 | */ 119 | virtual std::vector closeSubscriptions() = 0; 120 | }; 121 | 122 | class NostrServiceBase : public INostrServiceBase 123 | { 124 | public: 125 | NostrServiceBase( 126 | std::shared_ptr appender, 127 | std::shared_ptr client 128 | ); 129 | 130 | NostrServiceBase( 131 | std::shared_ptr appender, 132 | std::shared_ptr client, 133 | std::vector relays 134 | ); 135 | 136 | ~NostrServiceBase() override; 137 | 138 | std::vector defaultRelays() const; 139 | 140 | std::vector activeRelays() const; 141 | 142 | std::unordered_map> subscriptions() const; 143 | 144 | std::vector openRelayConnections() override; 145 | 146 | std::vector openRelayConnections(std::vector relays) override; 147 | 148 | void closeRelayConnections() override; 149 | 150 | void closeRelayConnections(std::vector relays) override; 151 | 152 | // TODO: Make this method return a promise. 153 | std::tuple, std::vector> publishEvent( 154 | std::shared_ptr event) override; 155 | 156 | // TODO: Add a timeout to this method to prevent hanging while waiting for the relay. 157 | std::future>> queryRelays( 158 | std::shared_ptr filters) override; 159 | 160 | std::string queryRelays( 161 | std::shared_ptr filters, 162 | std::function)> eventHandler, 163 | std::function eoseHandler, 164 | std::function closeHandler 165 | ) override; 166 | 167 | std::tuple, std::vector> closeSubscription( 168 | std::string subscriptionId 169 | ) override; 170 | 171 | bool closeSubscription(std::string subscriptionId, std::string relay) override; 172 | 173 | std::vector closeSubscriptions() override; 174 | 175 | private: 176 | ///< The maximum number of events the service will store for each subscription. 177 | const int MAX_EVENTS_PER_SUBSCRIPTION = 128; 178 | 179 | ///< The WebSocket client used to communicate with relays. 180 | std::shared_ptr _client; 181 | 182 | ///< A mutex to protect the instance properties. 183 | std::mutex _propertyMutex; 184 | 185 | ///< The default set of Nostr relays to which the service will attempt to connect. 186 | std::vector _defaultRelays; 187 | 188 | ///< The set of Nostr relays to which the service is currently connected. 189 | std::vector _activeRelays; 190 | 191 | ///< A map from subscription IDs to the relays on which each subscription is open. 192 | std::unordered_map> _subscriptions; 193 | 194 | std::vector _getConnectedRelays(std::vector relays); 195 | 196 | std::vector _getUnconnectedRelays(std::vector relays); 197 | 198 | bool _isConnected(std::string relay); 199 | 200 | void _eraseActiveRelay(std::string relay); 201 | 202 | void _connect(std::string relay); 203 | 204 | void _disconnect(std::string relay); 205 | 206 | std::string _generateSubscriptionId(); 207 | 208 | std::string _generateCloseRequest(std::string subscriptionId); 209 | 210 | bool _hasSubscription(std::string subscriptionId); 211 | 212 | bool _hasSubscription(std::string subscriptionId, std::string relay); 213 | 214 | void _onSubscriptionMessage( 215 | std::string message, 216 | std::function)> eventHandler, 217 | std::function eoseHandler, 218 | std::function closeHandler 219 | ); 220 | 221 | void _onAcceptance(std::string message, std::function acceptanceHandler); 222 | }; 223 | } // namespace service 224 | } // namespace nostr 225 | -------------------------------------------------------------------------------- /include/service/nostr_service_specializations.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | #include "client/websocketpp_client.hpp" 7 | #include "signer/noscrypt_signer.hpp" 8 | 9 | #include "nostr_service.hpp" 10 | 11 | using CONSOLE_WEBSOCKETPP_NOSCRYPT = NostrService< 12 | plog::ConsoleAppender, 13 | client::WebsocketppClient, 14 | signer::NoscryptSigner 15 | >; 16 | 17 | namespace nostr 18 | { 19 | namespace service 20 | { 21 | /** 22 | * Specialization of the NostrService class template that appends logs to the console, handles 23 | * WebSocket connections using the Websocket++ library, and signs events using noscrypt. 24 | */ 25 | extern template class NostrService, client::WebsocketppClient, signer::NoscryptSigner>; 26 | 27 | template<> 28 | shared_ptr CONSOLE_WEBSOCKETPP_NOSCRYPT::instance() 29 | { 30 | lock_guard lock(CONSOLE_WEBSOCKETPP_NOSCRYPT::_mutex); 31 | if (CONSOLE_WEBSOCKETPP_NOSCRYPT::_instance == nullptr) 32 | { 33 | auto appender = make_shared>(); 34 | auto client = make_shared(); 35 | auto base = make_shared(appender, client); 36 | auto signer = make_shared(appender, base); 37 | 38 | CONSOLE_WEBSOCKETPP_NOSCRYPT::_instance = shared_ptr( 39 | new CONSOLE_WEBSOCKETPP_NOSCRYPT(appender, client, signer)); 40 | } 41 | 42 | // When there is a ready instance, return it. 43 | return CONSOLE_WEBSOCKETPP_NOSCRYPT::_instance; 44 | }; 45 | } // namespace service 46 | } // namespace nostr 47 | -------------------------------------------------------------------------------- /include/signer/noscrypt_signer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "service/nostr_service_base.hpp" 8 | #include "signer/signer.hpp" 9 | 10 | namespace nostr 11 | { 12 | namespace signer 13 | { 14 | class NoscryptSigner : public INostrConnectSigner 15 | { 16 | public: 17 | NoscryptSigner( 18 | std::shared_ptr appender, 19 | std::shared_ptr nostrService 20 | ); 21 | 22 | ~NoscryptSigner(); 23 | 24 | void receiveConnectionToken(std::string connectionToken) override; 25 | 26 | std::string generateConnectionToken( 27 | std::vector relays, 28 | std::string secret, 29 | std::optional name, 30 | std::optional url, 31 | std::optional description 32 | ) override; 33 | 34 | std::promise ping() override; 35 | 36 | std::shared_ptr> sign(std::shared_ptr event) override; 37 | 38 | private: 39 | static constexpr int _nostrConnectKind = 24133; // Kind 24133 is reserved for NIP-46 events. 40 | 41 | Encryption _nostrConnectEncryption; 42 | 43 | std::shared_ptr _noscryptContext; 44 | std::shared_ptr _nostrService; 45 | 46 | ///< Local nsec for communicating with the remote signer. 47 | std::shared_ptr _localPrivateKey; 48 | 49 | ///< Local npub for communicating with the remote signer. 50 | std::shared_ptr _localPublicKey; 51 | 52 | ///< The npub on whose behalf the remote signer is acting. 53 | std::shared_ptr _remotePublicKey; 54 | 55 | ///< An optional secret value provided by the remote signer. 56 | std::string _bunkerSecret; 57 | 58 | ///< A list of relays that will be used to connect to the remote signer. 59 | std::vector _relays; 60 | 61 | #pragma region Private Accessors 62 | 63 | inline std::string _getLocalPrivateKey() const; 64 | 65 | inline void _setLocalPrivateKey(const std::string value); 66 | 67 | inline std::string _getLocalPublicKey() const; 68 | 69 | inline void _setLocalPublicKey(const std::string value); 70 | 71 | inline std::string _getRemotePublicKey() const; 72 | 73 | inline void _setRemotePublicKey(const std::string value); 74 | 75 | #pragma endregion 76 | 77 | #pragma region Setup 78 | 79 | /** 80 | * @brief Parses the remote signer npub from a connection token provided by the signer. 81 | * @param connectionToken A connection token beginning with `bunker://`. 82 | * @returns The index of the first character of the connection token's query string, or -1 if 83 | * no valid public key could be parsed. 84 | * @remark This function updates the `_remotePublicKey` string in its class instance by side 85 | * effect. 86 | */ 87 | int _parseRemotePublicKey(std::string connectionToken); 88 | 89 | /** 90 | * @brief Parses a single query param from a connection token provided by a remote signer. 91 | * @param param A single query param from the connection token of the form `key=value`. 92 | * @remark This function updates the `_relays` vector and the `_bunkerSecret` string in its 93 | * class instance by side effect. 94 | */ 95 | void _handleConnectionTokenParam(std::string param); 96 | 97 | #pragma endregion 98 | 99 | #pragma region Signer Helpers 100 | 101 | /** 102 | * @brief Generates a unique ID for a signer request. 103 | * @returns A GUID string. 104 | */ 105 | inline std::string _generateSignerRequestId() const; 106 | 107 | /** 108 | * @brief Builds and signs a wrapper event for JRPC-like signer messages. 109 | * @param jrpc The JRPC-like payload that will comprise the event content, as specified by 110 | * NIP-46. 111 | * @returns A shared pointer to the signed wrapper event. 112 | */ 113 | std::shared_ptr _wrapSignerMessage(nlohmann::json jrpc); 114 | 115 | /** 116 | * @brief Unwraps the JRPC-like payload from a signer message, typically one received from the 117 | * remote signer in response to a request. 118 | * @param event An event containing a NIP-46 message payload. 119 | * @returns The unwrapped payload. The returned object will be empty if no valid payload could 120 | * be extracted from the given event. 121 | */ 122 | std::string _unwrapSignerMessage(std::shared_ptr event); 123 | 124 | /** 125 | * @brief Constructs a filter set that queries for messages sent from the signer to the client. 126 | * @returns A shared pointer to the constructed filter set. 127 | */ 128 | inline std::shared_ptr _buildSignerMessageFilters() const; 129 | 130 | #pragma endregion 131 | 132 | #pragma region Cryptography 133 | 134 | /** 135 | * @brief Encrypts a string according to the standard specified in NIP-04. 136 | * @param input The string to be encrypted. 137 | * @return The resulting encrypted string, or an empty string if the input could not be 138 | * encrypted. 139 | */ 140 | std::string _encryptNip04(std::string input); 141 | 142 | /** 143 | * @brief Decrypts a NIP-04 encrypted string. 144 | * @param input The string to be decrypted. 145 | * @return The decrypted string, or an empty string if the input could not be decrypted. 146 | */ 147 | std::string _decryptNip04(std::string input); 148 | 149 | /** 150 | * @brief Encrypts a string according to the standard specified in NIP-44. 151 | * @param input The string to be encrypted. 152 | * @return The resulting encrypted string, or an empty string if the input could not be 153 | * encrypted. 154 | */ 155 | std::string _encryptNip44(const std::string input); // TODO: Return or set HMAC? 156 | 157 | /** 158 | * @brief Decrypts a NIP-44 encrypted string. 159 | * @param input The string to be decrypted. 160 | * @return The decrypted string, or an empty string if the input could not be decrypted. 161 | */ 162 | std::string _decryptNip44(const std::string input); 163 | 164 | #pragma endregion 165 | }; 166 | } // namespace signer 167 | } // namespace nostr 168 | -------------------------------------------------------------------------------- /include/signer/signer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "data/data.hpp" 10 | 11 | namespace nostr 12 | { 13 | namespace signer 14 | { 15 | enum class Encryption 16 | { 17 | NIP04, 18 | NIP44 19 | }; 20 | 21 | /** 22 | * @brief An interface for Nostr event signing that implements NIP-46. 23 | */ 24 | class ISigner 25 | { 26 | public: 27 | virtual ~ISigner() = default; 28 | 29 | /** 30 | * @brief Signs the given Nostr event. 31 | * @param event The event to sign. 32 | * @returns A promise that will be fulfilled when the event has been signed. It will be 33 | * fulfilled with `true` if the signing succeeded, and `false` if it failed. 34 | * @remark The event's `sig` field will be updated in-place with the signature. 35 | */ 36 | virtual std::shared_ptr> sign( 37 | std::shared_ptr event 38 | ) = 0; 39 | }; 40 | 41 | class INostrConnectSigner : public ISigner 42 | { 43 | public: 44 | virtual ~INostrConnectSigner() = default; 45 | 46 | /** 47 | * @brief Establishes a connection to a remote signer using a connection token generated by the 48 | * signer. 49 | * @param connectionToken A connection token string beginning with `bunker://`, as defined by 50 | * NIP-46. 51 | * @remark A typical use case for this method would be for the user to paste a signer-generated 52 | * connection token into a client application, which would then call this method to establish a 53 | * connection to the remote signer. 54 | */ 55 | virtual void receiveConnectionToken(std::string connectionToken) = 0; 56 | 57 | /** 58 | * @brief Generates a connection token that a remote signer may use to establish a connection 59 | * to the client. 60 | * @param relays A list of one or more relays the client will use to communicate with the 61 | * remote signer. 62 | * @param secret An arbitrary value that the remote signer must return when connecting to 63 | * validate its identity to the client. 64 | * @param name The name of the client application. 65 | * @param url The canonical URL of the client application. 66 | * @param description A description of the client application. 67 | * @returns A connection token string beginning with `nostrconnect://`, as specified by NIP-46, 68 | * that may be provided to a remote signer to establish a connection to the client. Returns an 69 | * empty string if the connection token generation fails. 70 | */ 71 | virtual std::string generateConnectionToken( 72 | std::vector relays, 73 | std::string secret, 74 | std::optional name, 75 | std::optional url, 76 | std::optional description 77 | ) = 0; 78 | 79 | /** 80 | * @brief Pings the remote signer to check if it is still connected. 81 | * @returns A promise that will be fulfilled with `true` if the remote signer is connected, and 82 | * `false` otherwise. 83 | */ 84 | virtual std::promise ping() = 0; 85 | }; 86 | } // namespace signer 87 | } // namespace nostr 88 | -------------------------------------------------------------------------------- /maintainers.yaml: -------------------------------------------------------------------------------- 1 | maintainers: 2 | - npub1wqfzz2p880wq0tumuae9lfwyhs8uz35xd0kr34zrvrwyh3kvrzuskcqsyn 3 | - npub1ecdlntvjzexlyfale2egzvvncc8tgqsaxkl5hw7xlgjv2cxs705s9qs735 4 | relays: 5 | - wss://theforest.nostr1.com 6 | - wss://thecitadel.nostr1.com 7 | - wss://nostr.wine 8 | - wss://nostr.thesamecat.io 9 | -------------------------------------------------------------------------------- /src/client/websocketpp_client.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "client/websocketpp_client.hpp" 4 | 5 | using namespace nostr::client; 6 | using namespace std; 7 | 8 | void WebsocketppClient::start() 9 | { 10 | this->_client.init_asio(); 11 | this->_client.start_perpetual(); 12 | }; 13 | 14 | void WebsocketppClient::stop() 15 | { 16 | this->_client.stop_perpetual(); 17 | this->_client.stop(); 18 | }; 19 | 20 | void WebsocketppClient::openConnection(string uri) 21 | { 22 | error_code error; 23 | websocketpp_client::connection_ptr connection = this->_client.get_connection(uri, error); 24 | 25 | if (error.value() == -1) 26 | { 27 | // PLOG_ERROR << "Error connecting to relay " << relay << ": " << error.message(); 28 | } 29 | 30 | // Configure the connection here via the connection pointer. 31 | connection->set_fail_handler([this, uri](auto handle) { 32 | // PLOG_ERROR << "Error connecting to relay " << relay << ": Handshake failed."; 33 | lock_guard lock(this->_propertyMutex); 34 | if (this->isConnected(uri)) 35 | { 36 | this->_connectionHandles.erase(uri); 37 | } 38 | }); 39 | 40 | lock_guard lock(this->_propertyMutex); 41 | this->_connectionHandles[uri] = connection->get_handle(); 42 | this->_client.connect(connection); 43 | }; 44 | 45 | bool WebsocketppClient::isConnected(string uri) 46 | { 47 | lock_guard lock(this->_propertyMutex); 48 | return this->_connectionHandles.find(uri) != this->_connectionHandles.end(); 49 | }; 50 | 51 | tuple WebsocketppClient::send(string message, string uri) 52 | { 53 | error_code error; 54 | 55 | // Make sure the connection isn't closed from under us. 56 | lock_guard lock(this->_propertyMutex); 57 | this->_client.send( 58 | this->_connectionHandles[uri], 59 | message, 60 | websocketpp::frame::opcode::text, 61 | error); 62 | 63 | if (error.value() == -1) 64 | { 65 | return make_tuple(uri, false); 66 | } 67 | 68 | return make_tuple(uri, true); 69 | }; 70 | 71 | tuple WebsocketppClient::send( 72 | string message, 73 | string uri, 74 | function messageHandler 75 | ) 76 | { 77 | auto successes = this->send(message, uri); 78 | this->receive(uri, messageHandler); 79 | return successes; 80 | }; 81 | 82 | void WebsocketppClient::receive( 83 | string uri, 84 | function messageHandler 85 | ) 86 | { 87 | lock_guard lock(this->_propertyMutex); 88 | auto connectionHandle = this->_connectionHandles[uri]; 89 | auto connection = this->_client.get_con_from_hdl(connectionHandle); 90 | 91 | connection->set_message_handler( 92 | [messageHandler]( 93 | websocketpp::connection_hdl connectionHandle, 94 | websocketpp_client::message_ptr message 95 | ) 96 | { 97 | messageHandler(message->get_payload()); 98 | } 99 | ); 100 | }; 101 | 102 | void WebsocketppClient::closeConnection(string uri) 103 | { 104 | lock_guard lock(this->_propertyMutex); 105 | 106 | websocketpp::connection_hdl handle = this->_connectionHandles[uri]; 107 | this->_client.close( 108 | handle, 109 | websocketpp::close::status::going_away, 110 | "_client requested close." 111 | ); 112 | 113 | this->_connectionHandles.erase(uri); 114 | }; 115 | -------------------------------------------------------------------------------- /src/cryptography/bech32.cpp: -------------------------------------------------------------------------------- 1 | 2 | #include 3 | 4 | namespace nostr 5 | { 6 | namespace encoding 7 | { 8 | static uint32_t bech32PolymodStep(uint32_t value) { 9 | uint8_t b = value >> 25; 10 | uint32_t ret = ((value & 0x1FFFFFF) << 5) ^ 11 | (-((b >> 0) & 1) & 0x3b6a57b2UL) ^ 12 | (-((b >> 1) & 1) & 0x26508e6dUL) ^ 13 | (-((b >> 2) & 1) & 0x1ea119faUL) ^ 14 | (-((b >> 3) & 1) & 0x3d4233ddUL) ^ 15 | (-((b >> 4) & 1) & 0x2a1462b3UL); 16 | return ret; 17 | } 18 | 19 | static uint32_t bech32FinalConstant(Bech32EncodingType enc) { 20 | if (enc == BECH32_ENCODING_BECH32) return 1; 21 | if (enc == BECH32_ENCODING_BECH32M) return 0x2bc830a3; 22 | assert(0); 23 | } 24 | 25 | int Bech32::segwitAddrEncode( 26 | std::string &output, 27 | const std::string hrp, 28 | int ver, 29 | const BytesArray &prog, 30 | SegwitProtocol protocol) 31 | { 32 | BytesArray data; 33 | std::size_t datalen = 0; 34 | Bech32EncodingType enc = BECH32_ENCODING_BECH32; 35 | if (ver > 16) 36 | return 0; 37 | if (ver == 0 && prog.size() != 20 && prog.size() != 32) 38 | return 0; 39 | if (prog.size() < 2 || prog.size() > 40) 40 | return 0; 41 | if (ver > 0) 42 | enc = BECH32_ENCODING_BECH32M; 43 | 44 | if (protocol >= SEGWIT_MAX) { 45 | fprintf(stderr, "There is no supported for protocol of number %d\n", protocol); 46 | return 0; 47 | } 48 | 49 | if (protocol == SEGWIT_BITCOIN) { 50 | data.push_back(ver); 51 | BytesArray data_no_ver = BytesArray(data.begin()+1, data.end()); 52 | Bech32::convertBits(data_no_ver, TO_BITS, prog, FROM_BITS, 1); 53 | return Bech32::encode(output, hrp, data, MAX_INPUT_LENGTH, enc); 54 | } 55 | 56 | // for NOSTR, witver is not included in the data to be encoded 57 | if (protocol == SEGWIT_NOSTR) { 58 | Bech32::convertBits(data, TO_BITS, prog, FROM_BITS, 1); 59 | return Bech32::encode(output, hrp, data, MAX_INPUT_LENGTH, enc); 60 | } 61 | 62 | return 0; 63 | } 64 | 65 | int Bech32::segwitAddrDecode( 66 | std::shared_ptr ver, 67 | BytesArray &prog, 68 | const std::string hrp, 69 | const std::string addr, 70 | SegwitProtocol protocol 71 | ) 72 | { 73 | BytesArray data; 74 | std::string hrp_actual; 75 | Bech32EncodingType enc = Bech32::decode(hrp_actual, data, addr, 90); 76 | if (enc == BECH32_ENCODING_NONE) 77 | return 0; 78 | if (data.size() == 0 || data.size() > 65) 79 | return 0; 80 | if (hrp.compare(0, 84, hrp_actual) != 0) 81 | return 0; 82 | if (data[0] > 16) 83 | return 0; 84 | if (data[0] == 0 && enc != BECH32_ENCODING_BECH32) 85 | return 0; 86 | if (data[0] > 0 && enc != BECH32_ENCODING_BECH32M) 87 | return 0; 88 | if (protocol >= SEGWIT_MAX) 89 | return 0; 90 | 91 | 92 | if(!Bech32::convertBits(prog, FROM_BITS, data, TO_BITS, 0)) 93 | return 0; 94 | if (prog.size() < 2 || prog.size() > 40) 95 | return 0; 96 | if (data[0] == 0 && prog.size() != 20 && prog.size() != 32) 97 | return 0; 98 | 99 | // include the witness version in the decoded data 100 | // in case this is segwit for bitcoin because ver 101 | // could be something other than 1. 102 | if (protocol == SEGWIT_BITCOIN) { 103 | *ver = data[0]; 104 | } 105 | 106 | return 1; 107 | } 108 | 109 | int Bech32::encode( 110 | std::string &output, 111 | const std::string hrp, 112 | const BytesArray &data, 113 | std::size_t max_input_len, 114 | Bech32EncodingType enc 115 | ) 116 | { 117 | uint32_t chk = 1; 118 | std::size_t i = 0; 119 | for(const char &c : hrp) 120 | { 121 | if (c < 33 || c > 126) 122 | return 0; 123 | 124 | if (c >= 'A' && c <= 'Z') 125 | return 0; 126 | 127 | chk = bech32PolymodStep(chk) ^ (c >> 5); 128 | ++i; 129 | } 130 | if (i + 7 + data.size() > max_input_len) 131 | return 0; 132 | 133 | chk = bech32PolymodStep(chk); 134 | for(const char& c : hrp) 135 | chk = bech32PolymodStep(chk) ^ (c & 0x1f); 136 | 137 | output += hrp + "1"; 138 | 139 | for (const uint8_t &d : data) { 140 | // guard against numbers that exceet 5 bits 141 | if (d >> TO_BITS) 142 | return 0; 143 | 144 | chk = bech32PolymodStep(chk) ^ d; 145 | output += bech32Charset[d]; 146 | } 147 | for (i = 0; i < 6; i++) 148 | chk = bech32PolymodStep(chk); 149 | 150 | chk ^= bech32FinalConstant(enc); 151 | for (i = 0; i < 6; i++) { 152 | char current = bech32Charset[(chk >> (5 - i) * 5) & 0x1f]; 153 | output += bech32Charset[(chk >> (5 - i) * 5) & 0x1f]; 154 | } 155 | return 1; 156 | } 157 | 158 | Bech32EncodingType Bech32::decode( 159 | std::string &hrp, 160 | BytesArray &data, 161 | const std::string input, 162 | std::size_t max_input_len 163 | ) 164 | { 165 | if (input.size() > max_input_len) 166 | return BECH32_ENCODING_NONE; 167 | 168 | return Bech32::decodeLen(hrp, data, input, input.size()); 169 | } 170 | 171 | 172 | Bech32EncodingType Bech32::decodeLen( 173 | std::string &hrp, 174 | BytesArray &data, 175 | const std::string input, 176 | std::size_t max_input_len 177 | ) 178 | { 179 | uint32_t chk = 1; 180 | std::size_t hrp_len; 181 | int have_lower = 0, have_upper = 0; 182 | if (input.size() < 8) 183 | return BECH32_ENCODING_NONE; 184 | 185 | hrp_len = input.find('1'); 186 | if (hrp_len == std::string::npos) 187 | return BECH32_ENCODING_NONE; 188 | 189 | hrp = input.substr(0, hrp_len); 190 | 191 | for (char &c : hrp) 192 | { 193 | if (c < 33 || c > 126) 194 | return BECH32_ENCODING_NONE; 195 | if (c >= 'a' && c <= 'z') 196 | have_lower = 1; 197 | else if (c >= 'A' && c <= 'Z') 198 | { 199 | have_upper = 1; 200 | c = (c - 'A') + 'a'; 201 | } 202 | chk = bech32PolymodStep(chk) ^ (c >> 5); 203 | } 204 | chk = bech32PolymodStep(chk); 205 | for (char &c : hrp) 206 | chk = bech32PolymodStep(chk) ^ (c & 0x1f); 207 | 208 | size_t i = 0; 209 | for (uint8_t byte : input.substr(hrp.size() + 1)) 210 | { 211 | int v = (byte & 0x80) ? -1 : bech32CharsetRev[(int)byte]; 212 | 213 | if (byte >= 'a' && byte <= 'z') 214 | have_lower = 1; 215 | if (byte >= 'A' && byte <= 'Z') 216 | have_upper = 1; 217 | 218 | if (v == -1) { 219 | return BECH32_ENCODING_NONE; 220 | } 221 | chk = bech32PolymodStep(chk) ^ v; 222 | 223 | if(i + 6 < input.substr(hrp.size() + 1).size()) 224 | data.push_back(v); 225 | ++i; 226 | } 227 | if (have_lower && have_upper) 228 | { 229 | return BECH32_ENCODING_NONE; 230 | } 231 | if (chk == bech32FinalConstant(BECH32_ENCODING_BECH32)) 232 | return BECH32_ENCODING_BECH32; 233 | else if (chk == bech32FinalConstant(BECH32_ENCODING_BECH32M)) 234 | { 235 | return BECH32_ENCODING_BECH32M; 236 | } 237 | else 238 | { 239 | printf("HERE\n"); 240 | return BECH32_ENCODING_NONE; 241 | } 242 | } 243 | 244 | 245 | int Bech32::convertBits(BytesArray &out, int outbits, 246 | const BytesArray &in, int inbits, 247 | int pad) 248 | { 249 | uint32_t val = 0; 250 | int bits = 0; 251 | uint32_t maxv = (((uint32_t)1) << outbits) - 1; 252 | for (const uint8_t i : in) 253 | { 254 | val = (val << inbits) | i; 255 | bits += inbits; 256 | while (bits >= outbits) { 257 | bits -= outbits; 258 | out.push_back((val >> bits) & maxv); 259 | } 260 | } 261 | if (pad) 262 | if (bits) 263 | out.push_back((val << (outbits - bits)) & maxv); 264 | 265 | else if (((val << (outbits - bits)) & maxv) || bits >= inbits) 266 | return 0; 267 | 268 | return 1; 269 | } 270 | } 271 | } -------------------------------------------------------------------------------- /src/cryptography/noscrypt_cipher.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | 6 | #include "nostr_secure_rng.hpp" 7 | #include "noscrypt_cipher.hpp" 8 | #include "../internal/noscrypt_logger.hpp" 9 | 10 | using namespace std; 11 | using namespace nostr::cryptography; 12 | 13 | 14 | NoscryptCipher::NoscryptCipher(NoscryptCipherVersion version, NoscryptCipherMode mode) : 15 | _cipher(version, mode) 16 | { 17 | /* 18 | * We can know what iv size we need for the cipher now and allocate 19 | * a buffer just to save some allocations and code during the 20 | * encryption phase. This buffer is only needed during an encryption 21 | * operation. 22 | */ 23 | 24 | if (mode == NoscryptCipherMode::CIPHER_MODE_ENCRYPT) 25 | { 26 | //Resize the vector to the size of the current cipher 27 | this->_ivBuffer.resize(this->_cipher.ivSize()); 28 | 29 | //Safe to assign the iv to the context now and it will maintain a pointer to the buffer 30 | this->_cipher.setIV(this->_ivBuffer); 31 | } 32 | } 33 | 34 | std::string NoscryptCipher::update( 35 | const std::shared_ptr libContext, 36 | const std::shared_ptr localKey, 37 | const std::shared_ptr remoteKey, 38 | const std::string& input 39 | ) 40 | { 41 | NCResult result; 42 | 43 | //Argument exception if the input is empty 44 | if (input.empty()) 45 | { 46 | return string(); 47 | } 48 | 49 | //Safely convert the string to a vector of bytes (allocates and copies, so maybe speed up later) 50 | const vector inputBuffer(input.begin(), input.end()); 51 | 52 | result = this->_cipher.setInput(inputBuffer); 53 | if (result != NC_SUCCESS) 54 | { 55 | NC_LOG_ERROR(result); 56 | return string(); 57 | } 58 | 59 | /* 60 | * If were in encryption mode a random nonce (iv) must be generated. The size was determined 61 | * when the cipher was created and already assigned to the context, so we just need to assign 62 | * the random data. 63 | * 64 | * Keep in mind, this will automatically work for nip44 and nip04, either the 65 | * AES iv or the ChaCha nonce. 66 | */ 67 | if (this->_cipher.mode() == NoscryptCipherMode::CIPHER_MODE_ENCRYPT) 68 | { 69 | //A secure random initialization vector is needed for encryption operations 70 | NostrSecureRng::fill(this->_ivBuffer); 71 | } 72 | 73 | //Performs the operation (either encryption or decryption) 74 | result = this->_cipher.update(libContext, localKey, remoteKey); 75 | if (result != NC_SUCCESS) 76 | { 77 | NC_LOG_ERROR(result); 78 | return string(); 79 | } 80 | 81 | /* 82 | * Time to read the ciper output by getting the size of the output, then creating 83 | * a string to store it to 84 | */ 85 | 86 | NCResult outputSize = this->_cipher.outputSize(); 87 | if (outputSize <= 0) 88 | { 89 | NC_LOG_ERROR(outputSize); 90 | return string(); 91 | } 92 | 93 | //Alloc vector for reading input data (maybe only alloc once) 94 | vector output(outputSize); 95 | 96 | result = this->_cipher.readOutput(output); 97 | if (result != outputSize) 98 | { 99 | NC_LOG_ERROR(result); 100 | return string(); 101 | } 102 | 103 | return string(output.begin(), output.end()); 104 | } 105 | 106 | string NoscryptCipher::naiveEncodeBase64(const std::string& str) 107 | { 108 | // Compute base64 size and allocate a string buffer of that size. 109 | const size_t encodedSize = NoscryptCipher::base64EncodedSize(str.size()); 110 | 111 | auto encodedData = make_unique(encodedSize); 112 | 113 | // Encode the input string to base64. 114 | EVP_EncodeBlock( 115 | encodedData.get(), 116 | reinterpret_cast(str.c_str()), 117 | str.size() 118 | ); 119 | 120 | // Construct the encoded string from the buffer. 121 | return string( 122 | reinterpret_cast(encodedData.get()), 123 | encodedSize 124 | ); 125 | } 126 | 127 | string NoscryptCipher::naiveDecodeBase64(const string& str) 128 | { 129 | // Compute the size of the decoded string and allocate a buffer of that size. 130 | const size_t decodedSize = NoscryptCipher::base64DecodedSize(str.size()); 131 | 132 | auto decodedData = make_unique(decodedSize); 133 | 134 | // Decode the input string from base64. 135 | EVP_DecodeBlock( 136 | decodedData.get(), 137 | reinterpret_cast(str.c_str()), 138 | str.size() 139 | ); 140 | 141 | // Construct the decoded string from the buffer. 142 | return string( 143 | reinterpret_cast(decodedData.get()), 144 | decodedSize 145 | ); 146 | }; 147 | -------------------------------------------------------------------------------- /src/cryptography/noscrypt_cipher.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | #include 7 | #include "../internal/noscrypt_logger.hpp" 8 | 9 | namespace nostr 10 | { 11 | namespace cryptography 12 | { 13 | 14 | enum NoscryptCipherMode : uint32_t 15 | { 16 | CIPHER_MODE_ENCRYPT = NC_UTIL_CIPHER_MODE_ENCRYPT, 17 | CIPHER_MODE_DECRYPT = NC_UTIL_CIPHER_MODE_DECRYPT, 18 | }; 19 | 20 | enum NoscryptCipherVersion : uint32_t 21 | { 22 | NIP04 = NC_ENC_VERSION_NIP04, 23 | NIP44 = NC_ENC_VERSION_NIP44, 24 | }; 25 | 26 | class NoscryptCipherContext 27 | { 28 | private: 29 | NCUtilCipherContext* _cipher; 30 | 31 | public: 32 | 33 | NoscryptCipherContext(NoscryptCipherVersion version, NoscryptCipherMode mode) 34 | { 35 | /* 36 | * Create a new cipher context with the specified 37 | * version and mode that will live for the duration of the 38 | * instance. 39 | * 40 | * The user is expected to use the noscryptutil mode for 41 | * setting encryption/decryption modes. 42 | * 43 | * The cipher will zero out the memory when it is freed. 44 | * 45 | * For decryption, by default the mac is verified before 46 | * decryption occurs. 47 | * 48 | * NOTE: The ciper is set to reusable mode, so encrypt/decrypt 49 | * can be called multiple times although it's not recommended, 50 | * its just the more predictable way for users to handle it. 51 | */ 52 | 53 | _cipher = NCUtilCipherAlloc( 54 | (uint32_t)version, 55 | ((uint32_t)mode) | NC_UTIL_CIPHER_ZERO_ON_FREE | NC_UTIL_CIPHER_REUSEABLE 56 | ); 57 | 58 | //TODO, may fail to allocate memory. 59 | } 60 | 61 | ~NoscryptCipherContext() 62 | { 63 | //Free the cipher context (will also zero any data/pointers) 64 | NCUtilCipherFree(_cipher); 65 | } 66 | 67 | NCResult update( 68 | const std::shared_ptr libContext, 69 | const std::shared_ptr localKey, 70 | const std::shared_ptr remoteKey 71 | ) const 72 | { 73 | return NCUtilCipherUpdate(_cipher, libContext.get(), localKey.get(), remoteKey.get()); 74 | } 75 | 76 | NCResult setIV(std::vector& iv) const 77 | { 78 | return NCUtilCipherSetProperty(_cipher, NC_ENC_SET_IV, iv.data(), (uint32_t)iv.size()); 79 | } 80 | 81 | size_t ivSize() const 82 | { 83 | NCResult size = NCUtilCipherGetIvSize(_cipher); 84 | 85 | if (size <= 0) 86 | { 87 | //TODO Implement error handling 88 | return 0; 89 | } 90 | 91 | return size; 92 | } 93 | 94 | NCResult outputSize() const 95 | { 96 | return NCUtilCipherGetOutputSize(_cipher); 97 | } 98 | 99 | uint32_t flags() const 100 | { 101 | NCResult result = NCUtilCipherGetFlags(_cipher); 102 | 103 | if (result <= 0) 104 | { 105 | //TODO Implement error handling 106 | return 0; 107 | } 108 | 109 | return (uint32_t)result; 110 | } 111 | 112 | NoscryptCipherMode mode() const 113 | { 114 | //Mode bit is lsb so just mask off the rest of the flags and convert back to enum 115 | return (NoscryptCipherMode)(flags() & NC_UTIL_CIPHER_MODE); 116 | } 117 | 118 | NCResult readOutput(std::vector& output) const 119 | { 120 | return NCUtilCipherReadOutput(_cipher, output.data(), (uint32_t)output.size()); 121 | } 122 | 123 | NCResult setInput(const std::vector& input) const 124 | { 125 | /* 126 | * Assign and validate input string. Init can be only called multiple times 127 | * without side effects when the reusable flag is set. (currently set) 128 | */ 129 | 130 | return NCUtilCipherInit(_cipher, input.data(), input.size()); 131 | } 132 | }; 133 | 134 | class NoscryptCipher 135 | { 136 | 137 | private: 138 | const NoscryptCipherContext _cipher; 139 | /* 140 | * Stores the initialziation vector (aka nonce for nip44) for the cipher. 141 | * Noscrypt needs a memory buffer to store the iv, as it only holds pointers. 142 | * 143 | * This buffer must always point to valid memory after the cipher is created. 144 | */ 145 | std::vector _ivBuffer; 146 | 147 | public: 148 | NoscryptCipher(NoscryptCipherVersion version, NoscryptCipherMode mode); 149 | 150 | /* 151 | * @brief Performs the cipher operation on the input data. Depending on the mode 152 | * the cipher was initialized as, this will either encrypt or decrypt the data. 153 | * @param libContext The noscrypt library context. 154 | * @param localKey The local secret key used to encrypt/decrypt the data. 155 | * @param remoteKey The remote public key used to encrypt/decrypt the data. 156 | * @param input The data to encrypt/decrypt. 157 | * @returns The opposite of the input data. 158 | * @remark This cipher function follows the nostr nips format and will use do it's 159 | * best to 160 | */ 161 | std::string update( 162 | const std::shared_ptr libContext, 163 | const std::shared_ptr localKey, 164 | const std::shared_ptr remoteKey, 165 | const std::string& input 166 | ); 167 | 168 | /** 169 | * @brief Computes the length of a base64 encoded string. 170 | * @param n The length of the string to be encoded. 171 | * @return The length of the resulting base64 encoded string. 172 | */ 173 | inline static size_t base64EncodedSize(const size_t n) 174 | { 175 | return (((n + 2) / 3) << 2) + 1; 176 | }; 177 | 178 | /** 179 | * @brief Computes the length of a string decoded from base64. 180 | * @param n The length of the base64 encoded string. 181 | * @return The length of the decoded string. 182 | */ 183 | inline static size_t base64DecodedSize(const size_t n) 184 | { 185 | return (n * 3) >> 2; 186 | }; 187 | 188 | static std::string naiveEncodeBase64(const std::string& str); 189 | 190 | static std::string naiveDecodeBase64(const std::string& str); 191 | }; 192 | } // namespace cryptography 193 | } // namespace nostr 194 | -------------------------------------------------------------------------------- /src/cryptography/nostr_bech32.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | namespace nostr 4 | { 5 | namespace encoding 6 | { 7 | 8 | // if `std::string hex_string` has an even number of character 9 | // from the set of hexadecimal digits, then return true 10 | bool isValidHex(std::string hex_string) 11 | { 12 | if (hex_string.size() % 2 != 0) 13 | { 14 | std::cerr << "A valid hex string must have an even number of characters\n"; 15 | return false; 16 | } 17 | for (char &c : hex_string) 18 | if (!std::isxdigit(c)) 19 | { 20 | std::cerr << "String is invalid. It contains '"<< c << "' which is not a hex character\n"; 21 | return false; 22 | } 23 | return true; 24 | } 25 | 26 | bool convertHexStringToByteArray(std::string &hex, BytesArray &array) 27 | { 28 | if (!isValidHex(hex)) 29 | { 30 | std::cerr << "String: '" << hex << "' is not a valid hex string\n"; 31 | return false; 32 | } 33 | 34 | std::size_t arraySize = hex.size() / 2; 35 | for(int i=0; i> (8 * (3 - i)) & 0xFF)); 163 | } 164 | 165 | 166 | ret = Bech32::convertBits(squashed_input, TO_BITS, input_hex, FROM_BITS, 1); 167 | if (!ret) 168 | { 169 | std::cerr << "Error: 'Bech32::convertBits'\n"; 170 | return false; 171 | } 172 | 173 | ret = Bech32::encode(encoding, "nevent", squashed_input, MAX_INPUT_LENGTH, BECH32_ENCODING_BECH32); 174 | if (!ret) 175 | { 176 | std::cerr << "Error: 'Bech32::encode'\n"; 177 | return false; 178 | } 179 | return true; 180 | } 181 | 182 | bool NostrBech32::encodeNostrBech32Nprofile(NostrBech32Encoding &input, std::string &encoding) 183 | { 184 | BytesArray input_hex, squashed_input; 185 | 186 | // include TLV for the pubkey 187 | // include TLV for the 'd' tag 188 | if (input.data.nprofile.pubkey.empty()) 189 | { 190 | std::cerr << "pubkey is mandatory for naddr encoding\n"; 191 | return false; 192 | } 193 | input_hex.push_back(TLV_SPECIAL); 194 | input_hex.push_back(KEY_LENGTH); 195 | 196 | bool ret = convertHexStringToByteArray(input.data.nprofile.pubkey, input_hex); 197 | if (!ret) 198 | { 199 | std::cerr << "Error: 'convertHexStringToByteArray'\n"; 200 | return false; 201 | } 202 | 203 | // include TLV for the relays 204 | if (!input.data.nprofile.relays.empty()) 205 | { 206 | for (std::string relay : input.data.nprofile.relays) 207 | { 208 | input_hex.push_back(TLV_RELAY); 209 | input_hex.push_back(relay.length()); 210 | input_hex.insert(input_hex.end(), relay.begin(), relay.end()); 211 | } 212 | } 213 | ret = Bech32::convertBits(squashed_input, TO_BITS, input_hex, FROM_BITS, 1); 214 | if (!ret) 215 | { 216 | std::cerr << "Error: 'Bech32::convertBits'\n"; 217 | return false; 218 | } 219 | ret = Bech32::encode(encoding, "nprofile", squashed_input, MAX_INPUT_LENGTH, BECH32_ENCODING_BECH32); 220 | if (!ret) 221 | { 222 | std::cerr << "Error: 'Bech32::encode'\n"; 223 | return false; 224 | } 225 | return true; 226 | } 227 | 228 | bool NostrBech32::encodeNostrBech32Naddr(NostrBech32Encoding &input, std::string &encoding) 229 | { 230 | BytesArray input_hex, squashed_input; 231 | 232 | // include TLV for the 'd' tag 233 | if (input.data.naddr.tag.empty()) 234 | { 235 | std::cerr << "'d' tag is mandatory for naddr encoding\n"; 236 | return false; 237 | } 238 | 239 | input_hex.push_back(TLV_SPECIAL); 240 | input_hex.push_back(input.data.naddr.tag.size()); 241 | input_hex.insert(input_hex.end(), 242 | input.data.naddr.tag.begin(), input.data.naddr.tag.end()); 243 | 244 | // include TLV for the relays if they exist 245 | // optional for naddr encoding 246 | if (!input.data.naddr.relays.empty()) 247 | { 248 | for (std::string relay : input.data.naddr.relays) 249 | { 250 | input_hex.push_back(TLV_RELAY); 251 | input_hex.push_back(relay.length()); 252 | input_hex.insert(input_hex.end(), relay.begin(), relay.end()); 253 | } 254 | } 255 | 256 | if (input.data.naddr.pubkey.empty()) 257 | { 258 | std::cerr << "Pubkey metadata field is not option for naddr encoding\n"; 259 | return false; 260 | } 261 | 262 | /// include tlv for author field 263 | if (!isValidHex(input.data.naddr.pubkey)) 264 | { 265 | std::cerr << "Pubkey '" << input.data.naddr.pubkey << "' is not a valid hex key\n"; 266 | return false; 267 | } 268 | input_hex.push_back(TLV_AUTHOR); 269 | input_hex.push_back(KEY_LENGTH); 270 | 271 | bool ret = convertHexStringToByteArray(input.data.naddr.pubkey, input_hex); 272 | if (!ret) 273 | { 274 | std::cerr << "Error: 'convertHexStringToByteArray'\n"; 275 | return false; 276 | } 277 | 278 | // include TLV for kind 279 | input_hex.push_back(TLV_KIND); 280 | input_hex.push_back(4); 281 | 282 | // convert kind into 4 bytes in big-endian 283 | for(int i=0; i<4; i++) 284 | input_hex.push_back((input.data.naddr.kind >> (8 * (3 - i)) & 0xFF)); 285 | 286 | ret = Bech32::convertBits(squashed_input, TO_BITS, input_hex, FROM_BITS, 1); 287 | if (!ret) 288 | { 289 | std::cerr << "Error: 'Bech32::convertBits'\n"; 290 | return false; 291 | } 292 | 293 | ret = Bech32::encode(encoding, "naddr", squashed_input, MAX_INPUT_LENGTH, BECH32_ENCODING_BECH32); 294 | if (!ret) 295 | { 296 | std::cerr << "Error: 'Bech32::encode'\n"; 297 | return false; 298 | } 299 | return true; 300 | } 301 | 302 | bool NostrBech32::encodeNostrBech32(NostrBech32Encoding &input, std::string &encoding) 303 | { 304 | bool ret; 305 | switch (input.type) 306 | { 307 | case NOSTR_BECH32_NOTE: 308 | ret = encodeNostrBech32Note(input, encoding); 309 | if(!ret) 310 | throw std::runtime_error("Error: 'encodeNostrBech32Note'"); 311 | break; 312 | case NOSTR_BECH32_NPUB: 313 | ret = encodeNostrBech32Npub(input, encoding); 314 | if(!ret) 315 | throw std::runtime_error("Error: 'encodeNostrBech32Npub'"); 316 | break; 317 | case NOSTR_BECH32_NPROFILE: 318 | ret = encodeNostrBech32Nprofile(input, encoding); 319 | if(!ret) 320 | throw std::runtime_error("Error: 'encodeNostrBech32Nprofile'"); 321 | break; 322 | case NOSTR_BECH32_NEVENT: 323 | ret = encodeNostrBech32Nevent(input, encoding); 324 | if(!ret) 325 | throw std::runtime_error("Error: 'encodeNostrBech32Nevent'"); 326 | break; 327 | 328 | case NOSTR_BECH32_NADDR: 329 | ret = encodeNostrBech32Naddr(input, encoding); 330 | if(!ret) 331 | throw std::runtime_error("Error: 'encodeNostrBech32Naddr'"); 332 | break; 333 | case NOSTR_BECH32_NSEC: 334 | ret = encodeNostrBech32Nsec(input, encoding); 335 | if(!ret) 336 | throw std::runtime_error("Error: 'encodeNostrBech32Nsec'"); 337 | break; 338 | default: 339 | throw std::runtime_error("Error: unrecognized encoding"); 340 | break; 341 | } 342 | return true; 343 | } 344 | 345 | bool NostrBech32::parseTlv(BytesArray &encoding, NostrTlv &tlv, int &cur) 346 | { 347 | tlv.type = encoding[cur++]; 348 | 349 | // unknown, fail! 350 | if (tlv.type >= TLV_KNOWN_TLVS) 351 | return false; 352 | 353 | tlv.len = encoding[cur++]; 354 | 355 | // is the reported length greater then our buffer? if so fail 356 | if (tlv.len + cur > encoding.size()) 357 | return false; 358 | 359 | tlv.value = TlvValues(encoding.begin() + cur, encoding.begin() + cur + tlv.len); 360 | cur += tlv.len; 361 | 362 | return true; 363 | } 364 | 365 | bool NostrBech32::parseTlvs(BytesArray &encoding, std::vector &tlvs) 366 | { 367 | int cur = 0; 368 | NostrTlv tlv; 369 | 370 | while (tlvs.size() < MAX_TLVS && parseTlv(encoding, tlv, cur)) 371 | tlvs.push_back(tlv); 372 | 373 | if (tlvs.size() == 0) 374 | return false; 375 | 376 | return true; 377 | } 378 | 379 | bool NostrBech32::findTlv(std::vector &tlvs, uint8_t type, NostrTlv &found_tlv) 380 | { 381 | for (NostrTlv tlv : tlvs) 382 | { 383 | if (tlv.type == type) 384 | { 385 | found_tlv = tlv; 386 | return true; 387 | } 388 | } 389 | return false; 390 | } 391 | 392 | bool NostrBech32::tlvToRelays(std::vector &tlvs, Relays &relays) 393 | { 394 | for (NostrTlv tlv : tlvs) 395 | { 396 | if (tlv.type != TLV_RELAY) 397 | continue; 398 | 399 | relays.push_back(std::string(tlv.value.begin(), tlv.value.end())); 400 | if (relays.size() > MAX_RELAYS) 401 | break; 402 | } 403 | return true; 404 | } 405 | 406 | bool NostrBech32::parseNostrBech32Npub(BytesArray &encoding, NostrBech32Encoding &parsed) 407 | { 408 | bool ret = convertByteArrayToHexString(encoding, parsed.data.npub.pubkey); 409 | if (!ret) 410 | { 411 | std::cerr << "Error: 'convertByteArrayToHexString'\n"; 412 | return false; 413 | } 414 | return true; 415 | } 416 | 417 | bool NostrBech32::parseNostrBech32Note(BytesArray &encoding, NostrBech32Encoding &parsed) 418 | { 419 | bool ret = convertByteArrayToHexString(encoding, parsed.data.note.event_id); 420 | if (!ret) 421 | { 422 | std::cerr << "Error: 'convertByteArrayToHexString'\n"; 423 | return false; 424 | } 425 | return true; 426 | } 427 | 428 | 429 | bool NostrBech32::parseNostrBech32Nsec(BytesArray &encoding, NostrBech32Encoding &parsed) 430 | { 431 | bool ret = convertByteArrayToHexString(encoding, parsed.data.nsec.privkey); 432 | if (!ret) 433 | { 434 | std::cerr << "Error: 'convertByteArrayToHexString'\n"; 435 | return false; 436 | } 437 | return true; 438 | } 439 | 440 | 441 | bool NostrBech32::parseNostrBech32Nprofile(BytesArray &encoding, NostrBech32Encoding &parsed) 442 | { 443 | std::vector tlvs; 444 | NostrTlv tlv; 445 | 446 | bool ret = parseTlvs(encoding, tlvs); 447 | if(!ret) 448 | { 449 | std::cerr << "Error: 'parseTlvs'\n"; 450 | return false; 451 | } 452 | 453 | ret = findTlv(tlvs, TLV_SPECIAL, tlv); 454 | if(!ret) 455 | { 456 | std::cerr << "Error: 'findTlv:TLV_SPECIAL'\n"; 457 | return false; 458 | } 459 | if (tlv.len != KEY_LENGTH) 460 | { 461 | std::cerr << "Expected pubkey of length 32 bytes\n"; 462 | return false; 463 | } 464 | 465 | ret = convertByteArrayToHexString(tlv.value, parsed.data.nprofile.pubkey); 466 | if (!ret) 467 | { 468 | std::cerr << "Error: 'convertByteArrayToHexString'\n"; 469 | return false; 470 | } 471 | 472 | ret = tlvToRelays(tlvs, parsed.data.nprofile.relays); 473 | if (!ret) 474 | { 475 | std::cerr << "Error: 'tlvToRelays'\n"; 476 | return false; 477 | } 478 | return true; 479 | } 480 | 481 | bool NostrBech32::parseNostrBech32Nevent(BytesArray &encoding, NostrBech32Encoding &parsed) 482 | { 483 | std::vector tlvs; 484 | NostrTlv tlv; 485 | 486 | bool ret = parseTlvs(encoding, tlvs); 487 | if(!ret) 488 | { 489 | std::cerr << "Error: 'parseTlvs'\n"; 490 | return false; 491 | } 492 | 493 | // find event id 494 | ret = findTlv(tlvs, TLV_SPECIAL, tlv); 495 | if(!ret) 496 | { 497 | std::cerr << "Error: 'findTlv:TLV_SPECIAL'\n"; 498 | return false; 499 | } 500 | 501 | if (tlv.len != KEY_LENGTH) 502 | { 503 | std::cerr << "Expected event id of length 32 bytes\n"; 504 | return false; 505 | } 506 | ret = convertByteArrayToHexString(tlv.value, parsed.data.nevent.event_id); 507 | 508 | // find pubkey 509 | ret = findTlv(tlvs, TLV_AUTHOR, tlv); 510 | if(!ret) 511 | { 512 | parsed.data.nevent.pubkey = ""; 513 | } 514 | else 515 | { 516 | if (tlv.len != KEY_LENGTH) 517 | { 518 | std::cerr << "Expected pubkey of length 32 bytes\n"; 519 | return false; 520 | } 521 | ret = convertByteArrayToHexString(tlv.value, parsed.data.nevent.pubkey); 522 | if (!ret) 523 | { 524 | std::cerr << "Error: 'convertByteArrayToHexString'\n"; 525 | return false; 526 | } 527 | } 528 | ret = findTlv(tlvs, TLV_KIND, tlv); 529 | if (ret) 530 | { 531 | if (tlv.len != 4) 532 | { 533 | std::cerr << "Error: tlv for kind does not have 4 bytes\n"; 534 | return false; 535 | } 536 | parsed.data.nevent.has_kind = true; 537 | parsed.data.nevent.kind = 0; 538 | 539 | // convert 4 bytes in big-endian to kind in uint32 540 | for(int i=0; i<4; i++) 541 | { 542 | parsed.data.nevent.kind += (tlv.value[i] & 0xFF) << (8*(3 - i)); 543 | } 544 | } 545 | else 546 | parsed.data.nevent.has_kind = false; 547 | 548 | ret = tlvToRelays(tlvs, parsed.data.nevent.relays); 549 | if (!ret) 550 | { 551 | std::cerr << "Error: 'tlvToRelays'\n"; 552 | return false; 553 | } 554 | 555 | return true; 556 | } 557 | 558 | bool NostrBech32::parseNostrBech32Naddr(BytesArray &encoding, NostrBech32Encoding &parsed) 559 | { 560 | std::vector tlvs; 561 | NostrTlv tlv; 562 | 563 | bool ret = parseTlvs(encoding, tlvs); 564 | if(!ret) 565 | { 566 | std::cerr << "Error: 'parseTlvs'\n"; 567 | return false; 568 | } 569 | 570 | // find 'd' tag 571 | ret = findTlv(tlvs, TLV_SPECIAL, tlv); 572 | if(!ret) 573 | { 574 | std::cerr << "Error: 'findTlv:TLV_SPECIAL'\n"; 575 | return false; 576 | } 577 | parsed.data.naddr.tag = std::string(tlv.value.begin(), tlv.value.end()); 578 | 579 | // find pubkey 580 | ret = findTlv(tlvs, TLV_AUTHOR, tlv); 581 | if(!ret) 582 | { 583 | std::cerr << "Error: 'findTlv:TLV_AUTHOR'\n"; 584 | return false; 585 | } 586 | ret = convertByteArrayToHexString(tlv.value, parsed.data.naddr.pubkey); 587 | if (!ret) 588 | { 589 | std::cerr << "Error: 'convertByteArrayToHexString'\n"; 590 | return false; 591 | } 592 | 593 | // find kind 594 | ret = findTlv(tlvs, TLV_KIND, tlv); 595 | if(!ret) 596 | { 597 | std::cerr << "Error: 'findTlv:TLV_KIND'\n"; 598 | return false; 599 | } 600 | 601 | if (tlv.len != 4) 602 | { 603 | std::cerr << "Error: tlv for kind does not have 4 bytes\n"; 604 | return false; 605 | } 606 | 607 | // convert 4 bytes in big-endian to kind in uint32 608 | parsed.data.naddr.kind = 0; 609 | for(int i=0; i<4; i++) 610 | { 611 | parsed.data.naddr.kind += (tlv.value[i] & 0xFF) << (8*(3 - i)); 612 | } 613 | 614 | ret = tlvToRelays(tlvs, parsed.data.naddr.relays); 615 | if (!ret) 616 | { 617 | std::cerr << "Error: 'tlvToRelays'\n"; 618 | return false; 619 | } 620 | 621 | return true; 622 | 623 | } 624 | 625 | 626 | bool NostrBech32::parseNostrBech32(std::string &encoding, NostrBech32Encoding &parsed) 627 | { 628 | std::string hrp; 629 | BytesArray data, unsquashed_data; 630 | 631 | if (Bech32::decodeLen(hrp, data, encoding, MAX_INPUT_LENGTH) != BECH32_ENCODING_BECH32) 632 | { 633 | std::cerr << "Error: 'Bech32::decodeLen'\n"; 634 | return false; 635 | } 636 | 637 | bool ret = Bech32::convertBits(unsquashed_data, FROM_BITS, data, TO_BITS, 0); 638 | if (!ret) 639 | { 640 | std::cerr << "Error: 'Bech32::convertBits'\n"; 641 | return false; 642 | } 643 | 644 | if (hrp == "note") 645 | parsed.type = NOSTR_BECH32_NOTE; 646 | else if (hrp == "npub") 647 | parsed.type = NOSTR_BECH32_NPUB; 648 | else if (hrp == "nsec") 649 | parsed.type = NOSTR_BECH32_NSEC; 650 | else if (hrp == "nprofile") 651 | parsed.type = NOSTR_BECH32_NPROFILE; 652 | else if (hrp == "nevent") 653 | parsed.type = NOSTR_BECH32_NEVENT; 654 | else if (hrp == "naddr") 655 | parsed.type = NOSTR_BECH32_NADDR; 656 | else 657 | goto unrecognized_prefix; 658 | 659 | switch (parsed.type) 660 | { 661 | case NOSTR_BECH32_NOTE: 662 | ret = parseNostrBech32Note(unsquashed_data, parsed); 663 | if (!ret) 664 | throw std::runtime_error("Error: 'parseNostrBech32Note'"); 665 | break; 666 | case NOSTR_BECH32_NPUB: 667 | ret = parseNostrBech32Npub(unsquashed_data, parsed); 668 | if (!ret) 669 | throw std::runtime_error("Error: 'parseNostrBech32Npub'"); 670 | break; 671 | case NOSTR_BECH32_NSEC: 672 | ret = parseNostrBech32Nsec(unsquashed_data, parsed); 673 | if (!ret) 674 | throw std::runtime_error("Error: 'parseNostrBech32Nsec'"); 675 | break; 676 | 677 | case NOSTR_BECH32_NPROFILE: 678 | ret = parseNostrBech32Nprofile(unsquashed_data, parsed); 679 | if (!ret) 680 | throw std::runtime_error("Error: 'parseNostrBech32Nprofile'"); 681 | break; 682 | case NOSTR_BECH32_NEVENT: 683 | ret = parseNostrBech32Nevent(unsquashed_data, parsed); 684 | if (!ret) 685 | throw std::runtime_error("Error: 'parseNostrBech32Nevent'"); 686 | break; 687 | 688 | case NOSTR_BECH32_NADDR: 689 | ret = parseNostrBech32Naddr(unsquashed_data, parsed); 690 | if (!ret) 691 | throw std::runtime_error("Error: 'parseNostrBech32Naddr'"); 692 | break; 693 | 694 | default: 695 | goto unrecognized_prefix; 696 | break; 697 | } 698 | 699 | return true; 700 | 701 | unrecognized_prefix: 702 | throw std::invalid_argument("Unrecognized human readable prefix"); 703 | } 704 | 705 | } 706 | } // namespace name 707 | -------------------------------------------------------------------------------- /src/cryptography/nostr_secure_rng.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #include "nostr_secure_rng.hpp" 9 | 10 | using namespace std; 11 | using namespace nostr::cryptography; 12 | 13 | void NostrSecureRng::fill(void* buffer, size_t length) 14 | { 15 | if (RAND_bytes((uint8_t*)buffer, length) != 1) 16 | { 17 | //TODO throw runtime exception 18 | PLOG_ERROR << "Failed to generate random bytes"; 19 | } 20 | } 21 | 22 | void NostrSecureRng::reseed(uint32_t bufferSize) 23 | { 24 | int rc = RAND_load_file("/dev/random", bufferSize); 25 | 26 | if (rc != bufferSize) 27 | { 28 | PLOG_WARNING << "Failed to reseed the RNG with /dev/random, falling back to /dev/urandom."; 29 | RAND_poll(); 30 | } 31 | } 32 | 33 | void NostrSecureRng::zero(void* buffer, size_t length) 34 | { 35 | OPENSSL_cleanse(buffer, length); 36 | } 37 | 38 | inline void NostrSecureRng::zero(vector& buffer) 39 | { 40 | zero(buffer.data(), buffer.size()); 41 | } -------------------------------------------------------------------------------- /src/cryptography/nostr_secure_rng.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace nostr 6 | { 7 | namespace cryptography 8 | { 9 | 10 | class NostrSecureRng 11 | { 12 | private: 13 | 14 | public: 15 | 16 | /** 17 | * @brief Fills the given buffer with secure random bytes. 18 | * @param buffer The buffer to fill with random bytes. 19 | * @param length The number of bytes to fill. 20 | */ 21 | static void fill(void* buffer, size_t length); 22 | 23 | /* 24 | * @brief Fills the given vector with secure random bytes. 25 | * @param buffer The vector to fill with random bytes. 26 | */ 27 | static inline void fill(std::vector& buffer) 28 | { 29 | fill(buffer.data(), buffer.size()); 30 | } 31 | 32 | /* 33 | * @brief Reseeds the RNG with random bytes from /dev/random. 34 | * @param bufferSize The number of bytes to read from /dev/random. 35 | * @remark Falls back to /dev/urandom if /dev/random is not available. 36 | */ 37 | static void reseed(uint32_t bufferSize = 32); 38 | 39 | /* 40 | * @brief Securley zeroes out the given buffer. 41 | * @param buffer A pointer to the buffer to zero out. 42 | * @param length The number of bytes to zero out. 43 | */ 44 | static void zero(void* buffer, size_t length); 45 | 46 | /* 47 | * @brief Securley zeroes out the given vector. 48 | * @param buffer The vector to zero out. 49 | */ 50 | static inline void zero(std::vector& buffer); 51 | }; 52 | 53 | } // namespace cryptography 54 | } // namespace nostr 55 | -------------------------------------------------------------------------------- /src/data/event.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | #include "data/data.hpp" 5 | #include "cryptography/nostr_bech32.hpp" 6 | 7 | using namespace nlohmann; 8 | using namespace nostr::data; 9 | using namespace nostr::encoding; 10 | using namespace std; 11 | 12 | string Event::serialize() 13 | { 14 | try 15 | { 16 | this->validate(); 17 | } 18 | catch (const invalid_argument& e) 19 | { 20 | throw e; 21 | } 22 | 23 | // Generate the event ID from the serialized data. 24 | this->generateId(); 25 | 26 | json j = *this; 27 | return j.dump(); 28 | }; 29 | 30 | Event Event::fromString(string jstr) 31 | { 32 | json j = json::parse(jstr); 33 | Event event; 34 | 35 | try 36 | { 37 | event = Event::fromJson(j); 38 | } 39 | catch (const invalid_argument& e) 40 | { 41 | throw e; 42 | } 43 | 44 | return event; 45 | }; 46 | 47 | Event Event::fromJson(json j) 48 | { 49 | Event event = j.get(); 50 | return event; 51 | }; 52 | 53 | void Event::validate() 54 | { 55 | bool hasPubkey = this->pubkey.length() > 0; 56 | if (!hasPubkey) 57 | { 58 | throw invalid_argument("Event::validate: The pubkey of the event author is required."); 59 | } 60 | 61 | bool hasCreatedAt = this->createdAt > 0; 62 | if (!hasCreatedAt) 63 | { 64 | this->createdAt = time(nullptr); 65 | } 66 | 67 | bool hasKind = this->kind >= 0 && this->kind < 40000; 68 | if (!hasKind) 69 | { 70 | throw std::invalid_argument("Event::validate: A valid event kind is required."); 71 | } 72 | }; 73 | 74 | void Event::generateId() 75 | { 76 | // Create a JSON array of values used to generate the event ID. 77 | json arr = { 0, this->pubkey, this->createdAt, this->kind, this->tags, this->content }; 78 | string serializedData = arr.dump(); 79 | 80 | unsigned char hash[SHA256_DIGEST_LENGTH]; 81 | EVP_Digest(serializedData.c_str(), serializedData.length(), hash, NULL, EVP_sha256(), NULL); 82 | 83 | stringstream ss; 84 | for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) 85 | { 86 | ss << hex << setw(2) << setfill('0') << (int)hash[i]; 87 | } 88 | 89 | this->id = ss.str(); 90 | }; 91 | 92 | bool Event::operator==(const Event& other) const 93 | { 94 | if (this->id.empty()) 95 | { 96 | throw invalid_argument("Event::operator==: Cannot check equality, the left-side argument is undefined."); 97 | } 98 | if (other.id.empty()) 99 | { 100 | throw invalid_argument("Event::operator==: Cannot check equality, the right-side argument is undefined."); 101 | } 102 | 103 | return this->id == other.id; 104 | }; 105 | 106 | void adl_serializer::to_json(json& j, const Event& event) 107 | { 108 | // Serialize the event to a JSON object. 109 | j = { 110 | { "id", event.id }, 111 | { "pubkey", event.pubkey }, 112 | { "created_at", event.createdAt }, 113 | { "kind", event.kind }, 114 | { "tags", event.tags }, 115 | { "content", event.content }, 116 | { "sig", event.sig }, 117 | }; 118 | } 119 | 120 | void adl_serializer::from_json(const json& j, Event& event) 121 | { 122 | // TODO: Set up custom exception types for improved exception handling. 123 | try 124 | { 125 | event.id = j.at("id"); 126 | event.pubkey = j.at("pubkey"); 127 | event.createdAt = j.at("created_at"); 128 | event.kind = j.at("kind"); 129 | event.tags = j.at("tags"); 130 | event.content = j.at("content"); 131 | event.sig = j.at("sig"); 132 | 133 | // TODO: Validate the event against its signature. 134 | } 135 | catch (const json::type_error& te) 136 | { 137 | throw te; 138 | } 139 | catch (const json::out_of_range& oor) 140 | { 141 | throw oor; 142 | } 143 | } 144 | 145 | // wrapper class implementation 146 | 147 | NostrEvent::NostrEvent() 148 | { 149 | this->data = std::make_shared(); 150 | } 151 | 152 | NostrEvent::NostrEvent(string jsonString) 153 | { 154 | this->data->fromString(jsonString); 155 | } 156 | NostrEvent::NostrEvent(json j) 157 | { 158 | this->data->fromJson(j); 159 | } 160 | NostrEvent::NostrEvent(shared_ptr e) 161 | { 162 | this->data = e; 163 | } 164 | 165 | bool NostrEvent::operator==(const NostrEvent& other) const 166 | { 167 | return this->data == other.data; 168 | } 169 | 170 | std::string NostrEvent::toNote() 171 | { 172 | NostrBech32 encoder = NostrBech32(); 173 | 174 | NostrBech32Encoding input; 175 | string output; 176 | input.type = NOSTR_BECH32_NOTE; 177 | input.data.note.event_id = this->data->id; 178 | 179 | if(!encoder.encodeNostrBech32(input, output)) 180 | std::cerr << "'encoder.encodeNostrBech32' failed\n"; 181 | 182 | return output; 183 | } 184 | 185 | std::string NostrEvent::toNevent() 186 | { 187 | NostrBech32 encoder = NostrBech32(); 188 | 189 | NostrBech32Encoding input; 190 | string output; 191 | input.type = NOSTR_BECH32_NEVENT; 192 | input.data.nevent.event_id = this->data->id; 193 | 194 | input.data.nevent.pubkey = this->data->pubkey; 195 | input.data.nevent.relays = this->relays; 196 | 197 | if (this->data->kind == 1) 198 | { 199 | input.data.nevent.has_kind = false; 200 | } 201 | else 202 | { 203 | input.data.nevent.has_kind = true; 204 | input.data.nevent.kind = this->data->kind; 205 | } 206 | 207 | if(!encoder.encodeNostrBech32(input, output)) 208 | std::cerr << "'encoder.encodeNostrBech32' failed\n"; 209 | 210 | return output; 211 | } 212 | 213 | std::string NostrEvent::toNaddr() 214 | { 215 | NostrBech32 encoder = NostrBech32(); 216 | 217 | NostrBech32Encoding input; 218 | string output; 219 | input.type = NOSTR_BECH32_NADDR; 220 | 221 | // fetch 'd' tag from tags in the base event 222 | input.data.naddr.tag = ""; 223 | for (auto tag : this->data->tags) 224 | { 225 | if (tag[0] == "d") 226 | { 227 | input.data.naddr.tag = tag[1]; 228 | break; 229 | } 230 | } 231 | if (input.data.naddr.tag.empty()) 232 | { 233 | std::cerr << "Could not find mandatory 'd' tag. Returning nullptr\n"; 234 | return nullptr; 235 | } 236 | 237 | input.data.naddr.pubkey = this->data->pubkey; 238 | if (input.data.naddr.pubkey.empty()) 239 | { 240 | std::cerr << "Could not find mandatory pubkey. Returning nullptr\n"; 241 | return nullptr; 242 | } 243 | 244 | input.data.naddr.relays = this->relays; 245 | input.data.naddr.kind = this->data->kind; 246 | 247 | if(!encoder.encodeNostrBech32(input, output)) 248 | std::cerr << "'encoder.encodeNostrBech32' failed\n"; 249 | 250 | return output; 251 | } 252 | 253 | 254 | void NostrEvent::fromNote(std::string &encoding) 255 | { 256 | NostrBech32 parser = NostrBech32(); 257 | NostrBech32Encoding parsedData; 258 | 259 | if(!parser.parseNostrBech32(encoding, parsedData)) 260 | std::cerr << "failed to decode Note encoding\n"; 261 | 262 | this->data->id = parsedData.data.note.event_id; 263 | } 264 | 265 | void NostrEvent::fromNevent(std::string &encoding) 266 | { 267 | NostrBech32 parser = NostrBech32(); 268 | NostrBech32Encoding parsedData; 269 | 270 | if(!parser.parseNostrBech32(encoding, parsedData)) 271 | std::cerr << "failed to decode nevent encoding\n"; 272 | 273 | this->data->id = parsedData.data.nevent.event_id; 274 | this->data->pubkey = parsedData.data.nevent.pubkey; 275 | this->relays = parsedData.data.nevent.relays; 276 | 277 | if(parsedData.data.nevent.has_kind) 278 | this->data->kind = parsedData.data.nevent.kind; 279 | } 280 | 281 | void NostrEvent::fromNaddr(std::string &encoding) 282 | { 283 | NostrBech32 parser = NostrBech32(); 284 | NostrBech32Encoding parsedData; 285 | 286 | if(!parser.parseNostrBech32(encoding, parsedData)) 287 | std::cerr << "failed to decode nevent encoding\n"; 288 | 289 | this->data->tags.push_back({"d", parsedData.data.naddr.tag}); 290 | this->data->pubkey = parsedData.data.naddr.pubkey; 291 | this->relays = parsedData.data.naddr.relays; 292 | this->data->kind = parsedData.data.naddr.kind; 293 | } 294 | -------------------------------------------------------------------------------- /src/data/filters.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include "data/data.hpp" 4 | 5 | using namespace nlohmann; 6 | using namespace nostr::data; 7 | using namespace std; 8 | 9 | string Filters::serialize(string& subscriptionId) 10 | { 11 | try 12 | { 13 | this->validate(); 14 | } 15 | catch (const invalid_argument& e) 16 | { 17 | throw e; 18 | } 19 | 20 | json j = *this; 21 | json jarr = json::array({ "REQ", subscriptionId, j }); 22 | 23 | return jarr.dump(); 24 | }; 25 | 26 | void Filters::validate() 27 | { 28 | bool hasLimit = this->limit > 0; 29 | if (!hasLimit) 30 | { 31 | throw invalid_argument("Filters::validate: The limit must be greater than 0."); 32 | } 33 | 34 | bool hasUntil = this->until > 0; 35 | if (!hasUntil) 36 | { 37 | this->until = time(nullptr); 38 | } 39 | 40 | bool hasIds = this->ids.size() > 0; 41 | bool hasAuthors = this->authors.size() > 0; 42 | bool hasKinds = this->kinds.size() > 0; 43 | bool hasTags = this->tags.size() > 0; 44 | 45 | bool hasFilter = hasIds || hasAuthors || hasKinds || hasTags; 46 | 47 | if (!hasFilter) 48 | { 49 | throw invalid_argument("Filters::validate: At least one filter must be set."); 50 | } 51 | }; 52 | 53 | void adl_serializer::to_json(json& j, const Filters& filters) 54 | { 55 | j = { 56 | { "ids", filters.ids }, 57 | { "authors", filters.authors }, 58 | { "kinds", filters.kinds }, 59 | { "since", filters.since }, 60 | { "until", filters.until }, 61 | { "limit", filters.limit } 62 | }; 63 | 64 | for (auto& tag : filters.tags) 65 | { 66 | string name = tag.first[0] == '#' 67 | ? tag.first 68 | : '#' + tag.first; 69 | 70 | json values = json::array(); 71 | for (auto& value : tag.second) 72 | { 73 | values.push_back(value); 74 | } 75 | 76 | j[name] = values; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/data/test_scenarios.md: -------------------------------------------------------------------------------- 1 | # Test Scenarios for `nostr::data` 2 | 3 | ## NIP-01: Nostr Event Format 4 | 5 | ### 1. Idempotent ID Generation 6 | 7 | GIVEN 2 events with the same values for `pubkey`, `created_at`, `kind`, `tags`, and `content` 8 | WHEN the `pubkey`, `created_at`, `kind`, `tags`, and `content` are arranged in a JSON array of the form `[0,pubkey,created_at,kind,tags,content]` to generate the events' IDs 9 | THEN the IDs generated for both events are identical 10 | 11 | ### 2. Escaping Special Characters in Content 12 | 13 | GIVEN the content field of an event contains any of the characters `\b`, `\t`, `\n`, `\f`, `\r`, `"`, or `\` 14 | WHEN the content is serialized for ID generation 15 | THEN the special characters are included verbatim in the resulting string, with proper escape sequences. 16 | -------------------------------------------------------------------------------- /src/internal/noscrypt_logger.cpp: -------------------------------------------------------------------------------- 1 | #include "noscrypt_logger.hpp" 2 | 3 | void _printNoscryptError(NCResult result, const char *func, int line) 4 | { 5 | uint8_t argPosition; 6 | 7 | switch (NCParseErrorCode(result, &argPosition)) 8 | { 9 | case E_NULL_PTR: 10 | PLOG_ERROR << "noscrypt - error: A null pointer was passed in " << func << "(" << argPosition << ") at line " << line; 11 | break; 12 | 13 | case E_INVALID_ARG: 14 | PLOG_ERROR << "noscrypt - error: An invalid argument was passed in " << func << "(" << argPosition << ") at line " << line; 15 | break; 16 | 17 | case E_INVALID_CONTEXT: 18 | PLOG_ERROR << "noscrypt - error: An invalid context was passed in " << func << "(" << argPosition << ") on line " << line; 19 | break; 20 | 21 | case E_ARGUMENT_OUT_OF_RANGE: 22 | PLOG_ERROR << "noscrypt - error: An argument was out of range in " << func << "(" << argPosition << ") at line " << line; 23 | break; 24 | 25 | case E_OPERATION_FAILED: 26 | PLOG_ERROR << "noscrypt - error: An operation failed in " << func << "(" << argPosition << ") at line " << line; 27 | break; 28 | 29 | default: 30 | PLOG_ERROR << "noscrypt - error: An unknown error " << result << " occurred in " << func << "(" << argPosition << ") at line " << line; 31 | break; 32 | } 33 | } -------------------------------------------------------------------------------- /src/internal/noscrypt_logger.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | /* 7 | * @brief Logs an error message with the function name and line number where the 8 | * error occurred. This is useful for debugging and logging errors in the Noscrypt 9 | * library. 10 | */ 11 | #define NC_LOG_ERROR(result) _printNoscryptError(result, __func__, __LINE__) 12 | 13 | void _printNoscryptError(NCResult result, const char *func, int line); 14 | -------------------------------------------------------------------------------- /src/service/nostr_service_base.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "service/nostr_service_base.hpp" 10 | 11 | using namespace nlohmann; 12 | using namespace nostr::service; 13 | using namespace std; 14 | 15 | NostrServiceBase::NostrServiceBase( 16 | shared_ptr appender, 17 | shared_ptr client 18 | ) : NostrServiceBase(appender, client, {}) { }; 19 | 20 | NostrServiceBase::NostrServiceBase( 21 | shared_ptr appender, 22 | shared_ptr client, 23 | vector relays 24 | ) : _defaultRelays(relays), _client(client) 25 | { 26 | plog::init(plog::debug, appender.get()); 27 | client->start(); 28 | }; 29 | 30 | NostrServiceBase::~NostrServiceBase() 31 | { 32 | this->_client->stop(); 33 | }; 34 | 35 | vector NostrServiceBase::defaultRelays() const 36 | { return this->_defaultRelays; }; 37 | 38 | vector NostrServiceBase::activeRelays() const 39 | { return this->_activeRelays; }; 40 | 41 | unordered_map> NostrServiceBase::subscriptions() const 42 | { return this->_subscriptions; }; 43 | 44 | vector NostrServiceBase::openRelayConnections() 45 | { 46 | return this->openRelayConnections(this->_defaultRelays); 47 | }; 48 | 49 | vector NostrServiceBase::openRelayConnections(vector relays) 50 | { 51 | PLOG_INFO << "Attempting to connect to Nostr relays."; 52 | vector unconnectedRelays = this->_getUnconnectedRelays(relays); 53 | 54 | vector connectionThreads; 55 | for (string relay : unconnectedRelays) 56 | { 57 | thread connectionThread([this, relay]() { 58 | this->_connect(relay); 59 | }); 60 | connectionThreads.push_back(move(connectionThread)); 61 | } 62 | 63 | for (thread& connectionThread : connectionThreads) 64 | { 65 | connectionThread.join(); 66 | } 67 | 68 | std::size_t targetCount = relays.size(); 69 | std::size_t activeCount = this->_activeRelays.size(); 70 | PLOG_INFO << "Connected to " << activeCount << "/" << targetCount << " target relays."; 71 | 72 | // This property should only contain successful relays at this point. 73 | return this->_activeRelays; 74 | }; 75 | 76 | void NostrServiceBase::closeRelayConnections() 77 | { 78 | if (this->_activeRelays.size() == 0) 79 | { 80 | PLOG_INFO << "No active relay connections to close."; 81 | return; 82 | } 83 | 84 | this->closeRelayConnections(this->_activeRelays); 85 | }; 86 | 87 | void NostrServiceBase::closeRelayConnections(vector relays) 88 | { 89 | PLOG_INFO << "Disconnecting from Nostr relays."; 90 | vector connectedRelays = this->_getConnectedRelays(relays); 91 | 92 | vector disconnectionThreads; 93 | for (string relay : connectedRelays) 94 | { 95 | thread disconnectionThread([this, relay]() { 96 | this->_disconnect(relay); 97 | }); 98 | disconnectionThreads.push_back(move(disconnectionThread)); 99 | 100 | // TODO: Close subscriptions before disconnecting. 101 | lock_guard lock(this->_propertyMutex); 102 | this->_subscriptions.erase(relay); 103 | } 104 | 105 | for (thread& disconnectionThread : disconnectionThreads) 106 | { 107 | disconnectionThread.join(); 108 | } 109 | }; 110 | 111 | // TODO: Make this method return a promise. 112 | tuple, vector> NostrServiceBase::publishEvent( 113 | shared_ptr event 114 | ) 115 | { 116 | vector successfulRelays; 117 | vector failedRelays; 118 | 119 | PLOG_INFO << "Attempting to publish event to Nostr relays."; 120 | 121 | json message; 122 | try 123 | { 124 | message = json::array({ "EVENT", event->serialize() }); 125 | } 126 | catch (const std::invalid_argument& e) 127 | { 128 | PLOG_ERROR << "Failed to sign event: " << e.what(); 129 | throw e; 130 | } 131 | catch (const json::exception& je) 132 | { 133 | PLOG_ERROR << "Failed to serialize event: " << je.what(); 134 | throw je; 135 | } 136 | 137 | lock_guard lock(this->_propertyMutex); 138 | vector targetRelays = this->_activeRelays; 139 | vector>> publishFutures; 140 | for (const string& relay : targetRelays) 141 | { 142 | promise> publishPromise; 143 | publishFutures.push_back(move(publishPromise.get_future())); 144 | 145 | auto [uri, success] = this->_client->send( 146 | message.dump(), 147 | relay, 148 | [this, &relay, &event, &publishPromise](string response) 149 | { 150 | this->_onAcceptance( 151 | response, 152 | [this, &relay, &event, &publishPromise](bool isAccepted) 153 | { 154 | if (isAccepted) 155 | { 156 | PLOG_INFO << "Relay " << relay << " accepted event: " << event->id; 157 | publishPromise.set_value(make_tuple(relay, true)); 158 | } 159 | else 160 | { 161 | PLOG_WARNING << "Relay " << relay << " rejected event: " << event->id; 162 | publishPromise.set_value(make_tuple(relay, false)); 163 | } 164 | } 165 | ); 166 | }); 167 | 168 | if (!success) 169 | { 170 | PLOG_WARNING << "Failed to send event to relay " << relay; 171 | publishPromise.set_value(make_tuple(relay, false)); 172 | } 173 | } 174 | 175 | for (auto& publishFuture : publishFutures) 176 | { 177 | auto [relay, isSuccess] = publishFuture.get(); 178 | if (isSuccess) 179 | { 180 | successfulRelays.push_back(relay); 181 | } 182 | else 183 | { 184 | failedRelays.push_back(relay); 185 | } 186 | } 187 | 188 | std::size_t targetCount = targetRelays.size(); 189 | std::size_t successfulCount = successfulRelays.size(); 190 | PLOG_INFO << "Published event to " << successfulCount << "/" << targetCount << " target relays."; 191 | 192 | return make_tuple(successfulRelays, failedRelays); 193 | }; 194 | 195 | // TODO: Add a timeout to this method to prevent hanging while waiting for the relay. 196 | future>> NostrServiceBase::queryRelays( 197 | shared_ptr filters) 198 | { 199 | return async(launch::async, [this, filters]() -> vector> 200 | { 201 | if (filters->limit > 64 || filters->limit < 1) 202 | { 203 | PLOG_WARNING << "Filters limit must be between 1 and 64, inclusive. Setting limit to 16."; 204 | filters->limit = 16; 205 | } 206 | 207 | vector> events; 208 | 209 | string subscriptionId = this->_generateSubscriptionId(); 210 | string request; 211 | 212 | try 213 | { 214 | request = filters->serialize(subscriptionId); 215 | } 216 | catch (const invalid_argument& e) 217 | { 218 | PLOG_ERROR << "Failed to serialize filters - invalid object: " << e.what(); 219 | throw e; 220 | } 221 | catch (const json::exception& je) 222 | { 223 | PLOG_ERROR << "Failed to serialize filters - JSON exception: " << je.what(); 224 | throw je; 225 | } 226 | 227 | vector>> requestFutures; 228 | 229 | unordered_set uniqueEventIds; 230 | 231 | // Send the same query to each relay. As events trickle in from each relay, they will be added 232 | // to the events vector. Duplicate copies of the same event will be ignored, as events are 233 | // stored on multiple relays. The function will block until all of the relays send an EOSE or 234 | // CLOSE message. 235 | for (const string relay : this->_activeRelays) 236 | { 237 | promise> eosePromise; 238 | requestFutures.push_back(move(eosePromise.get_future())); 239 | 240 | auto [uri, success] = this->_client->send( 241 | request, 242 | relay, 243 | [this, &relay, &events, &eosePromise, &uniqueEventIds](string payload) 244 | { 245 | this->_onSubscriptionMessage( 246 | payload, 247 | [&events, &uniqueEventIds](const string&, shared_ptr event) 248 | { 249 | // Check if the event is unique before adding. 250 | if (uniqueEventIds.insert(event->id).second) 251 | { 252 | events.push_back(event); 253 | } 254 | }, 255 | [relay, &eosePromise](const string&) 256 | { 257 | eosePromise.set_value(make_tuple(relay, true)); 258 | }, 259 | [relay, &eosePromise](const string&, const string&) 260 | { 261 | eosePromise.set_value(make_tuple(relay, false)); 262 | }); 263 | } 264 | ); 265 | 266 | if (success) 267 | { 268 | PLOG_INFO << "Sent query to relay " << relay; 269 | lock_guard lock(this->_propertyMutex); 270 | this->_subscriptions[subscriptionId].push_back(relay); 271 | } 272 | else 273 | { 274 | PLOG_WARNING << "Failed to send query to relay " << relay; 275 | eosePromise.set_value(make_tuple(uri, false)); 276 | } 277 | } 278 | 279 | 280 | // Close open subscriptions and disconnect from relays after events are received. 281 | 282 | for (auto& publishFuture : requestFutures) 283 | { 284 | auto [relay, isEose] = publishFuture.get(); 285 | if (isEose) 286 | { 287 | PLOG_INFO << "Received EOSE message from relay " << relay; 288 | } 289 | else 290 | { 291 | PLOG_WARNING << "Received CLOSE message from relay " << relay; 292 | this->closeRelayConnections({ relay }); 293 | } 294 | } 295 | this->closeSubscription(subscriptionId); 296 | 297 | return events; 298 | }); 299 | }; 300 | 301 | string NostrServiceBase::queryRelays( 302 | shared_ptr filters, 303 | function)> eventHandler, 304 | function eoseHandler, 305 | function closeHandler 306 | ) 307 | { 308 | vector successfulRelays; 309 | vector failedRelays; 310 | 311 | string subscriptionId = this->_generateSubscriptionId(); 312 | string request = filters->serialize(subscriptionId); 313 | vector>> requestFutures; 314 | for (const string relay : this->_activeRelays) 315 | { 316 | unique_lock lock(this->_propertyMutex); 317 | this->_subscriptions[subscriptionId].push_back(relay); 318 | lock.unlock(); 319 | 320 | future> requestFuture = async( 321 | [this, &relay, &request, &eventHandler, &eoseHandler, &closeHandler]() 322 | { 323 | return this->_client->send( 324 | request, 325 | relay, 326 | [this, &eventHandler, &eoseHandler, &closeHandler](string payload) 327 | { 328 | this->_onSubscriptionMessage(payload, eventHandler, eoseHandler, closeHandler); 329 | }); 330 | } 331 | ); 332 | requestFutures.push_back(move(requestFuture)); 333 | } 334 | 335 | for (auto& publishFuture : requestFutures) 336 | { 337 | auto [relay, isSuccess] = publishFuture.get(); 338 | if (isSuccess) 339 | { 340 | successfulRelays.push_back(relay); 341 | } 342 | else 343 | { 344 | failedRelays.push_back(relay); 345 | } 346 | } 347 | 348 | std::size_t targetCount = this->_activeRelays.size(); 349 | std::size_t successfulCount = successfulRelays.size(); 350 | PLOG_INFO << "Sent query to " << successfulCount << "/" << targetCount << " open relay connections."; 351 | 352 | return subscriptionId; 353 | }; 354 | 355 | tuple, vector> NostrServiceBase::closeSubscription(string subscriptionId) 356 | { 357 | vector successfulRelays; 358 | vector failedRelays; 359 | 360 | vector subscriptionRelays; 361 | std::size_t subscriptionRelayCount; 362 | vector>> closeFutures; 363 | 364 | try 365 | { 366 | unique_lock lock(this->_propertyMutex); 367 | subscriptionRelays = this->_subscriptions.at(subscriptionId); 368 | subscriptionRelayCount = subscriptionRelays.size(); 369 | lock.unlock(); 370 | } 371 | catch (const out_of_range& oor) 372 | { 373 | PLOG_WARNING << "Subscription " << subscriptionId << " not found."; 374 | return make_tuple(successfulRelays, failedRelays); 375 | } 376 | 377 | for (const string relay : subscriptionRelays) 378 | { 379 | future> closeFuture = async([this, subscriptionId, relay]() 380 | { 381 | bool success = this->closeSubscription(subscriptionId, relay); 382 | 383 | return make_tuple(relay, success); 384 | }); 385 | closeFutures.push_back(move(closeFuture)); 386 | } 387 | 388 | for (auto& closeFuture : closeFutures) 389 | { 390 | auto [uri, success] = closeFuture.get(); 391 | if (success) 392 | { 393 | successfulRelays.push_back(uri); 394 | } 395 | else 396 | { 397 | failedRelays.push_back(uri); 398 | } 399 | } 400 | 401 | std::size_t successfulCount = successfulRelays.size(); 402 | PLOG_INFO << "Sent CLOSE request for subscription " << subscriptionId << " to " << successfulCount << "/" << subscriptionRelayCount << " open relay connections."; 403 | 404 | // If there were no failures, and the subscription has been closed on all of its relays, forget 405 | // about the subscription. 406 | if (failedRelays.empty()) 407 | { 408 | lock_guard lock(this->_propertyMutex); 409 | this->_subscriptions.erase(subscriptionId); 410 | } 411 | 412 | return make_tuple(successfulRelays, failedRelays); 413 | }; 414 | 415 | bool NostrServiceBase::closeSubscription(string subscriptionId, string relay) 416 | { 417 | if (!this->_hasSubscription(subscriptionId, relay)) 418 | { 419 | PLOG_WARNING << "Subscription " << subscriptionId << " not found on relay " << relay; 420 | return false; 421 | } 422 | 423 | if (!this->_isConnected(relay)) 424 | { 425 | PLOG_WARNING << "Relay " << relay << " is not connected."; 426 | return false; 427 | } 428 | 429 | string request = this->_generateCloseRequest(subscriptionId); 430 | auto [uri, success] = this->_client->send(request, relay); 431 | 432 | if (success) 433 | { 434 | lock_guard lock(this->_propertyMutex); 435 | auto it = find( 436 | this->_subscriptions[subscriptionId].begin(), 437 | this->_subscriptions[subscriptionId].end(), 438 | relay); 439 | 440 | if (it != this->_subscriptions[subscriptionId].end()) 441 | { 442 | this->_subscriptions[subscriptionId].erase(it); 443 | } 444 | 445 | PLOG_INFO << "Sent close request for subscription " << subscriptionId << " to relay " << relay; 446 | } 447 | else 448 | { 449 | PLOG_WARNING << "Failed to send close request to relay " << relay; 450 | } 451 | 452 | return success; 453 | }; 454 | 455 | vector NostrServiceBase::closeSubscriptions() 456 | { 457 | unique_lock lock(this->_propertyMutex); 458 | vector subscriptionIds; 459 | for (auto& [subscriptionId, relays] : this->_subscriptions) 460 | { 461 | subscriptionIds.push_back(subscriptionId); 462 | } 463 | lock.unlock(); 464 | 465 | vector remainingSubscriptions; 466 | for (const string& subscriptionId : subscriptionIds) 467 | { 468 | auto [successes, failures] = this->closeSubscription(subscriptionId); 469 | if (!failures.empty()) 470 | { 471 | remainingSubscriptions.push_back(subscriptionId); 472 | } 473 | } 474 | 475 | return remainingSubscriptions; 476 | }; 477 | 478 | vector NostrServiceBase::_getConnectedRelays(vector relays) 479 | { 480 | PLOG_VERBOSE << "Identifying connected relays."; 481 | vector connectedRelays; 482 | for (string relay : relays) 483 | { 484 | bool isActive = find(this->_activeRelays.begin(), this->_activeRelays.end(), relay) 485 | != this->_activeRelays.end(); 486 | bool isConnected = this->_client->isConnected(relay); 487 | PLOG_VERBOSE << "Relay " << relay << " is active: " << isActive << ", is connected: " << isConnected; 488 | 489 | if (isActive && isConnected) 490 | { 491 | connectedRelays.push_back(relay); 492 | } 493 | else if (isActive && !isConnected) 494 | { 495 | this->_eraseActiveRelay(relay); 496 | } 497 | else if (!isActive && isConnected) 498 | { 499 | this->_activeRelays.push_back(relay); 500 | connectedRelays.push_back(relay); 501 | } 502 | } 503 | return connectedRelays; 504 | }; 505 | 506 | vector NostrServiceBase::_getUnconnectedRelays(vector relays) 507 | { 508 | PLOG_VERBOSE << "Identifying unconnected relays."; 509 | vector unconnectedRelays; 510 | for (string relay : relays) 511 | { 512 | bool isActive = find(this->_activeRelays.begin(), this->_activeRelays.end(), relay) 513 | != this->_activeRelays.end(); 514 | bool isConnected = this->_client->isConnected(relay); 515 | PLOG_VERBOSE << "Relay " << relay << " is active: " << isActive << ", is connected: " << isConnected; 516 | 517 | if (!isActive && !isConnected) 518 | { 519 | PLOG_VERBOSE << "Relay " << relay << " is not active and not connected."; 520 | unconnectedRelays.push_back(relay); 521 | } 522 | else if (isActive && !isConnected) 523 | { 524 | PLOG_VERBOSE << "Relay " << relay << " is active but not connected. Removing from active relays list."; 525 | this->_eraseActiveRelay(relay); 526 | unconnectedRelays.push_back(relay); 527 | } 528 | else if (!isActive && isConnected) 529 | { 530 | PLOG_VERBOSE << "Relay " << relay << " is connected but not active. Adding to active relays list."; 531 | this->_activeRelays.push_back(relay); 532 | } 533 | } 534 | return unconnectedRelays; 535 | }; 536 | 537 | bool NostrServiceBase::_isConnected(string relay) 538 | { 539 | auto it = find(this->_activeRelays.begin(), this->_activeRelays.end(), relay); 540 | if (it != this->_activeRelays.end()) // If the relay is in this->_activeRelays 541 | { 542 | return true; 543 | } 544 | return false; 545 | }; 546 | 547 | void NostrServiceBase::_eraseActiveRelay(string relay) 548 | { 549 | auto it = find(this->_activeRelays.begin(), this->_activeRelays.end(), relay); 550 | if (it != this->_activeRelays.end()) // If the relay is in this->_activeRelays 551 | { 552 | this->_activeRelays.erase(it); 553 | } 554 | }; 555 | 556 | void NostrServiceBase::_connect(string relay) 557 | { 558 | PLOG_VERBOSE << "Connecting to relay " << relay; 559 | this->_client->openConnection(relay); 560 | 561 | lock_guard lock(this->_propertyMutex); 562 | bool isConnected = this->_client->isConnected(relay); 563 | 564 | if (isConnected) 565 | { 566 | PLOG_VERBOSE << "Connected to relay " << relay << ": " << isConnected; 567 | this->_activeRelays.push_back(relay); 568 | } 569 | else 570 | { 571 | PLOG_ERROR << "Failed to connect to relay " << relay; 572 | } 573 | }; 574 | 575 | void NostrServiceBase::_disconnect(string relay) 576 | { 577 | this->_client->closeConnection(relay); 578 | 579 | lock_guard lock(this->_propertyMutex); 580 | this->_eraseActiveRelay(relay); 581 | }; 582 | 583 | string NostrServiceBase::_generateSubscriptionId() 584 | { 585 | UUIDv4::UUIDGenerator uuidGenerator; 586 | UUIDv4::UUID uuid = uuidGenerator.getUUID(); 587 | return uuid.str(); 588 | }; 589 | 590 | string NostrServiceBase::_generateCloseRequest(string subscriptionId) 591 | { 592 | json jarr = json::array({ "CLOSE", subscriptionId }); 593 | return jarr.dump(); 594 | }; 595 | 596 | bool NostrServiceBase::_hasSubscription(string subscriptionId) 597 | { 598 | lock_guard lock(this->_propertyMutex); 599 | auto it = this->_subscriptions.find(subscriptionId); 600 | 601 | return it != this->_subscriptions.end(); 602 | }; 603 | 604 | bool NostrServiceBase::_hasSubscription(string subscriptionId, string relay) 605 | { 606 | lock_guard lock(this->_propertyMutex); 607 | auto subscriptionIt = this->_subscriptions.find(subscriptionId); 608 | 609 | if (subscriptionIt == this->_subscriptions.end()) 610 | { 611 | return false; 612 | } 613 | 614 | auto relays = this->_subscriptions[subscriptionId]; 615 | auto relayIt = find(relays.begin(), relays.end(), relay); 616 | 617 | return relayIt != relays.end(); 618 | }; 619 | 620 | void NostrServiceBase::_onSubscriptionMessage( 621 | string message, 622 | function)> eventHandler, 623 | function eoseHandler, 624 | function closeHandler 625 | ) 626 | { 627 | try 628 | { 629 | json jMessage = json::parse(message); 630 | string messageType = jMessage.at(0); 631 | if (messageType == "EVENT") 632 | { 633 | string subscriptionId = jMessage.at(1); 634 | nostr::data::Event event = nostr::data::Event::fromString(jMessage.at(2)); 635 | eventHandler(subscriptionId, make_shared(event)); 636 | } 637 | else if (messageType == "EOSE") 638 | { 639 | string subscriptionId = jMessage.at(1); 640 | eoseHandler(subscriptionId); 641 | } 642 | else if (messageType == "CLOSE") 643 | { 644 | string subscriptionId = jMessage.at(1); 645 | string reason = jMessage.at(2); 646 | closeHandler(subscriptionId, reason); 647 | } 648 | } 649 | catch (const json::out_of_range& joor) 650 | { 651 | PLOG_ERROR << "JSON out-of-range exception: " << joor.what(); 652 | throw joor; 653 | } 654 | catch (const json::exception& je) 655 | { 656 | PLOG_ERROR << "JSON handling exception: " << je.what(); 657 | throw je; 658 | } 659 | catch (const invalid_argument& ia) 660 | { 661 | PLOG_ERROR << "Invalid argument exception: " << ia.what(); 662 | throw ia; 663 | } 664 | }; 665 | 666 | void NostrServiceBase::_onAcceptance( 667 | string message, 668 | function acceptanceHandler 669 | ) 670 | { 671 | try 672 | { 673 | json jMessage = json::parse(message); 674 | string messageType = jMessage[0]; 675 | if (messageType == "OK") 676 | { 677 | bool isAccepted = jMessage[2]; 678 | acceptanceHandler(isAccepted); 679 | } 680 | } 681 | catch (const json::exception& je) 682 | { 683 | PLOG_ERROR << "JSON handling exception: " << je.what(); 684 | throw je; 685 | } 686 | }; 687 | -------------------------------------------------------------------------------- /src/signer/noscrypt_signer.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | #include "signer/noscrypt_signer.hpp" 12 | #include "../cryptography/nostr_secure_rng.hpp" 13 | #include "../cryptography/noscrypt_cipher.hpp" 14 | #include "../internal/noscrypt_logger.hpp" 15 | 16 | using namespace std; 17 | using namespace nostr::data; 18 | using namespace nostr::service; 19 | using namespace nostr::signer; 20 | using namespace nostr::cryptography; 21 | 22 | #pragma region Local Statics 23 | 24 | static shared_ptr ncAllocContext() 25 | { 26 | /* 27 | * Use the utilties library to allocate a new Noscrypt context. 28 | * Shared pointer will call free when smart pointer is destroyed 29 | */ 30 | 31 | return shared_ptr( 32 | NCUtilContextAlloc(), 33 | &NCUtilContextFree 34 | ); 35 | } 36 | 37 | static shared_ptr initNoscryptContext() 38 | { 39 | //Use helper to allocate a dynamic sized shared pointer 40 | auto ctx = ncAllocContext(); 41 | 42 | auto randomEntropy = make_unique(NC_CONTEXT_ENTROPY_SIZE); 43 | NostrSecureRng::fill(randomEntropy.get(), NC_CONTEXT_ENTROPY_SIZE); 44 | 45 | NCResult initResult = NCInitContext(ctx.get(), randomEntropy.get()); 46 | 47 | NC_LOG_ERROR(initResult); 48 | 49 | return ctx; 50 | }; 51 | 52 | /** 53 | * @brief Generates a private/public key pair for local use. 54 | * @returns The generated keypair of the form `[privateKey, publicKey]`, or a pair of empty 55 | * strings if the function failed. 56 | * @remarks This keypair is intended for temporary use, and should not be saved or used outside 57 | * of this class. 58 | */ 59 | static void createLocalKeypair( 60 | const shared_ptr ctx, 61 | shared_ptr secret, 62 | shared_ptr pubkey 63 | ) 64 | { 65 | NCResult secretValidationResult; 66 | 67 | NostrSecureRng::fill(secret.get(), sizeof(NCSecretKey)); 68 | secretValidationResult = NCValidateSecretKey(ctx.get(), secret.get()); 69 | 70 | NC_LOG_ERROR(secretValidationResult); 71 | 72 | // Use noscrypt to derive the public key from its private counterpart. 73 | NCResult pubkeyGenerationResult = NCGetPublicKey(ctx.get(), secret.get(), pubkey.get()); 74 | 75 | NC_LOG_ERROR(pubkeyGenerationResult); 76 | }; 77 | 78 | #pragma endregion 79 | 80 | #pragma region Constructors and Destructors 81 | 82 | NoscryptSigner::NoscryptSigner( 83 | shared_ptr appender, 84 | shared_ptr nostrService 85 | ) 86 | { 87 | plog::init(plog::debug, appender.get()); 88 | 89 | this->_noscryptContext = initNoscryptContext(); 90 | 91 | createLocalKeypair( 92 | this->_noscryptContext, 93 | this->_localPrivateKey, 94 | this->_localPublicKey 95 | ); 96 | 97 | this->_nostrService = nostrService; 98 | }; 99 | 100 | NoscryptSigner::~NoscryptSigner() 101 | { 102 | NCDestroyContext(this->_noscryptContext.get()); 103 | }; 104 | 105 | #pragma endregion 106 | 107 | #pragma region Public Interface 108 | 109 | void NoscryptSigner::receiveConnectionToken(string connectionToken) 110 | { 111 | if (connectionToken.empty()) 112 | { 113 | PLOG_ERROR << "No connection token was provided - unable to connect to a remote signer."; 114 | return; 115 | } 116 | 117 | int queryStart = this->_parseRemotePublicKey(connectionToken); 118 | if (queryStart == -1) 119 | { 120 | return; 121 | } 122 | 123 | string remainingToken = connectionToken.substr(queryStart); 124 | int splitIndex = remainingToken.find('&'); 125 | do 126 | { 127 | string param = remainingToken.substr(0, splitIndex); 128 | this->_handleConnectionTokenParam(param); 129 | 130 | remainingToken = remainingToken.substr(splitIndex + 1); 131 | splitIndex = remainingToken.find('&'); 132 | } while (splitIndex != string::npos); 133 | 134 | // TODO: Handle any messaging with the remote signer. 135 | }; 136 | 137 | string NoscryptSigner::generateConnectionToken( 138 | vector relays, 139 | string secret, 140 | optional name, 141 | optional url, 142 | optional description 143 | ) 144 | { 145 | // Return an empty string if the local keypair is invalid. 146 | if (this->_getLocalPrivateKey().empty() || this->_getLocalPublicKey().empty()) 147 | { 148 | PLOG_ERROR << "A valid local keypair is required to connect to a remote signer."; 149 | return string(); 150 | } 151 | 152 | // Return an empty string if no relays are provided. 153 | if (relays.empty()) 154 | { 155 | PLOG_ERROR << "At least one relay must be provided to connect to a remote signer."; 156 | return string(); 157 | } 158 | 159 | // Store the provided relay list to be used for sending and receving connection events. 160 | this->_relays = relays; 161 | 162 | // Generate the connection token. 163 | stringstream ss; 164 | ss << "nostrconnect://" << this->_localPublicKey; 165 | for (int i = 0; i < relays.size(); i++) 166 | { 167 | ss << (i == 0 ? "?" : "&"); 168 | ss << "relay=" << relays[i]; 169 | } 170 | ss << "&secret=" << secret; 171 | 172 | if (name.has_value()) 173 | { 174 | ss << "&name=" << name.value(); 175 | } 176 | 177 | if (url.has_value()) 178 | { 179 | ss << "&url=" << url.value(); 180 | } 181 | 182 | if (description.has_value()) 183 | { 184 | ss << "&description=" << description.value(); 185 | } 186 | 187 | return ss.str(); 188 | }; 189 | 190 | promise NoscryptSigner::ping() 191 | { 192 | promise pingPromise; 193 | 194 | // Generate a ping message and wrap it for the signer. 195 | nlohmann::json jrpc = 196 | { 197 | { "id", this->_generateSignerRequestId() }, 198 | { "method", "ping" }, 199 | { "params", nlohmann::json::array() } 200 | }; 201 | auto messageEvent = this->_wrapSignerMessage(jrpc); 202 | 203 | // Generate a filter to receive the response. 204 | auto pingFilter = this->_buildSignerMessageFilters(); 205 | 206 | this->_nostrService->publishEvent(messageEvent); 207 | 208 | // TODO: Handle the relay response. 209 | this->_nostrService->queryRelays( 210 | pingFilter, 211 | [this, &pingPromise](const string&, shared_ptr pongEvent) 212 | { 213 | // 214 | string pongMessage = this->_unwrapSignerMessage(pongEvent); 215 | pingPromise.set_value(pongMessage == "pong"); 216 | }, 217 | [&pingPromise](const string&) 218 | { 219 | pingPromise.set_value(false); 220 | }, 221 | [&pingPromise](const string&, const string&) 222 | { 223 | pingPromise.set_value(false); 224 | }); 225 | 226 | return pingPromise; 227 | }; 228 | 229 | shared_ptr> NoscryptSigner::sign(shared_ptr event) 230 | { 231 | auto signingPromise = make_shared>(); 232 | 233 | // TODO: Check latency with ping() to establish a reasonable timeout. 234 | 235 | // Create the JSON-RPC-like message content. 236 | auto params = nlohmann::json::array(); 237 | params.push_back(event->serialize()); 238 | 239 | auto requestId = this->_generateSignerRequestId(); 240 | 241 | // Create a filter set to find events from the remote signer. 242 | auto remoteSignerFilters = this->_buildSignerMessageFilters(); 243 | 244 | // Generate the signing request event. 245 | nlohmann::json jrpc = { 246 | { "id", requestId }, 247 | { "method", "sign_event" }, 248 | { "params", params } 249 | }; 250 | auto signingRequest = this->_wrapSignerMessage(jrpc); 251 | 252 | // Send the signing request. 253 | this->_nostrService->publishEvent(signingRequest); 254 | 255 | // Wait for the remote signer's response. 256 | this->_nostrService->queryRelays( 257 | remoteSignerFilters, 258 | [this, &event, &signingPromise](const string&, shared_ptr signerEvent) 259 | { 260 | // Assign the response event to the `event` parameter, accomplishing the intended 261 | // function result via side effect. 262 | string signerResponse = this->_unwrapSignerMessage(signerEvent); 263 | event = make_shared(Event::fromString(signerResponse)); 264 | signingPromise->set_value(true); 265 | }, 266 | [&signingPromise](const string&) 267 | { 268 | signingPromise->set_value(false); 269 | }, 270 | [&signingPromise](const string&, const string&) 271 | { 272 | signingPromise->set_value(false); 273 | } 274 | ); 275 | 276 | return signingPromise; 277 | }; 278 | 279 | #pragma endregion 280 | 281 | #pragma region Private Accessors 282 | 283 | inline string NoscryptSigner::_getLocalPrivateKey() const 284 | { 285 | stringstream privkeyStream; 286 | for (int i = 0; i < sizeof(NCSecretKey); i++) 287 | { 288 | privkeyStream << hex << setw(2) << setfill('0') << static_cast(this->_localPrivateKey->key[i]); 289 | } 290 | 291 | return privkeyStream.str(); 292 | }; 293 | 294 | inline void NoscryptSigner::_setLocalPrivateKey(const string value) 295 | { 296 | auto seckey = make_unique(); 297 | 298 | for (auto i = 0; i < sizeof(NCSecretKey); i++) 299 | { 300 | stringstream ss; 301 | ss << hex << value.substr(i * 2, 2); 302 | uint8_t byte; 303 | ss >> byte; 304 | seckey->key[i] = byte; 305 | } 306 | 307 | this->_localPrivateKey = move(seckey); 308 | }; 309 | 310 | inline string NoscryptSigner::_getLocalPublicKey() const 311 | { 312 | stringstream pubkeyStream; 313 | for (int i = 0; i < sizeof(NCPublicKey); i++) 314 | { 315 | pubkeyStream << hex << setw(2) << setfill('0') << static_cast(this->_localPublicKey->key[i]); 316 | } 317 | 318 | return pubkeyStream.str(); 319 | }; 320 | 321 | inline void NoscryptSigner::_setLocalPublicKey(const string value) 322 | { 323 | auto pubkey = make_unique(); 324 | 325 | for (auto i = 0; i < sizeof(NCPublicKey); i++) 326 | { 327 | stringstream ss; 328 | ss << hex << value.substr(i * 2, 2); 329 | uint8_t byte; 330 | ss >> byte; 331 | pubkey->key[i] = byte; 332 | } 333 | 334 | this->_localPublicKey = move(pubkey); 335 | }; 336 | 337 | inline string NoscryptSigner::_getRemotePublicKey() const 338 | { 339 | stringstream pubkeyStream; 340 | for (int i = 0; i < sizeof(NCPublicKey); i++) 341 | { 342 | pubkeyStream << hex << setw(2) << setfill('0') << static_cast(this->_remotePublicKey->key[i]); 343 | } 344 | 345 | return pubkeyStream.str(); 346 | }; 347 | 348 | inline void NoscryptSigner::_setRemotePublicKey(const string value) 349 | { 350 | auto pubkey = make_unique(); 351 | 352 | for (auto i = 0; i < sizeof(NCPublicKey); i++) 353 | { 354 | stringstream ss; 355 | ss << hex << value.substr(i * 2, 2); 356 | uint8_t byte; 357 | ss >> byte; 358 | pubkey->key[i] = byte; 359 | } 360 | 361 | this->_remotePublicKey = move(pubkey); 362 | }; 363 | 364 | #pragma endregion 365 | 366 | #pragma region Setup 367 | 368 | int NoscryptSigner::_parseRemotePublicKey(string connectionToken) 369 | { 370 | int queryStart = connectionToken.find('?'); 371 | if (queryStart == string::npos) 372 | { 373 | PLOG_ERROR << "The connection token is invalid - no query string was found."; 374 | return -1; 375 | } 376 | 377 | const int pubkeyStart = 9; 378 | string prefix = connectionToken.substr(0, pubkeyStart); 379 | if (prefix != "bunker://") 380 | { 381 | PLOG_ERROR << "The connection token is invalid - the token must begin with 'bunker://'."; 382 | return -1; 383 | } 384 | 385 | string remotePubkey = connectionToken.substr(pubkeyStart, queryStart); 386 | this->_setRemotePublicKey(remotePubkey); 387 | 388 | return queryStart + 1; 389 | }; 390 | 391 | void NoscryptSigner::_handleConnectionTokenParam(string param) 392 | { 393 | // Parse the query param into a key-value pair. 394 | int splitIndex = param.find('='); 395 | if (splitIndex == string::npos) 396 | { 397 | PLOG_ERROR << "The connection token query param is invalid - it is not of the form 'key=value'."; 398 | return; 399 | } 400 | 401 | string key = param.substr(0, splitIndex); 402 | string value = param.substr(splitIndex + 1); 403 | 404 | // Handle the key-value pair. 405 | if (key == "relay") 406 | { 407 | this->_relays.push_back(value); 408 | } 409 | else if (key == "secret") 410 | { 411 | this->_bunkerSecret = value; 412 | } 413 | }; 414 | 415 | #pragma endregion 416 | 417 | #pragma region Signer Helpers 418 | 419 | inline string NoscryptSigner::_generateSignerRequestId() const 420 | { 421 | UUIDv4::UUIDGenerator uuidGenerator; 422 | UUIDv4::UUID uuid = uuidGenerator.getUUID(); 423 | return uuid.str(); 424 | }; 425 | 426 | shared_ptr NoscryptSigner::_wrapSignerMessage(nlohmann::json jrpc) 427 | { 428 | // Encrypt the message payload. 429 | string encryptedContent; 430 | switch (this->_nostrConnectEncryption) 431 | { 432 | case Encryption::NIP44: 433 | encryptedContent = this->_encryptNip44(jrpc.dump()); 434 | if (!encryptedContent.empty()) 435 | { 436 | break; 437 | } 438 | 439 | // Use NIP-04 encryption as a fallback. 440 | case Encryption::NIP04: 441 | encryptedContent = this->_encryptNip04(jrpc.dump()); 442 | break; 443 | } 444 | 445 | // Wrap the event to be signed in a signing request event. 446 | auto wrapperEvent = make_shared(); 447 | wrapperEvent->pubkey = this->_getLocalPublicKey(); 448 | wrapperEvent->kind = this->_nostrConnectKind; 449 | wrapperEvent->tags.push_back({ "p", this->_getRemotePublicKey() }); 450 | wrapperEvent->content = encryptedContent; 451 | 452 | uint8_t schnorrSig[64]; 453 | uint8_t random32[32]; 454 | 455 | //Secure random signing entropy is required 456 | NostrSecureRng::fill(random32, sizeof(random32)); 457 | 458 | // Sign the wrapper message with the local secret key. 459 | string serializedEvent = wrapperEvent->serialize(); 460 | 461 | NCResult signatureResult = NCSignData( 462 | this->_noscryptContext.get(), 463 | this->_localPrivateKey.get(), 464 | random32, 465 | reinterpret_cast(serializedEvent.c_str()), 466 | serializedEvent.length(), 467 | schnorrSig 468 | ); 469 | 470 | //Random buffer could leak sensitive signing information 471 | NostrSecureRng::zero(random32, sizeof(random32)); 472 | 473 | // TODO: Handle result codes. 474 | if (signatureResult != NC_SUCCESS) 475 | { 476 | NC_LOG_ERROR(signatureResult); 477 | return nullptr; 478 | } 479 | 480 | // Add the signature to the event. 481 | wrapperEvent->sig = string( 482 | reinterpret_cast(schnorrSig), 483 | sizeof(schnorrSig) 484 | ); 485 | 486 | return wrapperEvent; 487 | }; 488 | 489 | string NoscryptSigner::_unwrapSignerMessage(shared_ptr event) 490 | { 491 | // TODO: Verify the incoming event. 492 | 493 | // Extract and decrypt the event payload. 494 | string encryptedContent = event->content; 495 | string decryptedContent; 496 | 497 | // NIP-04 encrypted strings include `?iv=` near the end (source: hodlbod). 498 | if (encryptedContent.find("?iv=") != string::npos) 499 | { 500 | decryptedContent = this->_decryptNip04(encryptedContent); 501 | } 502 | else 503 | { 504 | decryptedContent = this->_decryptNip44(encryptedContent); 505 | } 506 | 507 | // Parse the decrypted string into a JSON object. 508 | return decryptedContent; 509 | }; 510 | 511 | inline shared_ptr NoscryptSigner::_buildSignerMessageFilters() const 512 | { 513 | auto filters = make_shared(); 514 | filters->authors.push_back(this->_getRemotePublicKey()); 515 | filters->kinds.push_back(this->_nostrConnectKind); 516 | filters->tags["p"] = { this->_getLocalPublicKey() }; 517 | filters->since = time(nullptr); 518 | 519 | return filters; 520 | }; 521 | 522 | #pragma endregion 523 | 524 | #pragma region Cryptography 525 | 526 | string NoscryptSigner::_encryptNip04(std::string input) 527 | { 528 | throw runtime_error("NIP-04 encryption is not yet implemented."); 529 | }; 530 | 531 | string NoscryptSigner::_decryptNip04(string input) 532 | { 533 | throw runtime_error("NIP-04 decryption is not yet implemented."); 534 | }; 535 | 536 | string NoscryptSigner::_encryptNip44(string input) 537 | { 538 | NoscryptCipher cipher = NoscryptCipher( 539 | NoscryptCipherVersion::NIP44, 540 | NoscryptCipherMode::CIPHER_MODE_ENCRYPT 541 | ); 542 | 543 | auto output = cipher.update( 544 | this->_noscryptContext, 545 | this->_localPrivateKey, 546 | this->_remotePublicKey, 547 | input 548 | ); 549 | 550 | return output.empty() 551 | ? string() 552 | : NoscryptCipher::naiveEncodeBase64(output); 553 | }; 554 | 555 | string NoscryptSigner::_decryptNip44(string input) 556 | { 557 | //TODO handle input validation as per nip44 spec 558 | 559 | NoscryptCipher cipher = NoscryptCipher( 560 | NoscryptCipherVersion::NIP44, 561 | NoscryptCipherMode::CIPHER_MODE_DECRYPT 562 | ); 563 | 564 | return cipher.update( 565 | this->_noscryptContext, 566 | this->_localPrivateKey, 567 | this->_remotePublicKey, 568 | NoscryptCipher::naiveDecodeBase64(input) 569 | ); 570 | }; 571 | 572 | #pragma endregion 573 | 574 | -------------------------------------------------------------------------------- /src/signer/test_scenarios.md: -------------------------------------------------------------------------------- 1 | # Test Scenarios for `nostr::signer` 2 | 3 | ## NIP-46: Remote Signing 4 | 5 | ### 1. Connection Handshake Initiated by the Remote Signer 6 | 7 | GIVEN a remote signer connected to a relay 8 | AND a client 9 | WHEN the remote signer provides the user a connection string of the form `bunker://?relay=` 10 | AND the user provides that connection string to the Nostr client 11 | THEN the client connects to the specified relay 12 | AND the client publishes a kind 24133 event to the relay 13 | AND the event has a `p` tag whose value is the remote signer's pubkey 14 | AND the event's `content` field is a NIP-04 encrypted JSON object containing the fields: `id`, `method`, and `params` 15 | AND the value of the `id` field is a unique string 16 | AND the value of the `method` field is `"connect"` 17 | AND the value of the `params` field is a JSON array containing the remote signer's pubkey in the first position 18 | AND the client establishes a subscription to the relay for kind 24133 events with a `p` tag containing the remote signer's pubkey and a `since` value equal to the time at which the client sent the event 19 | 20 | ### 2. Connection Handshake Completed by the Remote Signer 21 | 22 | GIVEN a remote signer connected to a relay 23 | AND a client connected to the same relay 24 | AND the client has published a kind 24133 event whose `content` field contains a `"connect"` method call 25 | AND the client has created a subscription to the relay for kind 24133 events with a `p` tag containing the remote signer's pubkey and a `since` value equal to the time at which the client sent the `"connect"` method call event 26 | WHEN the remote signer sends a kind 24133 event that matches the client's filter 27 | THEN the client shall record the event's `pubkey` value as the remote signer's pubkey 28 | AND the client shall decrypt the event's `content` field using NIP-04 29 | AND the client shall verify that the decrypted content is a JSON object containting the field `result` whose value is `"ack"` 30 | 31 | ### 3. Connection Handshake Initiated by the Client 32 | 33 | GIVEN a client connected to a relay 34 | AND a remote signer 35 | WHEN the client provides the user a connection string of the form `nostrconnect://?relay=&secret=` 36 | AND the user provides that connection string to the remote signer 37 | THEN the client creates a subscription to the specified relay for kind 24133 events with a `p` tag containing the client's pubkey and a `since` value equal to the time at which the client generated the connection string 38 | 39 | ### 4. Client-Initiated Connection Handshake Completed by the Remote Signer 40 | 41 | GIVEN a client connected to a relay 42 | AND a remote signer that has received a `nostrconnect://` connection string generated by the client 43 | AND a client subscription to the relay for kind 24133 events with a `p` tag containing the client's pubkey and a `since` value equal to the time at which the client generated the connection string 44 | WHEN the client receives a matching event from the relay 45 | THEN the client shall record the event's `pubkey` value as the remote signer's pubkey 46 | AND the client shall decrypt the event's `content` field using NIP-04 47 | AND the client shall verify that the decrypted content is a JSON object containting the field `result` whose value is equal to the `secret` param in the client-generated connection string 48 | -------------------------------------------------------------------------------- /test/nostr_bech32_test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | using namespace nostr::encoding; 5 | using namespace ::testing; 6 | 7 | namespace nostr_test 8 | { 9 | 10 | class Bech32Test: public testing::Test 11 | { 12 | protected: 13 | Bech32Test() {} 14 | ~Bech32Test() override {} 15 | void SetUp() override {} 16 | }; 17 | 18 | 19 | TEST_F(Bech32Test, NpubEncoding) { 20 | 21 | NostrBech32 encoder = NostrBech32(); 22 | NostrBech32Encoding input; 23 | input.data.npub.pubkey = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"; 24 | input.type = NOSTR_BECH32_NPUB; 25 | 26 | std::string expectedEncoding = "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6"; 27 | std::string encoding; 28 | 29 | if (!encoder.encodeNostrBech32(input, encoding)) 30 | FAIL() << "Failed to encode note"; 31 | ASSERT_EQ(expectedEncoding.compare(encoding), 0); 32 | } 33 | 34 | TEST_F(Bech32Test, NsecEncoding) { 35 | NostrBech32 encoder = NostrBech32(); 36 | NostrBech32Encoding input; 37 | input.data.nsec.privkey = "67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa"; 38 | input.type = NOSTR_BECH32_NSEC; 39 | 40 | std::string expectedEncoding = "nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5"; 41 | std::string encoding; 42 | 43 | if (!encoder.encodeNostrBech32(input, encoding)) 44 | FAIL() << "Failed to encode note"; 45 | ASSERT_EQ(expectedEncoding.compare(encoding), 0); 46 | } 47 | 48 | TEST_F(Bech32Test, NoteEncoding) { 49 | 50 | NostrBech32 encoder = NostrBech32(); 51 | NostrBech32Encoding input; 52 | input.data.note.event_id = "7cc7cc7eb9a1012079adef2bce95008c820f77c5a12bc6ed1a22ed6db79dd8bd"; 53 | input.type = NOSTR_BECH32_NOTE; 54 | 55 | std::string expectedEncoding = "note10nrucl4e5yqjq7ddau4ua9gq3jpq7a795y4udmg6ytkkmduamz7semt62g"; 56 | std::string encoding; 57 | 58 | if (!encoder.encodeNostrBech32(input, encoding)) 59 | FAIL() << "Failed to encode note"; 60 | ASSERT_EQ(expectedEncoding.compare(encoding), 0); 61 | } 62 | 63 | TEST_F(Bech32Test, NprofileEncoding) { 64 | NostrBech32 encoder = NostrBech32(); 65 | NostrBech32Encoding input; 66 | input.data.nprofile.pubkey = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"; 67 | input.data.nprofile.relays = {"wss://r.x.com", "wss://djbas.sadkb.com"}; 68 | input.type = NOSTR_BECH32_NPROFILE; 69 | 70 | std::string expectedEncoding = "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p"; 71 | std::string encoding; 72 | 73 | if (!encoder.encodeNostrBech32(input, encoding)) 74 | FAIL() << "Failed to encode note"; 75 | ASSERT_EQ(expectedEncoding.compare(encoding), 0); 76 | } 77 | 78 | TEST_F(Bech32Test, NeventEncoding) { 79 | NostrBech32 encoder = NostrBech32(); 80 | NostrBech32Encoding input; 81 | 82 | input.data.nevent.event_id = "6a72a9c4244745b1f8bd99270a1526c905dd915f6b1061061863f4baa6b0b710"; 83 | input.data.nevent.pubkey = "0689df5847a8d3376892da29622d7c0fdc1ef1958f4bc4471d90966aa1eca9f2"; 84 | input.data.nevent.relays = {"wss://relay.nostr.band/"}; 85 | input.data.nevent.has_kind = false; 86 | input.type = NOSTR_BECH32_NEVENT; 87 | 88 | std::string expectedEncoding = "nevent1qqsx5u4fcsjyw3d3lz7ejfc2z5nvjpwaj90kkyrpqcvx8a9656ctwyqpzamhxue69uhhyetvv9ujumn0wd68ytnzv9hxgtczyqrgnh6cg75dxdmgjtdzjc3d0s8ac8h3jk85h3z8rkgfv64paj5lyznxtln"; 89 | std::string encoding; 90 | 91 | if (!encoder.encodeNostrBech32(input, encoding)) 92 | FAIL() << "Failed to encode note"; 93 | ASSERT_EQ(expectedEncoding.compare(encoding), 0); 94 | } 95 | 96 | TEST_F(Bech32Test, NaddrEncoding) { 97 | NostrBech32 encoder = NostrBech32(); 98 | NostrBech32Encoding input; 99 | 100 | input.data.naddr.tag = "1737430513300"; 101 | input.data.naddr.pubkey = "75656740209960c74fe373e6943f8a21ab896889d8691276a60f86aadbc8f92a"; 102 | input.data.naddr.relays = {"wss://relay.nostr.band/"}; 103 | input.data.naddr.kind = 30023; 104 | input.type = NOSTR_BECH32_NADDR; 105 | 106 | std::string expectedEncoding = "naddr1qqxnzdenxu6rxvp4xyenxvpsqythwumn8ghj7un9d3shjtnwdaehgu3wvfskuep0qgs82et8gqsfjcx8fl3h8e55879zr2ufdzyas6gjw6nqlp42m0y0j2srqsqqqa285r8tkj"; 107 | std::string encoding; 108 | 109 | if (!encoder.encodeNostrBech32(input, encoding)) 110 | FAIL() << "Failed to encode note"; 111 | ASSERT_EQ(expectedEncoding.compare(encoding), 0); 112 | } 113 | 114 | TEST_F(Bech32Test, NpubDecoding) { 115 | NostrBech32 encoder = NostrBech32(); 116 | 117 | std::string encoding = "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6"; 118 | std::string expected_pubkey = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"; 119 | 120 | NostrBech32Encoding parsed; 121 | if (!encoder.parseNostrBech32(encoding, parsed)) 122 | FAIL() << "Failed to decode npub"; 123 | 124 | ASSERT_EQ(NOSTR_BECH32_NPUB, parsed.type); 125 | ASSERT_EQ(expected_pubkey, parsed.data.npub.pubkey); 126 | } 127 | 128 | TEST_F(Bech32Test, NsecDecoding) { 129 | NostrBech32 encoder = NostrBech32(); 130 | 131 | std::string encoding = "nsec1vl029mgpspedva04g90vltkh6fvh240zqtv9k0t9af8935ke9laqsnlfe5"; 132 | std::string expected_privkey = "67dea2ed018072d675f5415ecfaed7d2597555e202d85b3d65ea4e58d2d92ffa"; 133 | 134 | NostrBech32Encoding parsed; 135 | if (!encoder.parseNostrBech32(encoding, parsed)) 136 | FAIL() << "Failed to decode nsec"; 137 | 138 | ASSERT_EQ(NOSTR_BECH32_NSEC, parsed.type); 139 | ASSERT_EQ(expected_privkey, parsed.data.nsec.privkey); 140 | } 141 | 142 | TEST_F(Bech32Test, NoteDecoding) { 143 | NostrBech32 encoder = NostrBech32(); 144 | 145 | std::string encoding = "note10nrucl4e5yqjq7ddau4ua9gq3jpq7a795y4udmg6ytkkmduamz7semt62g"; 146 | std::string expected_id = "7cc7cc7eb9a1012079adef2bce95008c820f77c5a12bc6ed1a22ed6db79dd8bd"; 147 | 148 | NostrBech32Encoding parsed; 149 | if (!encoder.parseNostrBech32(encoding, parsed)) 150 | FAIL() << "Failed to decode note"; 151 | 152 | ASSERT_EQ(NOSTR_BECH32_NOTE, parsed.type); 153 | ASSERT_EQ(expected_id, parsed.data.note.event_id); 154 | } 155 | 156 | TEST_F(Bech32Test, NprofileDecoding) { 157 | NostrBech32 encoder = NostrBech32(); 158 | 159 | std::string encoding = "nprofile1qqsrhuxx8l9ex335q7he0f09aej04zpazpl0ne2cgukyawd24mayt8gpp4mhxue69uhhytnc9e3k7mgpz4mhxue69uhkg6nzv9ejuumpv34kytnrdaksjlyr9p"; 160 | std::string expected_pubkey = "3bf0c63fcb93463407af97a5e5ee64fa883d107ef9e558472c4eb9aaaefa459d"; 161 | Relays expected_relays = {"wss://r.x.com", "wss://djbas.sadkb.com"}; 162 | 163 | NostrBech32Encoding parsed; 164 | if (!encoder.parseNostrBech32(encoding, parsed)) 165 | FAIL() << "Failed to decode note"; 166 | 167 | ASSERT_EQ(NOSTR_BECH32_NPROFILE, parsed.type); 168 | ASSERT_EQ(expected_pubkey, parsed.data.nprofile.pubkey); 169 | ASSERT_EQ(expected_relays.size(), parsed.data.nprofile.relays.size()); 170 | for (int i=0; i 2 | #include 3 | 4 | #include "data/data.hpp" 5 | #include "cryptography/nostr_bech32.hpp" 6 | 7 | using namespace nostr::data; 8 | using namespace nostr::encoding; 9 | using namespace std; 10 | using namespace ::testing; 11 | 12 | shared_ptr testEvent() 13 | { 14 | auto event = make_shared(); 15 | 16 | event->pubkey = "13tn5ccv2guflxgffq4aj0hw5x39pz70zcdrfd6vym887gry38zys28dask"; 17 | event->createdAt = 1627846261; 18 | event->kind = 1; 19 | event->tags = { 20 | { "e", "5c83da77af1dec6d7289834998ad7aafbd9e2191396d75ec3cc27f5a77226f36", "wss://gitcitadel.nostr1.com" }, 21 | { "p", "f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca" }, 22 | { "a", "30023:f7234bd4c1394dda46d09f35bd384dd30cc552ad5541990f98844fb06676e9ca:abcd", "wss://gitcitadel.nostr1.com" } 23 | }; 24 | event->content = "Hello, World!"; 25 | 26 | return event; 27 | } 28 | 29 | shared_ptr testBech32() 30 | { 31 | auto event = make_shared(); 32 | 33 | event->pubkey = "dc4cd086cd7ce5b1832adf4fdd1211289880d2c7e295bcb0e684c01acee77c06"; 34 | event->id = "bf39598ec3b67e208e26f54e3744e5adf5221dbf1c480f9b673f21dddac8c7ef"; 35 | event->createdAt = 1741372469; 36 | event->kind = 30023; 37 | event->tags = { 38 | { "d", "mfayffebPrMI520ftzIlE" } 39 | }; 40 | event->content = "Hello, World!"; 41 | 42 | return event; 43 | } 44 | 45 | shared_ptr makeNostrEvent() 46 | { 47 | auto base_event = testBech32(); 48 | return std::make_shared(NostrEvent(base_event)); 49 | } 50 | 51 | TEST(NostrEventTest, Equivalent_Events_Have_Same_ID) 52 | { 53 | // Create two events with the same values 54 | auto event1 = testEvent(); 55 | auto event2 = testEvent(); 56 | 57 | // Serialize both events 58 | string serializedEvent1 = event1->serialize(); 59 | string serializedEvent2 = event2->serialize(); 60 | 61 | auto event1WithId = Event::fromString(serializedEvent1); 62 | auto event2WithId = Event::fromString(serializedEvent2); 63 | 64 | // Hash both serialized events using sha256 65 | string id1 = event1WithId.id; 66 | string id2 = event2WithId.id; 67 | 68 | // Verify that both hashes are equal 69 | ASSERT_EQ(id1, id2); 70 | } 71 | 72 | TEST(NostrEventTest, Special_Characters_Are_Escaped_When_Serialized) 73 | { 74 | // Test backspace (0x08) 75 | auto backspaceEvent = testEvent(); 76 | backspaceEvent->content = string("Hello") + char(0x08) + "World"; 77 | string serializedBackspace = backspaceEvent->serialize(); 78 | EXPECT_THAT(serializedBackspace, HasSubstr("\\b")); 79 | 80 | // Test tab (0x09) 81 | auto tabEvent = testEvent(); 82 | tabEvent->content = string("Hello") + char(0x09) + "World"; 83 | string serializedTab = tabEvent->serialize(); 84 | EXPECT_THAT(serializedTab, HasSubstr("\\t")); 85 | 86 | // Test newline (0x0A) 87 | auto newlineEvent = testEvent(); 88 | newlineEvent->content = string("Hello") + char(0x0A) + "World"; 89 | string serializedNewline = newlineEvent->serialize(); 90 | EXPECT_THAT(serializedNewline, HasSubstr("\\n")); 91 | 92 | // Test form feed (0x0C) 93 | auto formFeedEvent = testEvent(); 94 | formFeedEvent->content = string("Hello") + char(0x0C) + "World"; 95 | string serializedFormFeed = formFeedEvent->serialize(); 96 | EXPECT_THAT(serializedFormFeed, HasSubstr("\\f")); 97 | 98 | // Test carriage return (0x0D) 99 | auto crEvent = testEvent(); 100 | crEvent->content = string("Hello") + char(0x0D) + "World"; 101 | string serializedCr = crEvent->serialize(); 102 | EXPECT_THAT(serializedCr, HasSubstr("\\r")); 103 | 104 | // Test double quote (0x22) 105 | auto quoteEvent = testEvent(); 106 | quoteEvent->content = string("Hello") + char(0x22) + "World"; 107 | string serializedQuote = quoteEvent->serialize(); 108 | EXPECT_THAT(serializedQuote, HasSubstr("\\\"")); 109 | 110 | // Test backslash (0x5C) 111 | auto backslashEvent = testEvent(); 112 | backslashEvent->content = string("Hello") + char(0x5C) + "World"; 113 | string serializedBackslash = backslashEvent->serialize(); 114 | EXPECT_THAT(serializedBackslash, HasSubstr("\\\\")); 115 | } 116 | 117 | TEST(NostrEventTest, Bech32_On_Wrapper_Class) 118 | { 119 | auto nostr_event = makeNostrEvent(); 120 | std::string naddr = nostr_event->toNaddr(); 121 | std::string note = nostr_event->toNote(); 122 | std::string nevent = nostr_event->toNevent(); 123 | 124 | ASSERT_FALSE(naddr.empty()); 125 | ASSERT_FALSE(note.empty()); 126 | ASSERT_FALSE(nevent.empty()); 127 | 128 | NostrEvent decoded = NostrEvent(); 129 | decoded.fromNaddr(naddr); 130 | std::string reencoded_naddr = decoded.toNaddr(); 131 | 132 | ASSERT_EQ(reencoded_naddr, naddr); 133 | 134 | decoded = NostrEvent(); 135 | decoded.fromNote(note); 136 | std::string reencoded_note = decoded.toNote(); 137 | ASSERT_EQ(reencoded_note, note); 138 | 139 | decoded = NostrEvent(); 140 | decoded.fromNevent(nevent); 141 | std::string reencoded_nevent = decoded.toNevent(); 142 | ASSERT_EQ(reencoded_nevent, nevent); 143 | } -------------------------------------------------------------------------------- /vcpkg-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | "default-registry": { 3 | "kind": "git", 4 | "baseline": "582a4de14bef91df217f4f49624cf5b2b04bd7ca", 5 | "repository": "https://github.com/microsoft/vcpkg" 6 | }, 7 | "registries": [ 8 | { 9 | "kind": "artifact", 10 | "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip", 11 | "name": "microsoft" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /vcpkg.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | "nlohmann-json", 4 | "openssl", 5 | "plog", 6 | "websocketpp" 7 | ] 8 | } 9 | --------------------------------------------------------------------------------