├── .gitignore ├── COPYING ├── README.md ├── jelling.c ├── jelling.conf ├── jelling.service.in └── meson.build /.gitignore: -------------------------------------------------------------------------------- 1 | .autotools 2 | .cproject 3 | .project 4 | .settings/ 5 | 6 | .deps/ 7 | Makefile 8 | aclocal.m4 9 | autom4te.cache/ 10 | compile 11 | config.guess 12 | config.log 13 | config.status 14 | config.sub 15 | configure 16 | depcomp 17 | install-sh 18 | jelling 19 | missing 20 | 21 | *.service 22 | *.in 23 | *.o 24 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Jelling is a simple daemon for Linux which receives OTP tokens from FreeOTP. 2 | 3 | # Dependencies 4 | 5 | * systemd >= 221 6 | * bluez >= 5.42 7 | 8 | # How to Build and Install 9 | 10 | # meson build 11 | # ninja -C build 12 | # ninja -C build install 13 | 14 | # How to Run 15 | 16 | 1. Start and enable Jelling: 17 | 18 | # systemctl enable --now jelling.service 19 | 20 | # Test Results 21 | 22 | | Device | OS | Adv. | Connect | Discovery | Pair | GATT | 23 | | ---------- | ---------------- | ---- | ------- | --------- | ---- | ---- | 24 | | iPhone 6+ | iOS 11.2 | ✔ | ✔ | ✔ | ✔ | ✔ | 25 | | Nexus 5x | LineageOS 14.1 | ✔ | ✔ | ✘ | ✘ | ✘ | 26 | | Pixel | Android 8.1 beta | ✔ | ✘ | ✘ | ✘ | ✘ | 27 | 28 | # How to Test 29 | 30 | 1. Make sure your phone and computer are **NOT** paired. 31 | 32 | 2. Start Jelling on your computer. 33 | 34 | 3. Install and open LightBlue on your phone. 35 | 36 | 4. Does LightBlue see your computer within a few seconds (<30)? If so, 37 | advertisement is working. 38 | 39 | 5. Click on your computer in LightBlue. Does it successfully connect? Do you 40 | see Jelling among the list of services? The Jelling Service UUID is: 41 | `B670003C-0079-465C-9BA7-6C0539CCD67F`. If so, connection and service 42 | discovery are working. If not, you'll have to get Bluetooth packet logs to 43 | see whether connection or service discovery is failing. 44 | 45 | 6. Click on the service. Do you see the Jelling characteristic? The Jelling 46 | Characteristic UUID is: `B670003C-0079-465C-9BA7-6C0539CCD67F`. Is it 47 | listed as write-only? If so, discovery is working. 48 | 49 | 7. Attempt to perform a write operation in LightBlue against the 50 | characteristic. A good test value is `30` (which is hex for `0` in 51 | ASCII). Does the device attempt to pair? Does it succeed? If so, pairing is 52 | working. 53 | 54 | 8. If pairing succeeds, perform the previous step again. This time, instead of 55 | pairing it should perform the actual GATT write. Does your computer type 56 | `0`? If so, the GATT write is working. 57 | 58 | 9. Submit a pull request which updates the above table with your test results. 59 | -------------------------------------------------------------------------------- /jelling.c: -------------------------------------------------------------------------------- 1 | /* vim: set tabstop=8 shiftwidth=4 softtabstop=4 expandtab smarttab colorcolumn=80: */ 2 | /* 3 | * Authors: Nathaniel McCallum 4 | * 5 | * Copyright (C) 2015 Nathaniel McCallum, Red Hat 6 | * 7 | * Licensed under the Apache License, Version 2.0 (the "License"); 8 | * you may not use this file except in compliance with the License. 9 | * You may obtain a copy of the License at 10 | * 11 | * http://www.apache.org/licenses/LICENSE-2.0 12 | * 13 | * Unless required by applicable law or agreed to in writing, software 14 | * distributed under the License is distributed on an "AS IS" BASIS, 15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | * See the License for the specific language governing permissions and 17 | * limitations under the License. 18 | */ 19 | 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | 31 | #include 32 | #include 33 | 34 | #define MAN_PATH "/" 35 | #define ADV_PATH "/adv" 36 | #define SVC_PATH "/svc" 37 | #define CHR_PATH "/svc/chr" 38 | #define SVC_UUID "B670003C-0079-465C-9BA7-6C0539CCD67F" 39 | #define CHR_UUID "F4186B06-D796-4327-AF39-AC22C50BDCA8" 40 | 41 | #define CHR_FLAGS "secure-write" 42 | #define CHR_NFLAG 1 43 | 44 | #define PROP(name, sig, func) \ 45 | SD_BUS_PROPERTY(name, sig, func, 0, SD_BUS_VTABLE_PROPERTY_CONST) 46 | 47 | #define METH(name, sig, rsig, func) \ 48 | SD_BUS_METHOD(name, sig, rsig, func, SD_BUS_VTABLE_UNPRIVILEGED) 49 | 50 | #define MATCH \ 51 | "type='signal',sender='org.bluez',path='/',member='InterfacesAdded'," \ 52 | "interface='org.freedesktop.DBus.ObjectManager'" 53 | 54 | #define COUNT(array) (sizeof(array) / sizeof(*array)) 55 | 56 | #define SCOPED(type) \ 57 | __attribute__((cleanup(type ## _cleanup))) type 58 | 59 | typedef int uinput; 60 | 61 | static void 62 | uinput_cleanup(uinput *i) 63 | { 64 | if (i == NULL || *i < 0) 65 | return; 66 | 67 | ioctl(*i, UI_DEV_DESTROY); 68 | close(*i); 69 | } 70 | 71 | static void 72 | sd_bus_message_cleanup(sd_bus_message **msg) 73 | { 74 | if (msg == NULL || *msg == NULL) 75 | return; 76 | 77 | sd_bus_message_unref(*msg); 78 | } 79 | 80 | static void 81 | sd_bus_cleanup(sd_bus **bus) 82 | { 83 | if (bus == NULL || *bus == NULL) 84 | return; 85 | 86 | sd_bus_unref(*bus); 87 | } 88 | 89 | static inline uint16_t 90 | char2key(uint8_t c) 91 | { 92 | switch (c) { 93 | case '0': return KEY_0; 94 | case '1': return KEY_1; 95 | case '2': return KEY_2; 96 | case '3': return KEY_3; 97 | case '4': return KEY_4; 98 | case '5': return KEY_5; 99 | case '6': return KEY_6; 100 | case '7': return KEY_7; 101 | case '8': return KEY_8; 102 | case '9': return KEY_9; 103 | default: return KEY_UNKNOWN; 104 | } 105 | } 106 | 107 | static int 108 | adv_props(sd_bus *bus, const char *path, const char *interface, 109 | const char *property, sd_bus_message *reply, void *userdata, 110 | sd_bus_error *ret_error) 111 | { 112 | if (strcmp(property, "Type") == 0) 113 | return sd_bus_message_append(reply, "s", "peripheral"); 114 | 115 | if (strcmp(property, "ServiceUUIDs") == 0) 116 | return sd_bus_message_append(reply, "as", 1, SVC_UUID); 117 | 118 | if (strcmp(property, "Includes") == 0) 119 | return sd_bus_message_append(reply, "as", 1, "local-name"); 120 | 121 | return -ENOENT; 122 | } 123 | 124 | static int 125 | svc_props(sd_bus *bus, const char *path, const char *interface, 126 | const char *property, sd_bus_message *reply, void *userdata, 127 | sd_bus_error *ret_error) 128 | { 129 | if (strcmp(property, "UUID") == 0) 130 | return sd_bus_message_append(reply, "s", SVC_UUID); 131 | 132 | if (strcmp(property, "Primary") == 0) 133 | return sd_bus_message_append(reply, "b", true); 134 | 135 | if (strcmp(property, "Includes") == 0) 136 | return sd_bus_message_append(reply, "ao", 0); 137 | 138 | return -ENOENT; 139 | } 140 | 141 | static int 142 | chr_props(sd_bus *bus, const char *path, const char *interface, 143 | const char *property, sd_bus_message *reply, void *userdata, 144 | sd_bus_error *ret_error) 145 | { 146 | if (strcmp(property, "UUID") == 0) 147 | return sd_bus_message_append(reply, "s", CHR_UUID); 148 | 149 | if (strcmp(property, "Service") == 0) 150 | return sd_bus_message_append(reply, "o", SVC_PATH); 151 | 152 | if (strcmp(property, "Flags") == 0) 153 | return sd_bus_message_append(reply, "as", CHR_NFLAG, CHR_FLAGS); 154 | 155 | return -ENOENT; 156 | } 157 | 158 | static int 159 | chr_notsup(sd_bus_message *m, void *misc, sd_bus_error *err) 160 | { 161 | return sd_bus_error_set( 162 | err, "org.bluez.Error.NotSupported", "Not supported" 163 | ); 164 | } 165 | 166 | static int 167 | event(uinput input, uint16_t k, bool down) 168 | { 169 | const struct input_event evts[] = { 170 | { .type = EV_SYN }, 171 | { .type = EV_KEY, .code = k, .value = down }, 172 | }; 173 | 174 | for (size_t i = 0; i < COUNT(evts) && evts[i].code != KEY_UNKNOWN; i++) { 175 | ssize_t r = write(input, &evts[i], sizeof(evts[i])); 176 | if (r < 0) 177 | return -errno; 178 | } 179 | 180 | usleep(50000); 181 | return down ? event(input, k, false) : 0; 182 | } 183 | 184 | static int 185 | chr_writevalue(sd_bus_message *m, void *misc, sd_bus_error *err) 186 | { 187 | const uint8_t *bytes = NULL; 188 | uinput *input = misc; 189 | size_t size = 0; 190 | int r; 191 | 192 | r = sd_bus_message_has_signature(m, "aya{sv}"); 193 | if (r < 0) 194 | return r; 195 | 196 | r = sd_bus_message_read_array(m, 'y', (const void **) &bytes, &size); 197 | if (r < 0) 198 | return r; 199 | 200 | if (size == 0 || size > 32) { 201 | return sd_bus_reply_method_errorf( 202 | m, "org.bluez.Error.InvalidValueLength", "Invalid value length" 203 | ); 204 | } 205 | 206 | /* Validate input. */ 207 | for (size_t i = 0; i < size; i++) { 208 | if (char2key(bytes[i]) == KEY_UNKNOWN) { 209 | return sd_bus_reply_method_errorf( 210 | m, "org.bluez.Error.NotPermitted", "Invalid value" 211 | ); 212 | } 213 | } 214 | 215 | for (size_t i = 0; i < size && r >= 0; i++) 216 | r = event(*input, char2key(bytes[i]), true); 217 | if (r >= 0) 218 | r = event(*input, KEY_ENTER, true); 219 | if (r >= 0) 220 | r = event(*input, KEY_UNKNOWN, false); 221 | if (r < 0) { 222 | return sd_bus_reply_method_errorf( 223 | m, "org.bluez.Error.Failed", "Write failed" 224 | ); 225 | } 226 | 227 | return sd_bus_reply_method_return(m, ""); 228 | } 229 | 230 | static int 231 | meth_noop(sd_bus_message *m, void *misc, sd_bus_error *err) 232 | { 233 | return sd_bus_reply_method_return(m, ""); 234 | } 235 | 236 | static const sd_bus_vtable adv_vtable[] = { 237 | SD_BUS_VTABLE_START(0), 238 | PROP("Type", "s", adv_props), 239 | PROP("ServiceUUIDs", "as", adv_props), 240 | PROP("Includes", "as", adv_props), 241 | METH("Release", "", "", meth_noop), 242 | SD_BUS_VTABLE_END 243 | }; 244 | 245 | static const sd_bus_vtable svc_vtable[] = { 246 | SD_BUS_VTABLE_START(0), 247 | PROP("UUID", "s", svc_props), 248 | PROP("Primary", "b", svc_props), 249 | PROP("Includes", "ao", svc_props), 250 | SD_BUS_VTABLE_END 251 | }; 252 | 253 | static const sd_bus_vtable chr_vtable[] = { 254 | SD_BUS_VTABLE_START(0), 255 | PROP("UUID", "s", chr_props), 256 | PROP("Service", "o", chr_props), 257 | PROP("Flags", "as", chr_props), 258 | METH("ReadValue", "a{sv}", "ay", chr_notsup), 259 | METH("WriteValue", "aya{sv}", "", chr_writevalue), 260 | METH("StartNotify", "", "", chr_notsup), 261 | METH("StopNotify", "", "", meth_noop), 262 | SD_BUS_VTABLE_END 263 | }; 264 | 265 | static int 266 | on_reply(sd_bus_message *m, void *userdata, sd_bus_error *ret_error) 267 | { 268 | if (sd_bus_error_is_set(ret_error)) 269 | fprintf(stderr, "Error registering: %s: %s\n", 270 | ret_error->name, ret_error->message); 271 | 272 | return 0; 273 | } 274 | 275 | static int 276 | on_bt_iface(sd_bus_message *m, void *bus, sd_bus_error *ret_error) 277 | { 278 | const char *obj = NULL; 279 | int r; 280 | 281 | r = sd_bus_message_has_signature(m, "oa{sa{sv}}"); 282 | if (r < 0) 283 | return r; 284 | 285 | r = sd_bus_message_read(m, "o", &obj); 286 | if (r < 0) 287 | return r; 288 | 289 | r = sd_bus_message_enter_container(m, 'a', "{sa{sv}}"); 290 | if (r < 0) 291 | return r; 292 | 293 | while ((r = sd_bus_message_enter_container(m, 'e', "sa{sv}")) > 0) { 294 | const char *iface = NULL; 295 | 296 | r = sd_bus_message_read(m, "s", &iface); 297 | if (r < 0) 298 | return r; 299 | 300 | r = sd_bus_message_skip(m, "a{sv}"); 301 | if (r < 0) 302 | return r; 303 | 304 | r = sd_bus_message_exit_container(m); 305 | if (r < 0) 306 | return r; 307 | 308 | if (strcmp(iface, "org.bluez.GattManager1") == 0) { 309 | r = sd_bus_call_method_async(bus, NULL, "org.bluez", obj, iface, 310 | "RegisterApplication", on_reply, NULL, 311 | "oa{sv}", MAN_PATH, 0); 312 | if (r < 0) 313 | return r; 314 | } 315 | 316 | if (strcmp(iface, "org.bluez.LEAdvertisingManager1") == 0) { 317 | r = sd_bus_call_method_async(bus, NULL, "org.bluez", obj, iface, 318 | "RegisterAdvertisement", on_reply, NULL, 319 | "oa{sv}", ADV_PATH, 0); 320 | if (r < 0) 321 | return r; 322 | } 323 | } 324 | if (r < 0) 325 | return r; 326 | 327 | r = sd_bus_message_exit_container(m); 328 | if (r < 0) 329 | return r; 330 | 331 | return 0; 332 | } 333 | 334 | static void 335 | setup_uinput(uinput *input) 336 | { 337 | static const struct uinput_user_dev dev = { 338 | .name = "Jelling", 339 | .id = { 340 | .bustype = BUS_USB, 341 | .vendor = 0xef0f, 342 | .product = 0xd746, 343 | .version = 1 344 | } 345 | }; 346 | 347 | static const char *devices[] = { 348 | "/dev/input/uinput", 349 | "/dev/uinput", 350 | "/dev/misc/uinput", 351 | NULL 352 | }; 353 | 354 | SCOPED(uinput) fd = -1; 355 | int r; 356 | 357 | for (size_t i = 0; fd < 0; i++) { 358 | fd = open(devices[i], O_WRONLY); 359 | if (fd < 0) { 360 | if (errno == ENOENT) 361 | continue; 362 | error(EXIT_FAILURE, errno, "Error opening %s", devices[i]); 363 | } 364 | } 365 | if (fd < 0) 366 | error(EXIT_FAILURE, errno, "Error finding uevent"); 367 | 368 | r = ioctl(fd, UI_SET_EVBIT, EV_KEY); 369 | if (r < 0) 370 | error(EXIT_FAILURE, errno, "Error setting uinput KEY type"); 371 | 372 | r = ioctl(fd, UI_SET_EVBIT, EV_SYN); 373 | if (r < 0) 374 | error(EXIT_FAILURE, errno, "Error setting uinput SYN type"); 375 | 376 | for (uint8_t c = 0; c < UINT8_MAX; c++) { 377 | uint16_t k = char2key(c); 378 | if (k == KEY_UNKNOWN) { 379 | if (c != '\n') 380 | continue; 381 | k = KEY_ENTER; 382 | } 383 | 384 | r = ioctl(fd, UI_SET_KEYBIT, k); 385 | if (r < 0) 386 | error(EXIT_FAILURE, errno, "Error setting uinput keybit: %c", c); 387 | } 388 | 389 | r = write(fd, &dev, sizeof(dev)); 390 | if (r < 0) 391 | error(EXIT_FAILURE, errno, "Error writing uinput device description"); 392 | 393 | r = ioctl(fd, UI_DEV_CREATE); 394 | if (r < 0) 395 | error(EXIT_FAILURE, errno, "Error creating uinput device"); 396 | 397 | *input = fd; 398 | fd = -1; 399 | } 400 | 401 | static void 402 | setup_objects(sd_bus *bus, uinput *i) 403 | { 404 | int r; 405 | 406 | r = sd_bus_add_object_manager(bus, NULL, MAN_PATH); 407 | if (r < 0) 408 | error(EXIT_FAILURE, -r, "Error adding object manager"); 409 | 410 | r = sd_bus_add_object_vtable(bus, NULL, ADV_PATH, 411 | "org.bluez.LEAdvertisement1", 412 | adv_vtable, i); 413 | if (r < 0) 414 | error(EXIT_FAILURE, -r, "Error creating advertisement"); 415 | 416 | r = sd_bus_add_object_vtable(bus, NULL, SVC_PATH, 417 | "org.bluez.GattService1", 418 | svc_vtable, i); 419 | if (r < 0) 420 | error(EXIT_FAILURE, -r, "Error creating service"); 421 | 422 | r = sd_bus_add_object_vtable(bus, NULL, CHR_PATH, 423 | "org.bluez.GattCharacteristic1", 424 | chr_vtable, i); 425 | if (r < 0) 426 | error(EXIT_FAILURE, -r, "Error creating characteristic"); 427 | } 428 | 429 | static void 430 | setup_registration(sd_bus *bus) 431 | { 432 | SCOPED(sd_bus_message) *msg = NULL; 433 | int r; 434 | 435 | r = sd_bus_add_match(bus, NULL, MATCH, on_bt_iface, bus); 436 | if (r < 0) 437 | error(EXIT_FAILURE, -r, "Error registering for bluetooth interfaces"); 438 | 439 | r = sd_bus_call_method(bus, "org.bluez", "/", 440 | "org.freedesktop.DBus.ObjectManager", 441 | "GetManagedObjects", NULL, &msg, ""); 442 | if (r < 0) 443 | error(EXIT_FAILURE, -r, "Error calling bluez ObjectManager"); 444 | 445 | r = sd_bus_message_enter_container(msg, 'a', "{oa{sa{sv}}}"); 446 | if (r < 0) 447 | error(EXIT_FAILURE, -r, "Error parsing bluez results"); 448 | 449 | while ((r = sd_bus_message_enter_container(msg, 'e', "oa{sa{sv}}")) > 0) { 450 | r = on_bt_iface(msg, bus, NULL); 451 | if (r < 0) 452 | error(EXIT_FAILURE, -r, "Error parsing bluez results"); 453 | 454 | r = sd_bus_message_exit_container(msg); 455 | if (r < 0) 456 | error(EXIT_FAILURE, -r, "Error parsing bluez results"); 457 | } 458 | if (r < 0) 459 | error(EXIT_FAILURE, -r, "Error parsing bluez results"); 460 | 461 | r = sd_bus_message_exit_container(msg); 462 | if (r < 0) 463 | error(EXIT_FAILURE, -r, "Error parsing bluez results"); 464 | } 465 | 466 | static void 467 | on_signal(int sig) 468 | { 469 | 470 | } 471 | 472 | int 473 | main(int argc, char *argv[]) 474 | { 475 | SCOPED(sd_bus) *bus = NULL; 476 | SCOPED(uinput) i = -1; 477 | int r; 478 | 479 | signal(SIGHUP, on_signal); 480 | signal(SIGINT, on_signal); 481 | signal(SIGPIPE, on_signal); 482 | signal(SIGTERM, on_signal); 483 | signal(SIGUSR1, on_signal); 484 | signal(SIGUSR2, on_signal); 485 | 486 | r = sd_bus_default_system(&bus); 487 | if (r < 0) 488 | error(EXIT_FAILURE, -r, "Error connecting to system bus"); 489 | 490 | setup_uinput(&i); 491 | setup_objects(bus, &i); 492 | setup_registration(bus); 493 | 494 | while ((r = sd_bus_wait(bus, (uint64_t) -1)) >= 0) { 495 | while ((r = sd_bus_process(bus, NULL)) > 0) 496 | continue; 497 | if (r < 0) 498 | error(EXIT_FAILURE, -r, "Error processing bus"); 499 | } 500 | if (r < 0 && r != -EINTR) 501 | error(EXIT_FAILURE, -r, "Error waiting on bus"); 502 | 503 | return EXIT_SUCCESS; 504 | } 505 | -------------------------------------------------------------------------------- /jelling.conf: -------------------------------------------------------------------------------- 1 | uinput 2 | -------------------------------------------------------------------------------- /jelling.service.in: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Jelling BTLE OTP Receiver daemon 3 | Requires=bluetooth.service 4 | After=bluetooth.service 5 | 6 | [Service] 7 | ExecStart=@libexecdir@/jelling 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('jelling', 'c') 2 | 3 | libexecdir = join_paths(get_option('prefix'), get_option('libexecdir')) 4 | 5 | libsystemd = dependency('libsystemd', version: '>=221') 6 | systemd = dependency('systemd', version: '>=221') 7 | bluez = dependency('bluez', version: '>=5.42') 8 | 9 | unitdir = systemd.get_pkgconfig_variable('systemdsystemunitdir') 10 | modsdir = systemd.get_pkgconfig_variable('modulesloaddir') 11 | 12 | config = configuration_data() 13 | config.set('libexecdir', libexecdir) 14 | configure_file( 15 | input: 'jelling.service.in', 16 | output: 'jelling.service', 17 | configuration: config, 18 | install_dir: unitdir 19 | ) 20 | 21 | install_data( 22 | sources: 'jelling.conf', 23 | install_dir: modsdir 24 | ) 25 | 26 | executable( 27 | 'jelling', 28 | 'jelling.c', 29 | install_dir : libexecdir, 30 | dependencies: libsystemd, 31 | install: true, 32 | c_args: [ 33 | '-Wall', 34 | '-Wextra', 35 | '-Werror', 36 | '-Wstrict-aliasing', 37 | '-Wchar-subscripts', 38 | '-Wformat-security', 39 | '-Wmissing-declarations', 40 | '-Wmissing-prototypes', 41 | '-Wnested-externs', 42 | '-Wpointer-arith', 43 | '-Wshadow', 44 | '-Wsign-compare', 45 | '-Wstrict-prototypes', 46 | '-Wtype-limits', 47 | '-Wunused-function', 48 | '-Wno-missing-field-initializers', 49 | '-Wno-unused-parameter', 50 | ] 51 | ) 52 | --------------------------------------------------------------------------------