From 04318ec2ebaf2d5b6639bf9e959299438fcfd8c7 Mon Sep 17 00:00:00 2001 From: qo-op Date: Wed, 6 May 2020 19:42:21 +0200 Subject: [PATCH] sboc add --- .install/sbotc/Makefile | 42 ++ .install/sbotc/README.md | 31 + .install/sbotc/base64.c | 118 ++++ .install/sbotc/base64.h | 6 + .install/sbotc/jsmn.c | 278 +++++++++ .install/sbotc/jsmn.h | 67 ++ .install/sbotc/sbotc | Bin 0 -> 50088 bytes .install/sbotc/sbotc.1 | 190 ++++++ .install/sbotc/sbotc.c | 1270 ++++++++++++++++++++++++++++++++++++++ .install/sbotc/test-shs-inner.sh | 8 + 10 files changed, 2010 insertions(+) create mode 100644 .install/sbotc/Makefile create mode 100644 .install/sbotc/README.md create mode 100644 .install/sbotc/base64.c create mode 100644 .install/sbotc/base64.h create mode 100644 .install/sbotc/jsmn.c create mode 100644 .install/sbotc/jsmn.h create mode 100755 .install/sbotc/sbotc create mode 100644 .install/sbotc/sbotc.1 create mode 100644 .install/sbotc/sbotc.c create mode 100755 .install/sbotc/test-shs-inner.sh diff --git a/.install/sbotc/Makefile b/.install/sbotc/Makefile new file mode 100644 index 0000000..3a26f42 --- /dev/null +++ b/.install/sbotc/Makefile @@ -0,0 +1,42 @@ +BIN = sbotc + +PREFIX = /usr/local +BINDIR = $(PREFIX)/bin +MANDIR = $(PREFIX)/share/man + +CFLAGS = -Wall -Werror -Wextra + +ifdef STATIC + LDLIBS = -l:libsodium.a +else + LDLIBS = -lsodium +endif + +all: $(BIN) + +$(BIN): $(BIN).c base64.c jsmn.c + +install: all + @mkdir -vp $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 + @cp -vf $(BIN) $(DESTDIR)$(BINDIR) + @cp -vf $(BIN).1 $(DESTDIR)$(MANDIR)/man1 + +link: all + @mkdir -vp $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1 + @ln -svf $(shell realpath $(BIN)) $(DESTDIR)$(BINDIR) + @ln -svf $(shell realpath $(BIN).1) $(DESTDIR)$(MANDIR)/man1 + +uninstall: + @rm -vf \ + $(DESTDIR)$(BINDIR)/$(BIN) \ + $(DESTDIR)$(MANDIR)/man1/$(BIN).1 + +test-shs1: + @# %lzzcAZlM21slUIoiH4yd/wgDnXu8raNLvwqjxqrU06k=.sha256 + shs1testclient ./test-shs-inner.sh $(SHS1_TEST_SEED) + +clean: + @rm -vf $(BIN) + +.PHONY: + all install link uninstall test-shs1 clean diff --git a/.install/sbotc/README.md b/.install/sbotc/README.md new file mode 100644 index 0000000..cc3d0dd --- /dev/null +++ b/.install/sbotc/README.md @@ -0,0 +1,31 @@ +# sbotc + +A command-line SSB client in C. Use like the `sbot` command (except for `server`/`start`). + +## Install + +Install the dependency, *sodium*. On Debian: `sudo apt-get install libsodium-dev` + +Compile and install the program: + +```sh +make +sudo make install +``` + +## Compile options + +To build a binary statically linked with libsodium, use `make STATIC=1` + +## Usage + +```sh +sbotc [-j] [-T] [-l] [-r] [-e] + [ -n | [-c ] [-k ] [-K ] ] + [ [-s ] [-p ] [ -4 | -6 ] | [-u ] ] + [ -a | [-t ] [...] ] +``` + +Arguments must be explicitly JSON-encoded. + +For more information, see the manual page `sbotc(1)`. diff --git a/.install/sbotc/base64.c b/.install/sbotc/base64.c new file mode 100644 index 0000000..1ee0395 --- /dev/null +++ b/.install/sbotc/base64.c @@ -0,0 +1,118 @@ +/* + + This code is public domain software. + +*/ + +#include "base64.h" + +#include +#include +#include + + +// single base64 character conversion +// +static int POS(char c) +{ + if (c>='A' && c<='Z') return c - 'A'; + if (c>='a' && c<='z') return c - 'a' + 26; + if (c>='0' && c<='9') return c - '0' + 52; + if (c == '+') return 62; + if (c == '/') return 63; + if (c == '=') return -1; + return -2; +} + +// base64 decoding +// +// s: base64 string +// str_len size of the base64 string +// data: output buffer for decoded data +// data_len expected size of decoded data +// return: 0 on success, -1 on failure +// +int base64_decode(const char* s, size_t str_len, void *data, size_t data_len) +{ + const char *p, *str_end; + unsigned char *q, *end; + int n[4] = { 0, 0, 0, 0 }; + + if (str_len % 4) { errno = EBADMSG; return -1; } + q = (unsigned char*) data; + end = q + data_len; + str_end = s + str_len; + + for (p = s; p < str_end; ) { + n[0] = POS(*p++); + n[1] = POS(*p++); + n[2] = POS(*p++); + n[3] = POS(*p++); + + if (n[0] == -2 || n[1] == -2 || n[2] == -2 || n[3] == -2) + { errno = EBADMSG; return -1; } + + if (n[0] == -1 || n[1] == -1) + { errno = EBADMSG; return -1; } + + if (n[2] == -1 && n[3] != -1) + { errno = EBADMSG; return -1; } + + if (q >= end) { errno = EMSGSIZE; return -1; } + q[0] = (n[0] << 2) + (n[1] >> 4); + if (n[2] != -1) { + if (q+1 >= end) { errno = EMSGSIZE; return -1; } + q[1] = ((n[1] & 15) << 4) + (n[2] >> 2); + } + if (n[3] != -1) { + if (q+2 >= end) { errno = EMSGSIZE; return -1; } + q[2] = ((n[2] & 3) << 6) + n[3]; + } + q += 3; + } + + return 0; +} + +int base64_encode(const void* buf, size_t size, char *str, size_t out_size) { + static const char base64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + char* p = str; + const unsigned char* q = (const unsigned char*) buf; + size_t i = 0; + + if ((size+3)*4/3 + 1 > out_size) { + errno = EMSGSIZE; + return -1; + } + + while (i < size) { + int c = q[i++]; + c *= 256; + if (i < size) + c += q[i]; + i++; + + c *= 256; + if (i < size) + c += q[i]; + i++; + + *p++ = base64[(c & 0x00fc0000) >> 18]; + *p++ = base64[(c & 0x0003f000) >> 12]; + + if (i > size + 1) + *p++ = '='; + else + *p++ = base64[(c & 0x00000fc0) >> 6]; + + if (i > size) + *p++ = '='; + else + *p++ = base64[c & 0x0000003f]; + } + + *p = 0; + + return 0; +} diff --git a/.install/sbotc/base64.h b/.install/sbotc/base64.h new file mode 100644 index 0000000..8ca4652 --- /dev/null +++ b/.install/sbotc/base64.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +int base64_encode(const void* buf, size_t size, char *str, size_t out_size); +int base64_decode(const char *s, size_t str_len, void *data, size_t data_len); diff --git a/.install/sbotc/jsmn.c b/.install/sbotc/jsmn.c new file mode 100644 index 0000000..9fda563 --- /dev/null +++ b/.install/sbotc/jsmn.c @@ -0,0 +1,278 @@ +/** + * jsmn + * Copyright (c) 2010 Serge A. Zaitsev + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include + +#include "jsmn.h" + +/** + * Allocates a fresh unused token from the token pull. + */ +static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser, + jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *tok; + if (parser->toknext >= (int)num_tokens) { + return NULL; + } + tok = &tokens[parser->toknext++]; + tok->start = tok->end = -1; + tok->size = 0; +#ifdef JSMN_PARENT_LINKS + tok->parent = -1; +#endif + return tok; +} + +/** + * Fills token type and boundaries. + */ +static void jsmn_fill_token(jsmntok_t *token, jsmntype_t type, + int start, int end) { + token->type = type; + token->start = start; + token->end = end; + token->size = 0; +} + +/** + * Fills next available token with JSON primitive. + */ +static jsmnerr_t jsmn_parse_primitive(jsmn_parser *parser, const char *js, + jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + int start; + + start = parser->pos; + + for (; js[parser->pos] != '\0'; parser->pos++) { + switch (js[parser->pos]) { +#ifndef JSMN_STRICT + /* In strict mode primitive must be followed by "," or "}" or "]" */ + case ':': +#endif + case '\t' : case '\r' : case '\n' : case ' ' : + case ',' : case ']' : case '}' : + goto found; + } + if (js[parser->pos] < 32 || js[parser->pos] >= 127) { + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } +#ifdef JSMN_STRICT + /* In strict mode primitive must be followed by a comma/object/array */ + parser->pos = start; + return JSMN_ERROR_PART; +#endif + +found: + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + parser->pos--; + return JSMN_SUCCESS; +} + +/** + * Filsl next token with JSON string. + */ +static jsmnerr_t jsmn_parse_string(jsmn_parser *parser, const char *js, + jsmntok_t *tokens, size_t num_tokens) { + jsmntok_t *token; + + int start = parser->pos; + + parser->pos++; + + /* Skip starting quote */ + for (; js[parser->pos] != '\0'; parser->pos++) { + char c = js[parser->pos]; + + /* Quote: end of string */ + if (c == '\"') { + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) { + parser->pos = start; + return JSMN_ERROR_NOMEM; + } + jsmn_fill_token(token, JSMN_STRING, start+1, parser->pos); +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + return JSMN_SUCCESS; + } + + /* Backslash: Quoted symbol expected */ + if (c == '\\') { + parser->pos++; + switch (js[parser->pos]) { + /* Allowed escaped symbols */ + case '\"': case '/' : case '\\' : case 'b' : + case 'f' : case 'r' : case 'n' : case 't' : + break; + /* Allows escaped symbol \uXXXX */ + case 'u': + /* TODO */ + break; + /* Unexpected symbol */ + default: + parser->pos = start; + return JSMN_ERROR_INVAL; + } + } + } + parser->pos = start; + return JSMN_ERROR_PART; +} + +/** + * Parse JSON string and fill tokens. + */ +jsmnerr_t jsmn_parse(jsmn_parser *parser, const char *js, jsmntok_t *tokens, + unsigned int num_tokens) { + jsmnerr_t r; + int i; + jsmntok_t *token; + + for (; js[parser->pos] != '\0'; parser->pos++) { + char c; + jsmntype_t type; + + c = js[parser->pos]; + switch (c) { + case '{': case '[': + token = jsmn_alloc_token(parser, tokens, num_tokens); + if (token == NULL) + return JSMN_ERROR_NOMEM; + if (parser->toksuper != -1) { + tokens[parser->toksuper].size++; +#ifdef JSMN_PARENT_LINKS + token->parent = parser->toksuper; +#endif + } + token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY); + token->start = parser->pos; + parser->toksuper = parser->toknext - 1; + break; + case '}': case ']': + type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY); +#ifdef JSMN_PARENT_LINKS + if (parser->toknext < 1) { + return JSMN_ERROR_INVAL; + } + token = &tokens[parser->toknext - 1]; + for (;;) { + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + token->end = parser->pos + 1; + parser->toksuper = token->parent; + break; + } + if (token->parent == -1) { + break; + } + token = &tokens[token->parent]; + } +#else + for (i = parser->toknext - 1; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + if (token->type != type) { + return JSMN_ERROR_INVAL; + } + parser->toksuper = -1; + token->end = parser->pos + 1; + break; + } + } + /* Error if unmatched closing bracket */ + if (i == -1) return JSMN_ERROR_INVAL; + for (; i >= 0; i--) { + token = &tokens[i]; + if (token->start != -1 && token->end == -1) { + parser->toksuper = i; + break; + } + } +#endif + break; + case '\"': + r = jsmn_parse_string(parser, js, tokens, num_tokens); + if (r < 0) return r; + if (parser->toksuper != -1) + tokens[parser->toksuper].size++; + break; + case '\t' : case '\r' : case '\n' : case ':' : case ',': case ' ': + break; +#ifdef JSMN_STRICT + /* In strict mode primitives are: numbers and booleans */ + case '-': case '0': case '1' : case '2': case '3' : case '4': + case '5': case '6': case '7' : case '8': case '9': + case 't': case 'f': case 'n' : +#else + /* In non-strict mode every unquoted value is a primitive */ + default: +#endif + r = jsmn_parse_primitive(parser, js, tokens, num_tokens); + if (r < 0) return r; + if (parser->toksuper != -1) + tokens[parser->toksuper].size++; + break; + +#ifdef JSMN_STRICT + /* Unexpected char in strict mode */ + default: + return JSMN_ERROR_INVAL; +#endif + + } + } + + for (i = parser->toknext - 1; i >= 0; i--) { + /* Unmatched opened object or array */ + if (tokens[i].start != -1 && tokens[i].end == -1) { + return JSMN_ERROR_PART; + } + } + + return JSMN_SUCCESS; +} + +/** + * Creates a new parser based over a given buffer with an array of tokens + * available. + */ +void jsmn_init(jsmn_parser *parser) { + parser->pos = 0; + parser->toknext = 0; + parser->toksuper = -1; +} + diff --git a/.install/sbotc/jsmn.h b/.install/sbotc/jsmn.h new file mode 100644 index 0000000..03b2c1a --- /dev/null +++ b/.install/sbotc/jsmn.h @@ -0,0 +1,67 @@ +#ifndef __JSMN_H_ +#define __JSMN_H_ + +/** + * JSON type identifier. Basic types are: + * o Object + * o Array + * o String + * o Other primitive: number, boolean (true/false) or null + */ +typedef enum { + JSMN_PRIMITIVE = 0, + JSMN_OBJECT = 1, + JSMN_ARRAY = 2, + JSMN_STRING = 3 +} jsmntype_t; + +typedef enum { + /* Not enough tokens were provided */ + JSMN_ERROR_NOMEM = -1, + /* Invalid character inside JSON string */ + JSMN_ERROR_INVAL = -2, + /* The string is not a full JSON packet, more bytes expected */ + JSMN_ERROR_PART = -3, + /* Everything was fine */ + JSMN_SUCCESS = 0 +} jsmnerr_t; + +/** + * JSON token description. + * @param type type (object, array, string etc.) + * @param start start position in JSON data string + * @param end end position in JSON data string + */ +typedef struct { + jsmntype_t type; + int start; + int end; + int size; +#ifdef JSMN_PARENT_LINKS + int parent; +#endif +} jsmntok_t; + +/** + * JSON parser. Contains an array of token blocks available. Also stores + * the string being parsed now and current position in that string + */ +typedef struct { + unsigned int pos; /* offset in the JSON string */ + int toknext; /* next token to allocate */ + int toksuper; /* superior token node, e.g parent object or array */ +} jsmn_parser; + +/** + * Create JSON parser over an array of tokens + */ +void jsmn_init(jsmn_parser *parser); + +/** + * Run JSON parser. It parses a JSON data string into and array of tokens, each describing + * a single JSON object. + */ +jsmnerr_t jsmn_parse(jsmn_parser *parser, const char *js, + jsmntok_t *tokens, unsigned int num_tokens); + +#endif /* __JSMN_H_ */ diff --git a/.install/sbotc/sbotc b/.install/sbotc/sbotc new file mode 100755 index 0000000000000000000000000000000000000000..82c420db9cd444c90d19aba88ebc51c303bfd755 GIT binary patch literal 50088 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 literal 0 HcmV?d00001 diff --git a/.install/sbotc/sbotc.1 b/.install/sbotc/sbotc.1 new file mode 100644 index 0000000..0a741fc --- /dev/null +++ b/.install/sbotc/sbotc.1 @@ -0,0 +1,190 @@ +.Dd 2017-06-03 +.Dt SBOTC 1 +.Os SSBC +.ds REPO ssb://%133ulDgs/oC1DXjoK04vDFy6DgVBB/Zok15YJmuhD5Q=.sha256 +.Sh NAME +.Nm sbotc +.Nd Call a scuttlebot/secret-stack RPC method +.Sh SYNOPSIS +.Nm +.Op Fl j +.Op Fl l +.Op Fl r +.Op Fl T +.Op Fl e +.Op Fl a +. +.Oo +.Fl n +| +.Op Fl c Ar cap +.Op Fl k Ar key +.Op Fl K Ar keypair +.Oc +. +.Oo +.Op Fl s Ar host +.Op Fl p Ar port +.Oo +.Fl 4 +| +.Fl 6 +.Oc +| +.Op Fl u Ar socket_path +.Oc +. +.Oo +.Fl a +| +.Op Fl t Ar type +.Ar method +.Op Ar argument ... +.Oc +.Sh DESCRIPTION +Connect to a scuttlebot/secret-stack server, and call a method on it, with +standard I/O. +.Sh OPTIONS +.Bl -tag +.It Fl j +Send stdin data as JSON. +.It Fl l +Don't output newlines after string or JSON packets. +.It Fl r +Raw mode. Disables stdin line buffering/editing and echoing. Implies +.Fl l . +.It Fl e +Encode arguments as strings, rather than expecting them to be JSON-encoded. +.It Fl T +Test using shs1-testsuite protocol. Instead of connecting to a server and running +a command, connect to stdio. On successful handshake, output concatenation of +the encryption key, encryption nonce, decryption key and decryption nonce. +.It Fl a +Passthrough mode. Instead of making a muxrpc call, pass through the box-stream +to stdio. +.It Fl n +Noauth mode. Skip secret-handshake authentication and box-stream encryption. +This option makes the +.Fl k , +.Fl K , +and +.Fl c +options have no effect and output a warning if used. +.It Fl 4 +Connect to server over IPv4 only. +.It Fl 6 +Connect to server over IPv6 only. +.It Fl c Ar cap +Capability key for secret-handshake. Default is SSB's capability key, +.Li 1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s= . +.It Fl s Ar host +The hostname to connect to. Default is localhost. If set to localhost and connection to localhost fails, +.Nm +may attempt to connect to other local interface addresses. +.It Fl p Ar port +The port to connect to. Default is 8008. +.It Fl u Ar socket_path +Unix socket path to connect to, instead of TCP socket. Conflicts with +.Fl p +and +.Fl s . +.It Fl k Ar key +The key to connect to. Default is your public key, as read from your +private key file. +.It Fl K Ar keypair +Private key or private key seed to use for secret-handshake. Default is to use the private key +from your +.Pa ~/.ssb/secret +file or other secret file according to the environmental variables described in +.Sx ENVIRONMENT . +.It Fl t Ar type +The type of method: +.Dq async , +.Dq source , +.Dq sink , +or +.Dq duplex . +Default is to look up the method in +.Pa ~/.ssb/manifest.json . +.It Ar method +Method name. +.It Op Ar argument ... +Arguments to pass to the method call. Each argument must be JSON-encoded, unless the +.Fl e +option is used, in which the arguments are treated as strings. +.El +.Sh ENVIRONMENT +.Bl -tag +.It Ev ssb_appname +Name of the app. Default is +.Dq ssb . +Used to construct the app's directory if +.Ev ssb_path +is not present. +.It Ev ssb_path +Path to the app's directory. Default is to use +.Ev ssb_appname to construct the path as +.Dq ~/. +.El +.Sh FILES +.Bl -tag -width -indent +.It Pa ~/.ssb/secret +Your private key, used for authenticating to the server with the +secret-handshake protocol. +.It Pa ~/.ssb/manifest.json +A map of method names to method types. +.It Pa ~/.ssb/config +JSON file containing key, host, port, and/or SHS cap key to use if the +.Fl s , +.Fl p +or +.Fl c +options are not given, respectively. +.It Pa ~/.ssb/socket +UNIX socket stream file for noauth connections. +If none of the options +.Fl s , +.Fl p , +.Fl u , +.Fl c , +.Fl k , +.Fl K , +.Fl c , +.Fl 4 , +.Fl 6 , +or +.Fl T +are specified, +.Nm +will attempt to connect in noauth mode to this socket file. If the socket file +is not present or the connection fails, +.Nm +will fall back to connecting with TCP and secret-handshake according to the +config file - unless the +.Fl n +option is specified, in which case the command will fail. +.El +.Pp +The base path +.Dq ~/.ssb/ +of these file names may be changed by setting +.Ev ssb_appname +or +.Ev ssb_path . +.Sh EXIT STATUS +.Bl -tag -width Ds +.It 0 +The command completed successfully. +.It 1 +An error occurred. +.It 2 +The command completed with an error. +.El +.Sh AUTHORS +.Nm +was written by +.An cel Aq @f/6sQ6d2CMxRUhLpspgGIulDxDCwYD7DzFzPNr7u5AU=.ed25519 . +.Sh BUGS +.Pp +Please report any bugs by making a post on SSB mentioning the repo, +.Lk \*[REPO] diff --git a/.install/sbotc/sbotc.c b/.install/sbotc/sbotc.c new file mode 100644 index 0000000..8972a38 --- /dev/null +++ b/.install/sbotc/sbotc.c @@ -0,0 +1,1270 @@ +/* + * sbotc.c + * Copyright (c) 2017 Secure Scuttlebutt Consortium + * + * Usage of the works is permitted provided that this instrument is + * retained with the works, so that any entity that uses the works is + * notified of this instrument. + * + * DISCLAIMER: THE WORKS ARE WITHOUT WARRANTY. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "base64.h" +#include "jsmn.h" + +#define BOXS_MAXLEN 4096 + +#define write_buf(fd, buf) \ + write_all(fd, buf, sizeof(buf)-1) + +struct boxs_header { + uint16_t len; + uint8_t mac[16]; +}; + +struct boxs { + int s; + unsigned char encrypt_key[32]; + unsigned char decrypt_key[32]; + unsigned char nonce1[24]; + unsigned char nonce2[24]; + unsigned char rx_nonce[24]; + unsigned char rx_buf[BOXS_MAXLEN]; + size_t rx_buf_pos; + size_t rx_buf_len; + bool noauth; + bool wrote_goodbye; +}; + +enum pkt_type { + pkt_type_buffer = 0, + pkt_type_string = 1, + pkt_type_json = 2, +}; + +enum pkt_flags { + pkt_flags_buffer = 0, + pkt_flags_string = 1, + pkt_flags_json = 2, + pkt_flags_end = 4, + pkt_flags_stream = 8, +}; + +struct pkt_header { + uint32_t len; + int32_t req; +} __attribute__((packed)); + +enum muxrpc_type { + muxrpc_type_async, + muxrpc_type_source, + muxrpc_type_sink, + muxrpc_type_duplex, +}; + +enum stream_state { + stream_state_open, + stream_state_ended_ok, + stream_state_ended_error, +}; + +enum ip_family { + ip_family_ipv4 = AF_INET, + ip_family_ipv6 = AF_INET6, + ip_family_any = AF_UNSPEC +}; + +static unsigned char zeros[24] = {0}; + +static const unsigned char ssb_cap[] = { + 0xd4, 0xa1, 0xcb, 0x88, 0xa6, 0x6f, 0x02, 0xf8, + 0xdb, 0x63, 0x5c, 0xe2, 0x64, 0x41, 0xcc, 0x5d, + 0xac, 0x1b, 0x08, 0x42, 0x0c, 0xea, 0xac, 0x23, + 0x08, 0x39, 0xb7, 0x55, 0x84, 0x5a, 0x9f, 0xfb +}; + +struct termios orig_tc; + +static void reset_termios() { + int rc = tcsetattr(STDIN_FILENO, TCSANOW, &orig_tc); + if (rc < 0) warn("tcsetattr"); +} + +static void usage() { + fputs("usage: sbotc [-j] [-T] [-l] [-r] [-e]\n" + " [ -n | [-c ] [-k ] [-K ] ]\n" + " [ [-s ] [-p ] [ -4 | -6 ] | [-u ] ]\n" + " [ -a | [-t ] [...] ]\n", stderr); + exit(EXIT_FAILURE); +} + +static int connect_localhost(const char *port, enum ip_family ip_family) { + int rc, family, fd, err; + struct ifaddrs *ifaddr, *ifa; + rc = getifaddrs(&ifaddr); + if (rc < 0) return -1; + int port_n = htons(atoi(port)); + + for (ifa = ifaddr; ifa != NULL; ifa = ifa->ifa_next) { + if (ifa->ifa_addr == NULL) continue; + family = ifa->ifa_addr->sa_family; + socklen_t addrlen; + if (family == AF_INET) { + if (ip_family != ip_family_ipv4 && ip_family != ip_family_any) continue; + addrlen = sizeof(struct sockaddr_in); + struct sockaddr_in *addr = (struct sockaddr_in *)ifa->ifa_addr; + addr->sin_port = port_n; + } else if (family == AF_INET6) { + if (ip_family != ip_family_ipv6 && ip_family != ip_family_any) continue; + addrlen = sizeof(struct sockaddr_in6); + struct sockaddr_in6 *addr = (struct sockaddr_in6 *)ifa->ifa_addr; + addr->sin6_port = port_n; + } else continue; + fd = socket(family, SOCK_STREAM, IPPROTO_TCP); + if (fd < 0) continue; + if (connect(fd, ifa->ifa_addr, addrlen) == 0) break; + err = errno; + close(fd); + errno = err; + } + if (ifa == NULL) fd = -1; + + freeifaddrs(ifaddr); + return fd; +} + +static int tcp_connect(const char *host, const char *port, enum ip_family ip_family) { + struct addrinfo hints; + struct addrinfo *result, *rp; + int s; + int fd; + int err; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = ip_family; + hints.ai_protocol = IPPROTO_TCP; + + s = getaddrinfo(host, port, &hints, &result); + if (s < 0) errx(1, "unable to resolve host: %s", gai_strerror(s)); + + for (rp = result; rp; rp = rp->ai_next) { + fd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); + if (fd < 0) continue; + if (connect(fd, rp->ai_addr, rp->ai_addrlen) == 0) break; + err = errno; + close(fd); + errno = err; + } + if (rp == NULL) fd = -1; + + freeaddrinfo(result); + + if (fd == -1 && errno == ECONNREFUSED && (host == NULL || !strcmp(host, "localhost"))) { + return connect_localhost(port, ip_family); + } + + return fd; +} + +static int unix_connect(const char *path) { + struct sockaddr_un name; + const size_t path_len = strlen(path); + int s, rc; + if (path_len >= sizeof(name.sun_path)-1) errx(1, "socket path too long"); + s = socket(AF_UNIX, SOCK_STREAM, 0); + if (s < 0) return -1; + memset(&name, 0, sizeof(struct sockaddr_un)); + name.sun_family = AF_UNIX; + strncpy(name.sun_path, path, sizeof(name.sun_path) - 1); + rc = connect(s, (const struct sockaddr *)&name, sizeof name); + if (rc < 0) return -1; + return s; +} + +static int get_socket_path(char *buf, size_t len, const char *app_dir) { + struct stat st; + int sz = snprintf(buf, len-1, "%s/%s", app_dir, "socket"); + if (sz < 0 || sz >= (int)len-1) err(1, "failed to get socket path"); + int rc = stat(buf, &st); + if (rc < 0) return -1; + if (!(st.st_mode & S_IFSOCK)) { errno = EINVAL; return -1; } + return 0; +} + +static int read_all(int fd, void *buf, size_t count) { + ssize_t nbytes; + while (count > 0) { + nbytes = read(fd, buf, count); + if (nbytes == 0) { errno = EPIPE; return -1; } + if (nbytes < 0 && errno == EINTR) continue; + if (nbytes < 0) return -1; + buf += nbytes; + count -= nbytes; + } + return 0; +} + +static int read_some(int fd, unsigned char *buf, size_t *lenp) { + ssize_t nbytes; + do nbytes = read(fd, buf, *lenp); + while (nbytes < 0 && errno == EINTR); + if (nbytes == 0) { errno = EPIPE; return -1; } + if (nbytes < 0) return -1; + *lenp = nbytes; + return 0; +} + +static int write_all(int fd, const void *buf, size_t count) { + ssize_t nbytes; + while (count > 0) { + nbytes = write(fd, buf, count); + if (nbytes < 0 && errno == EINTR) continue; + if (nbytes < 0) return -1; + buf += nbytes; + count -= nbytes; + } + return 0; +} + +static void shs_connect(int sfd, int infd, int outfd, const unsigned char pubkey[32], const unsigned char seckey[64], const unsigned char appkey[32], const unsigned char server_pubkey[32], struct boxs *bs) { + int rc; + unsigned char local_app_mac[32], remote_app_mac[32]; + + unsigned char kx_pk[32], kx_sk[32]; + rc = crypto_box_keypair(kx_pk, kx_sk); + if (rc < 0) errx(1, "failed to generate auth keypair"); + + rc = crypto_auth(local_app_mac, kx_pk, 32, appkey); + if (rc < 0) err(1, "failed to generate app mac"); + + // send challenge + unsigned char buf[64]; + memcpy(buf, local_app_mac, 32); + memcpy(buf+32, kx_pk, 32); + rc = write_all(outfd, buf, sizeof(buf)); + if (rc < 0) err(1, "failed to send challenge"); + + // recv challenge + unsigned char remote_kx_pk[32]; + rc = read_all(infd, buf, sizeof(buf)); + if (rc < 0) err(1, "challenge not accepted"); + memcpy(remote_app_mac, buf, 32); + memcpy(remote_kx_pk, buf+32, 32); + rc = crypto_auth_verify(buf, remote_kx_pk, 32, appkey); + if (rc < 0) errx(1, "wrong protocol (version?)"); + + // send auth + + unsigned char secret[32]; + rc = crypto_scalarmult(secret, kx_sk, remote_kx_pk); + if (rc < 0) errx(1, "failed to derive shared secret"); + + unsigned char remote_pk_curve[32]; + rc = crypto_sign_ed25519_pk_to_curve25519(remote_pk_curve, server_pubkey); + if (rc < 0) errx(1, "failed to curvify remote public key"); + + unsigned char a_bob[32]; + rc = crypto_scalarmult(a_bob, kx_sk, remote_pk_curve); + if (rc < 0) errx(1, "failed to derive a_bob"); + + unsigned char secret2a[96]; + memcpy(secret2a, appkey, 32); + memcpy(secret2a+32, secret, 32); + memcpy(secret2a+64, a_bob, 32); + + unsigned char secret2[32]; + rc = crypto_hash_sha256(secret2, secret2a, sizeof(secret2a)); + if (rc < 0) errx(1, "failed to hash secret2"); + + unsigned char shash[32]; + rc = crypto_hash_sha256(shash, secret, sizeof(secret)); + if (rc < 0) errx(1, "failed to hash secret"); + + unsigned char signed1[96]; + memcpy(signed1, appkey, 32); + memcpy(signed1+32, server_pubkey, 32); + memcpy(signed1+64, shash, 32); + + unsigned char sig[64]; + rc = crypto_sign_detached(sig, NULL, signed1, sizeof(signed1), seckey); + if (rc < 0) errx(1, "failed to sign inner hello"); + + unsigned char hello[96]; + memcpy(hello, sig, 64); + memcpy(hello+64, pubkey, 32); + + unsigned char boxed_auth[112]; + rc = crypto_secretbox_easy(boxed_auth, hello, sizeof(hello), zeros, secret2); + if (rc < 0) errx(1, "failed to box hello"); + + rc = write_all(outfd, boxed_auth, sizeof(boxed_auth)); + if (rc < 0) errx(1, "failed to send auth"); + + // verify accept + + unsigned char boxed_okay[80]; + rc = read_all(infd, boxed_okay, sizeof(boxed_okay)); + if (rc < 0) err(1, "hello not accepted"); + + unsigned char local_sk_curve[32]; + rc = crypto_sign_ed25519_sk_to_curve25519(local_sk_curve, seckey); + if (rc < 0) errx(1, "failed to curvify local secret key"); + + unsigned char b_alice[32]; + rc = crypto_scalarmult(b_alice, local_sk_curve, remote_kx_pk); + if (rc < 0) errx(1, "failed to derive b_alice"); + + unsigned char secret3a[128]; + memcpy(secret3a, appkey, 32); + memcpy(secret3a+32, secret, 32); + memcpy(secret3a+64, a_bob, 32); + memcpy(secret3a+96, b_alice, 32); + + unsigned char secret3[32]; + rc = crypto_hash_sha256(secret3, secret3a, sizeof(secret3a)); + if (rc < 0) errx(1, "failed to hash secret3"); + + rc = crypto_secretbox_open_easy(sig, boxed_okay, sizeof(boxed_okay), zeros, secret3); + if (rc < 0) errx(1, "failed to unbox the okay"); + + unsigned char signed2[160]; + memcpy(signed2, appkey, 32); + memcpy(signed2+32, hello, 96); + memcpy(signed2+128, shash, 32); + + rc = crypto_sign_verify_detached(sig, signed2, sizeof(signed2), server_pubkey); + if (rc < 0) errx(1, "server not authenticated"); + + rc = crypto_hash_sha256(secret, secret3, 32); + if (rc < 0) errx(1, "failed to hash secret3"); + + unsigned char enc_key_hashed[64]; + memcpy(enc_key_hashed, secret, 32); + memcpy(enc_key_hashed+32, server_pubkey, 32); + rc = crypto_hash_sha256(bs->encrypt_key, enc_key_hashed, 64); + if (rc < 0) errx(1, "failed to hash the encrypt key"); + + unsigned char dec_key_hashed[64]; + memcpy(dec_key_hashed, secret, 32); + memcpy(dec_key_hashed+32, pubkey, 32); + rc = crypto_hash_sha256(bs->decrypt_key, dec_key_hashed, 64); + if (rc < 0) errx(1, "failed to hash the decrypt key"); + + memcpy(bs->nonce1, remote_app_mac, 24); + memcpy(bs->nonce2, remote_app_mac, 24); + memcpy(bs->rx_nonce, local_app_mac, 24); + + bs->rx_buf_pos = 0; + bs->rx_buf_len = 0; + bs->s = sfd; + bs->noauth = false; + bs->wrote_goodbye = false; +} + +static int pubkey_decode(const char *key_str, unsigned char key[32]) { + if (!key_str) { errno = EPROTO; return -1; } + if (!*key_str) { errno = EPROTO; return -1; } + if (*key_str == '@') key_str++; + size_t len = strlen(key_str); + if (len == 52 && strcmp(key_str+44, ".ed25519") == 0) {} + else if (len != 44) { errno = EMSGSIZE; return -1; } + return base64_decode(key_str, 44, key, 32); +} + +static int seckey_decode(const char *key_str, unsigned char key[64]) { + if (!key_str) { errno = EPROTO; return -1; } + if (!*key_str) { errno = EPROTO; return -1; } + if (*key_str == '@') key_str++; + size_t len = strlen(key_str); + if (len > 8 && memcmp(key_str + len - 8, ".ed25519", 8) == 0) len -= 8; + return base64_decode(key_str, len, key, 64); +} + +static jsmntok_t *json_lookup(const char *buf, jsmntok_t *tok, const char *prop, size_t prop_len) { + jsmntok_t *end = tok + tok->size + 1; + if (tok->type != JSMN_OBJECT) { errno = EPROTO; return NULL; } + tok++; + while (tok < end) { + if (tok + 1 >= end) { errno = EPROTO; return NULL; } + if (tok->type == JSMN_STRING + && tok->end - tok->start == (int)prop_len + && memcmp(buf + tok->start, prop, prop_len) == 0) + return tok + 1; + tok += tok->size + 1; + end += tok->size; + } + return NULL; +} + +static ssize_t json_get_value(const char *buf, const char *path, const char **value) { + static const int num_tokens = 1024; + jsmntok_t tokens[num_tokens], *tok = tokens; + jsmn_parser parser; + + jsmn_init(&parser); + switch (jsmn_parse(&parser, buf, tokens, num_tokens)) { + case JSMN_ERROR_NOMEM: errno = ENOMEM; return -1; + case JSMN_ERROR_INVAL: errno = EINVAL; return -1; + case JSMN_ERROR_PART: errno = EMSGSIZE; return -1; + case JSMN_SUCCESS: break; + default: errno = EPROTO; return -1; + } + + while (*path) { + const char *end = strchr(path, '.'); + size_t part_len = end ? (size_t)end - (size_t)path : strlen(path); + tok = json_lookup(buf, tok, path, part_len); + if (!tok) { errno = ENOMSG; return -1; } + path += part_len; + if (*path == '.') path++; + } + + *value = buf + tok->start; + return tok->end - tok->start; +} + +static void get_app_dir(char *dir, size_t len) { + const char *path, *home, *appname; + int rc; + path = getenv("ssb_path"); + if (path) { + if (strlen(path) > len) errx(1, "ssb_path too long"); + strncpy(dir, path, len); + return; + } + home = getenv("HOME"); + if (!home) home = "."; + appname = getenv("ssb_appname"); + if (!appname) appname = "ssb"; + rc = snprintf(dir, len, "%s/.%s", home, appname); + if (rc < 0) err(1, "failed to get app dir"); + if ((size_t)rc >= len) errx(1, "path to app dir too long"); +} + +static ssize_t read_file(char *buf, size_t len, const char *fmt, ...) { + va_list ap; + int rc; + struct stat st; + int fd; + + va_start(ap, fmt); + rc = vsnprintf(buf, len, fmt, ap); + va_end(ap); + if (rc < 0) return -1; + if ((size_t)rc >= len) { errno = ENAMETOOLONG; return -1; } + + rc = stat(buf, &st); + if (rc < 0) return -1; + if (st.st_size > (off_t)(len-1)) { errno = EMSGSIZE; return -1; } + + fd = open(buf, O_RDONLY); + if (fd < 0) return -1; + + rc = read_all(fd, buf, st.st_size); + if (rc < 0) return -1; + buf[st.st_size] = '\0'; + + close(fd); + return st.st_size; +} + + +static void read_private_key(const char *dir, unsigned char pk[64]) { + ssize_t len; + char buf[8192]; + const char *pk_b64; + int rc; + ssize_t key_len; + char *line; + + len = read_file(buf, sizeof(buf), "%s/secret", dir); + if (len < 0) err(1, "failed to read secret file"); + + // strip comments + for (line = buf; *line; ) { + if (*line == '#') while (*line && *line != '\n') *line++ = ' '; + else while (*line && *line++ != '\n'); + } + + key_len = json_get_value(buf, "private", &pk_b64); + if (key_len < 0) err(1, "unable to read private key"); + + if (key_len > 8 && memcmp(pk_b64 + key_len - 8, ".ed25519", 8) == 0) + key_len -= 8; + rc = base64_decode(pk_b64, key_len, pk, 64); + if (rc < 0) err(1, "unable to decode private key"); +} + +static void increment_nonce(uint8_t nonce[24]) { + int i; + for (i = 23; i >= 0 && nonce[i] == 0xff; i--) nonce[i] = 0; + if (i >= 0) nonce[i]++; +} + +static void bs_write_end_box(struct boxs *bs) { + unsigned char boxed[34]; + int rc = crypto_secretbox_easy(boxed, zeros, 18, bs->nonce1, bs->encrypt_key); + if (rc < 0) errx(1, "failed to box packet end header"); + increment_nonce(bs->nonce1); + increment_nonce(bs->nonce2); + rc = write_all(bs->s, boxed, 34); + if (rc < 0) err(1, "failed to write boxed end header"); +} + +static void bs_write_packet(struct boxs *bs, const unsigned char *buf, uint16_t len) { + size_t boxed_len = len + 34; + unsigned char boxed[boxed_len]; + increment_nonce(bs->nonce2); + int rc = crypto_secretbox_easy(boxed + 18, buf, len, bs->nonce2, bs->encrypt_key); + if (rc < 0) errx(1, "failed to box packet data"); + struct boxs_header header; + header.len = htons(len); + memcpy(header.mac, boxed + 18, 16); + rc = crypto_secretbox_easy(boxed, (unsigned char *)&header, 18, bs->nonce1, bs->encrypt_key); + if (rc < 0) errx(1, "failed to box packet header"); + increment_nonce(bs->nonce1); + increment_nonce(bs->nonce1); + increment_nonce(bs->nonce2); + rc = write_all(bs->s, boxed, boxed_len); + if (rc < 0) err(1, "failed to write boxed packet"); +} + +static void bs_end(struct boxs *bs) { + if (bs->wrote_goodbye) return; + bs->wrote_goodbye = true; + if (!bs->noauth) { + bs_write_end_box(bs); + } + shutdown(bs->s, SHUT_WR); +} + +static int bs_read_packet(struct boxs *bs, void *buf, size_t *lenp) { + int rc; + if (bs->noauth) { + rc = read_some(bs->s, buf, lenp); + if (rc < 0 && errno == EPIPE) return -1; + if (rc < 0) err(1, "failed to read packet data"); + return 0; + } + unsigned char boxed_header[34]; + struct boxs_header header; + rc = read_all(bs->s, boxed_header, 34); + if (rc < 0 && errno == EPIPE) errx(1, "unexpected end of parent stream"); + if (rc < 0) err(1, "failed to read boxed packet header"); + rc = crypto_secretbox_open_easy((unsigned char *)&header, boxed_header, 34, bs->rx_nonce, bs->decrypt_key); + if (rc < 0) errx(1, "failed to unbox packet header"); + increment_nonce(bs->rx_nonce); + if (header.len == 0 && !memcmp(header.mac, zeros, 16)) { errno = EPIPE; return -1; } + size_t len = ntohs(header.len); + if (len > BOXS_MAXLEN) errx(1, "received boxed packet too large"); + unsigned char boxed_data[len + 16]; + rc = read_all(bs->s, boxed_data + 16, len); + if (rc < 0) err(1, "failed to read boxed packet data"); + memcpy(boxed_data, header.mac, 16); + rc = crypto_secretbox_open_easy(buf, boxed_data, len+16, bs->rx_nonce, bs->decrypt_key); + if (rc < 0) errx(1, "failed to unbox packet data"); + increment_nonce(bs->rx_nonce); + *lenp = len; + return 0; +} + +static int bs_read(struct boxs *bs, char *buf, size_t len) { + if (bs->noauth) { + int rc = read_all(bs->s, buf, len); + if (rc < 0) err(1, "failed to read packet data"); + return 0; + } + size_t remaining; + while (len > 0) { + remaining = bs->rx_buf_len > len ? len : bs->rx_buf_len; + if (buf) memcpy(buf, bs->rx_buf + bs->rx_buf_pos, remaining); + bs->rx_buf_len -= remaining; + bs->rx_buf_pos += remaining; + len -= remaining; + buf += remaining; + if (len == 0) return 0; + if (bs_read_packet(bs, bs->rx_buf, &bs->rx_buf_len) < 0) return -1; + bs->rx_buf_pos = 0; + } + return 0; +} + +static enum stream_state bs_read_out_1(struct boxs *bs, int fd) { + size_t buf[4096]; + size_t len = sizeof(buf); + int rc; + rc = bs_read_packet(bs, buf, &len); + if (rc < 0 && errno == EPIPE) { + bs_end(bs); + return stream_state_ended_ok; + } + if (rc < 0) return stream_state_ended_error; + rc = write_all(fd, buf, len); + if (rc < 0) return stream_state_ended_error; + return stream_state_open; +} + +static int bs_read_out(struct boxs *bs, int fd, size_t len) { + size_t chunk; + char buf[4096]; + int rc; + while (len > 0) { + chunk = len > sizeof(buf) ? sizeof(buf) : len; + rc = bs_read(bs, buf, chunk); + if (rc < 0) return -1; + rc = write_all(fd, buf, chunk); + if (rc < 0) return -1; + len -= chunk; + } + return 0; +} + +static int bs_read_error(struct boxs *bs, int errfd, enum pkt_flags flags, size_t len, bool no_newline) { + // suppress printing "true" indicating end without error + if (flags & pkt_flags_json && len == 4) { + char buf[4]; + if (bs_read(bs, buf, 4) < 0) return -1; + if (strncmp(buf, "true", 4) == 0) { + return 0; + } + if (write_all(errfd, buf, 4) < 0) return -1; + } else { + if (bs_read_out(bs, errfd, len) < 0) return -1; + } + if (flags & (pkt_flags_json | pkt_flags_string) && !no_newline) { + if (write_buf(errfd, "\n") < 0) return -1; + } + return 1; +} + +static void bs_write(struct boxs *bs, const unsigned char *buf, size_t len) { + if (bs->noauth) { + int rc = write_all(bs->s, buf, len); + if (rc < 0) err(1, "failed to write packet"); + return; + } + while (len > 0) { + size_t l = len > BOXS_MAXLEN ? BOXS_MAXLEN : len; + bs_write_packet(bs, buf, l); + len -= l; + buf += l; + } +} + +static enum stream_state bs_write_in_1(struct boxs *bs, int fd) { + unsigned char buf[4096]; + ssize_t sz = read(fd, buf, sizeof(buf)); + if (sz < 0) err(1, "read"); + if (sz == 0) { + bs_end(bs); + return stream_state_ended_ok; + } + bs_write(bs, buf, sz); + return stream_state_open; +} + +static void ps_write(struct boxs *bs, const char *data, size_t len, enum pkt_type type, int req_id, bool stream, bool end) { + size_t out_len = 9 + len; + unsigned char out_buf[out_len]; + struct pkt_header header = {htonl(len), htonl(req_id)}; + out_buf[0] = (stream << 3) | (end << 2) | (type & 3); + memcpy(out_buf+1, &header, 8); + memcpy(out_buf+9, data, len); + bs_write(bs, out_buf, out_len); +} + +static void ps_goodbye(struct boxs *bs) { + if (bs->wrote_goodbye) return; + bs->wrote_goodbye = true; + bs_write(bs, zeros, 9); +} + +static int ps_read_header(struct boxs *bs, size_t *len, int *req_id, enum pkt_flags *flags) { + char buf[9]; + struct pkt_header header; + if (bs_read(bs, buf, sizeof(buf)) < 0) return -1; + memcpy(&header, buf+1, 8); + if (len) *len = ntohl(header.len); + if (req_id) *req_id = ntohl(header.req); + if (flags) *flags = buf[0]; + return 0; +} + +static void muxrpc_call(struct boxs *bs, const char *method, const char *argument, enum muxrpc_type type, const char *typestr, int req_id) { + char req[33792]; // 32768 max message value size + 1024 extra + ssize_t reqlen; + bool is_request = type == muxrpc_type_async; + + if (is_request) { + reqlen = snprintf(req, sizeof(req), + "{\"name\":%s,\"args\":%s}", + method, argument); + } else { + reqlen = snprintf(req, sizeof(req), + "{\"name\":%s,\"args\":%s,\"type\":\"%s\"}", + method, argument, typestr); + } + if (reqlen < 0) err(1, "failed to construct request"); + if ((size_t)reqlen >= sizeof(req)) errx(1, "request too large"); + + ps_write(bs, req, reqlen, pkt_type_json, req_id, !is_request, false); +} + +static int bs_passthrough(struct boxs *bs, int infd, int outfd) { + int rc; + fd_set rd; + int sfd = bs->s; + int maxfd = infd > sfd ? infd : sfd; + enum stream_state in = stream_state_open; + enum stream_state out = stream_state_open; + + while (out == stream_state_open) { + FD_ZERO(&rd); + if (in == stream_state_open) FD_SET(infd, &rd); + if (out == stream_state_open) FD_SET(sfd, &rd); + rc = select(maxfd + 1, &rd, 0, 0, NULL); + if (rc < 0) err(1, "select"); + if (FD_ISSET(infd, &rd)) in = bs_write_in_1(bs, infd); + if (FD_ISSET(sfd, &rd)) out = bs_read_out_1(bs, outfd); + } + + return in != stream_state_ended_error && out == stream_state_ended_ok ? 0 : + in == stream_state_ended_error || out == stream_state_ended_error ? 2 : 1; +} + +static void ps_reject(struct boxs *bs, size_t len, int32_t req, enum pkt_flags flags) { + // ignore the packet. if this is a request, the substream on the other end + // will just have to wait until the rpc connection closes. + (void)req; + (void)flags; + write_buf(STDERR_FILENO, "ignoring packet: "); + int rc = bs_read_out(bs, STDERR_FILENO, len); + if (rc < 0) err(1, "bs_read_out"); + write_buf(STDERR_FILENO, "\n"); +} + +static enum stream_state muxrpc_read_source_1(struct boxs *bs, int outfd, int req_id, bool no_newline) { + enum pkt_flags flags; + size_t len; + int32_t req; + int rc = ps_read_header(bs, &len, &req, &flags); + if (rc < 0) err(1, "ps_read_header"); + if (req == 0 && len == 0) { + if (bs->wrote_goodbye) return stream_state_ended_ok; + warnx("unexpected end of parent stream"); + return stream_state_ended_error; + } + if (req != -req_id) { + ps_reject(bs, len, req, flags); + return stream_state_open; + } + if (flags & pkt_flags_end) { + rc = bs_read_error(bs, STDERR_FILENO, flags, len, no_newline); + if (rc < 0) err(1, "bs_read_error"); + if (rc == 1) return stream_state_ended_error; + return stream_state_ended_ok; + } + rc = bs_read_out(bs, outfd, len); + if (rc < 0) err(1, "bs_read_out"); + if (flags & (pkt_flags_json | pkt_flags_string) && !no_newline) { + rc = write_buf(outfd, "\n"); + if (rc < 0) err(1, "write_buf"); + } + return stream_state_open; +} + +static int muxrpc_read_source(struct boxs *bs, int outfd, int req_id, bool no_newline) { + enum stream_state state; + while ((state = muxrpc_read_source_1(bs, outfd, req_id, no_newline)) == stream_state_open); + return state == stream_state_ended_ok ? 0 : + state == stream_state_ended_error ? 2 : 1; +} + +static int muxrpc_read_async(struct boxs *bs, int outfd, int req_id, bool no_newline) { + enum pkt_flags flags; + size_t len; + int32_t req; + int rc; + + while (1) { + rc = ps_read_header(bs, &len, &req, &flags); + if (rc < 0) err(1, "ps_read_header"); + if (req == -req_id) break; + if (req == 0 && len == 0) errx(1, "unexpected end of parent stream"); + ps_reject(bs, len, req, flags); + } + if (flags & pkt_flags_end) { + rc = bs_read_error(bs, STDERR_FILENO, flags, len, no_newline); + if (rc < 0) err(1, "bs_read_error"); + if (rc == 1) return 2; + return 1; + } + rc = bs_read_out(bs, outfd, len); + if (rc < 0) err(1, "bs_read_out"); + if (flags & (pkt_flags_json | pkt_flags_string) && !no_newline) { + rc = write_buf(outfd, "\n"); + if (rc < 0) err(1, "write_buf"); + } + return 0; +} + +static enum stream_state muxrpc_write_sink_1(struct boxs *bs, int infd, + enum pkt_type ptype, int req_id) { + char buf[4096]; + ssize_t sz = read(infd, buf, sizeof(buf)); + if (sz < 0) err(1, "read"); + if (sz == 0) { + ps_write(bs, "true", 4, pkt_type_json, req_id, true, true); + return stream_state_ended_ok; + } + ps_write(bs, buf, sz, ptype, req_id, true, false); + return stream_state_open; +} + +static enum stream_state muxrpc_write_sink_1_hashed(struct boxs *bs, int infd, + crypto_hash_sha256_state *hash_state, int req_id) { + int rc; + unsigned char buf[4096]; + ssize_t sz = read(infd, buf, sizeof(buf)); + if (sz < 0) err(1, "read"); + if (sz == 0) { + ps_write(bs, "true", 4, pkt_type_json, req_id, true, true); + return stream_state_ended_ok; + } + rc = crypto_hash_sha256_update(hash_state, buf, sz); + if (rc < 0) errx(1, "hash update failed"); + ps_write(bs, (char *)buf, sz, pkt_type_buffer, req_id, true, false); + return stream_state_open; +} + +static int muxrpc_write_sink(struct boxs *bs, int infd, enum pkt_type ptype, int req_id, bool no_newline) { + int rc; + fd_set rd; + int sfd = bs->s; + int maxfd = infd > sfd ? infd : sfd; + enum stream_state in = stream_state_open; + enum stream_state out = stream_state_open; + + while (out == stream_state_open) { + FD_ZERO(&rd); + if (in == stream_state_open) FD_SET(infd, &rd); + if (out == stream_state_open) FD_SET(sfd, &rd); + rc = select(maxfd + 1, &rd, 0, 0, NULL); + if (rc < 0) err(1, "select"); + if (FD_ISSET(infd, &rd)) in = muxrpc_write_sink_1(bs, infd, ptype, req_id); + if (FD_ISSET(sfd, &rd)) out = muxrpc_read_source_1(bs, -1, req_id, no_newline); + } + + return in == stream_state_ended_ok && out == stream_state_ended_ok ? 0 : + in == stream_state_ended_error || out == stream_state_ended_error ? 2 : 1; +} + +static int muxrpc_write_blob_add(struct boxs *bs, int infd, int outfd, int req_id, bool no_newline) { + int rc; + fd_set rd; + int sfd = bs->s; + int maxfd = infd > sfd ? infd : sfd; + enum stream_state in = stream_state_open; + enum stream_state out = stream_state_open; + crypto_hash_sha256_state hash_state; + unsigned char hash[32]; + char id[54] = "&"; + + rc = crypto_hash_sha256_init(&hash_state); + if (rc < 0) { errno = EINVAL; return -1; } + + while (out == stream_state_open) { + FD_ZERO(&rd); + if (in == stream_state_open) FD_SET(infd, &rd); + if (out == stream_state_open) FD_SET(sfd, &rd); + rc = select(maxfd + 1, &rd, 0, 0, NULL); + if (rc < 0) err(1, "select"); + if (FD_ISSET(infd, &rd)) in = muxrpc_write_sink_1_hashed(bs, infd, &hash_state, req_id); + if (FD_ISSET(sfd, &rd)) out = muxrpc_read_source_1(bs, -1, req_id, no_newline); + } + + rc = crypto_hash_sha256_final(&hash_state, hash); + if (rc < 0) errx(1, "hash finalize failed"); + + rc = base64_encode(hash, 32, id+1, sizeof(id)-1); + if (rc < 0) err(1, "encoding hash failed"); + strcpy(id + 45, ".sha256\n"); + rc = write_all(outfd, id, sizeof(id)-1); + if (rc < 0) err(1, "writing hash failed"); + + return in == stream_state_ended_ok && out == stream_state_ended_ok ? 0 : + in == stream_state_ended_error || out == stream_state_ended_error ? 2 : 1; +} + +static int muxrpc_duplex(struct boxs *bs, int infd, int outfd, enum pkt_type in_ptype, int req_id, bool no_newline) { + int rc; + fd_set rd; + int sfd = bs->s; + int maxfd = infd > sfd ? infd : sfd; + enum stream_state in = stream_state_open; + enum stream_state out = stream_state_open; + + while (out == stream_state_open) { + FD_ZERO(&rd); + if (in == stream_state_open) FD_SET(infd, &rd); + if (out == stream_state_open) FD_SET(sfd, &rd); + rc = select(maxfd + 1, &rd, 0, 0, NULL); + if (rc < 0) err(1, "select"); + if (FD_ISSET(infd, &rd)) in = muxrpc_write_sink_1(bs, infd, in_ptype, req_id); + if (FD_ISSET(sfd, &rd)) out = muxrpc_read_source_1(bs, outfd, req_id, no_newline); + } + + return in == stream_state_ended_ok && out == stream_state_ended_ok ? 0 : + in == stream_state_ended_error || out == stream_state_ended_error ? 2 : 1; +} + +static int method_to_json(char *out, size_t outlen, const char *str) { + // blobs.get => ["blobs", "get"] + size_t i = 0; + char c; + if (i+2 > outlen) return -1; + out[i++] = '['; + out[i++] = '"'; + while ((c = *str++)) { + if (c == '.') { + if (i+3 > outlen) return -1; + out[i++] = '"'; + out[i++] = ','; + out[i++] = '"'; + } else if (c == '"') { + if (i+2 > outlen) return -1; + out[i++] = '\\'; + out[i++] = '"'; + } else { + if (i+1 > outlen) return -1; + out[i++] = c; + } + } + if (i+3 > outlen) return -1; + out[i++] = '"'; + out[i++] = ']'; + out[i++] = '\0'; + return i; +} + +static int args_to_json_length(int argc, char *argv[], bool encode_strings) { + int i = 0; + int len = 3; // "[]\0" + for (i = 0; i < argc; i++) { + if (!encode_strings) { + len += strlen(argv[i])+1; + } else { + len += 3; // "\"\"," + char *arg = argv[i], c; + while ((c = *arg++)) switch (c) { + case '"': len += 2; break; + case '\\': len += 2; break; + default: len++; + } + } + } + return len; +} + +static int args_to_json(char *out, size_t outlen, unsigned int argc, char *argv[], bool encode_strings) { + size_t i = 0; + size_t j; + if (i+1 > outlen) return -1; + out[i++] = '['; + for (j = 0; j < argc; j++) { + if (!encode_strings) { + size_t len = strlen(argv[j]); + if (j > 0) out[i++] = ','; + if (i+len > outlen) return -1; + strncpy(out+i, argv[j], len); + i += len; + } else { + char *arg = argv[j]; + char c; + if (j > 0) { + if (i+1 > outlen) return -1; + out[i++] = ','; + } + if (i+1 > outlen) return -1; + out[i++] = '"'; + while ((c = *arg++)) { + if (i+2 > outlen) return -1; + if (c == '"' || c == '\\') out[i++] = '\\'; + out[i++] = c; + } + if (i+1 > outlen) return -1; + out[i++] = '"'; + } + } + if (i+2 > outlen) return -1; + out[i++] = ']'; + out[i++] = '\0'; + return i; +} + +int main(int argc, char *argv[]) { + int i, s, infd, outfd, rc; + const char *key = NULL; + const char *keypair_seed_str = NULL; + const char *host = NULL; + const char *port = "8008"; + const char *typestr = NULL, *methodstr = NULL; + const char *shs_cap_key_str = NULL; + const char *socket_path = NULL; + size_t argument_len; + unsigned char private_key[64]; + unsigned char public_key[32]; + unsigned char remote_key[32]; + unsigned char shs_cap_key[32]; + enum muxrpc_type type; + enum pkt_type ptype = pkt_type_buffer; + char method[256]; + char app_dir[_POSIX_PATH_MAX]; + ssize_t len; + bool test = false; + bool noauth = false; + bool no_newline = false; + bool raw = false; + bool host_arg = false; + bool port_arg = false; + bool key_arg = false; + bool shs_cap_key_str_arg = false; + bool ipv4_arg = false; + bool ipv6_arg = false; + bool passthrough = false; + bool strings = false; + enum ip_family ip_family; + + get_app_dir(app_dir, sizeof(app_dir)); + + char config_buf[8192]; + len = read_file(config_buf, sizeof(config_buf), "%s/config", app_dir); + if (len > 0) { + ssize_t key_len = json_get_value(config_buf, "key", &key); + ssize_t host_len = json_get_value(config_buf, "host", &host); + ssize_t port_len = json_get_value(config_buf, "port", &port); + ssize_t shs_cap_len = json_get_value(config_buf, "caps.shs", &shs_cap_key_str); + if (key_len >= 0) ((char *)key)[key_len] = '\0'; + if (host_len >= 0) ((char *)host)[host_len] = '\0'; + if (port_len >= 0) ((char *)port)[port_len] = '\0'; + if (shs_cap_len >= 0) ((char *)shs_cap_key_str)[shs_cap_len] = '\0'; + } else if (len < 0 && errno != ENOENT) { + err(1, "failed to read config"); + } + + for (i = 1; i < argc && (argv[i][0] == '-'); i++) { + switch (argv[i][1]) { + case 'c': shs_cap_key_str = argv[++i]; shs_cap_key_str_arg = true; break; + case 'j': ptype = pkt_type_json; break; + case 'T': test = true; break; + case 's': host = argv[++i]; host_arg = true; break; + case 'k': key = argv[++i]; key_arg = true; break; + case 'K': keypair_seed_str = argv[++i]; break; + case 'p': port = argv[++i]; port_arg = true; break; + case 'u': socket_path = argv[++i]; break; + case 't': typestr = argv[++i]; break; + case 'n': noauth = true; break; + case '4': ipv4_arg = true; break; + case '6': ipv6_arg = true; break; + case 'a': passthrough = true; break; + case 'l': no_newline = true; break; + case 'r': raw = true; no_newline = true; break; + case 'e': strings = true; break; + default: usage(); + } + } + if (i < argc) methodstr = argv[i++]; + else if (!test && !passthrough) usage(); + + if (ipv4_arg && ipv6_arg) errx(1, "options -4 and -6 conflict"); + ip_family = + ipv4_arg ? ip_family_ipv4 : + ipv6_arg ? ip_family_ipv6 : + ip_family_any; + + if (shs_cap_key_str) { + rc = pubkey_decode(shs_cap_key_str, shs_cap_key); + if (rc < 0) err(1, "unable to decode cap key '%s'", shs_cap_key_str); + } else { + memcpy(shs_cap_key, ssb_cap, 32); + } + + argument_len = test ? 0 : args_to_json_length(argc-i, argv+i, strings); + char argument[argument_len]; + + if (passthrough) { + if (methodstr) errx(1, "-a option conflicts with method"); + if (typestr) errx(1, "-a option conflicts with -t option"); + if (argc-i > 0) errx(1, "-a option conflicts with method arguments"); + if (test) errx(1, "-a option conflicts with -T test"); + + } else if (!test) { + rc = args_to_json(argument, sizeof(argument), argc-i, argv+i, strings); + if (rc < 0) errx(1, "unable to collect arguments"); + + char manifest_buf[8192]; + if (!typestr) { + len = read_file(manifest_buf, sizeof(manifest_buf), + "%s/manifest.json", app_dir); + if (len < 0) err(1, "failed to read manifest file"); + + ssize_t type_len = json_get_value(manifest_buf, methodstr, &typestr); + if (!typestr && errno == ENOMSG) errx(1, + "unable to find method '%s' in manifest", methodstr); + if (!typestr) err(1, "unable to read manifest %s/%s", manifest_buf, methodstr); + ((char *)typestr)[type_len] = '\0'; + } + if (strcmp(typestr, "sync") == 0) type = muxrpc_type_async; + else if (strcmp(typestr, "async") == 0) type = muxrpc_type_async; + else if (strcmp(typestr, "sink") == 0) type = muxrpc_type_sink; + else if (strcmp(typestr, "source") == 0) type = muxrpc_type_source; + else if (strcmp(typestr, "duplex") == 0) type = muxrpc_type_duplex; + else errx(1, "type must be one of "); + + rc = method_to_json(method, sizeof(method), methodstr); + if (rc < 0) errx(0, "unable to convert method name"); + } + + if (keypair_seed_str == NULL) { + read_private_key(app_dir, private_key); + memcpy(public_key, private_key+32, 32); + } else if (strlen(keypair_seed_str) > 55) { + rc = seckey_decode(keypair_seed_str, private_key); + if (rc < 0) err(1, "unable to decode private key"); + memcpy(public_key, private_key+32, 32); + } else if (keypair_seed_str) { + unsigned char seed[crypto_sign_SEEDBYTES]; + unsigned char ed25519_skpk[crypto_sign_ed25519_SECRETKEYBYTES]; + + rc = pubkey_decode(keypair_seed_str, ed25519_skpk); + if (rc < 0) err(1, "unable to decode private key"); + rc = crypto_sign_ed25519_sk_to_seed(seed, ed25519_skpk); + if (rc < 0) err(1, "unable to convert private key to seed"); + rc = crypto_sign_seed_keypair(public_key, private_key, seed); + if (rc < 0) err(1, "unable to generate keypair from seed"); + } + + if (key) { + rc = pubkey_decode(key, remote_key); + if (rc < 0) err(1, "unable to decode remote key '%s'", key); + } else { + memcpy(remote_key, public_key, 32); + } + + bool implied_tcp = host_arg || port_arg || ipv4_arg || ipv6_arg; + bool implied_auth = key_arg || keypair_seed_str || shs_cap_key_str_arg || test; + + if (test) { + infd = STDIN_FILENO; + outfd = STDOUT_FILENO; + s = -1; + + } else if (socket_path) { + if (implied_tcp) errx(1, "-u option conflicts with host/port options"); + s = unix_connect(socket_path); + if (s < 0) err(1, "unix_connect"); + infd = outfd = s; + + } else if (!implied_tcp && !implied_auth) { + char socket_path_buf[_POSIX_PATH_MAX]; + rc = get_socket_path(socket_path_buf, sizeof(socket_path_buf), app_dir); + if (rc < 0 && noauth) err(1, "get_socket_path"); + if (rc < 0) goto do_tcp_connect; + s = unix_connect(socket_path_buf); + if (s < 0 && noauth) err(1, "unix_connect"); + if (s < 0) goto do_tcp_connect; + noauth = true; + infd = outfd = s; + + } else { +do_tcp_connect: + s = tcp_connect(host, port, ip_family); + if (s < 0) err(1, "tcp_connect"); + infd = outfd = s; + } + + struct boxs bs; + if (noauth) { + bs.s = s; + bs.noauth = true; + if (implied_auth) errx(1, "-n option conflicts with -k, -K, -c and -T options."); + } else { + shs_connect(s, infd, outfd, public_key, private_key, shs_cap_key, remote_key, &bs); + } + + if (test) { + rc = write_all(outfd, bs.encrypt_key, sizeof(bs.encrypt_key)); + rc |= write_all(outfd, bs.nonce1, sizeof(bs.nonce1)); + rc |= write_all(outfd, bs.decrypt_key, sizeof(bs.decrypt_key)); + rc |= write_all(outfd, bs.rx_nonce, sizeof(bs.rx_nonce)); + if (rc < 0) err(1, "failed to write handshake result"); + return 0; + } + + if (passthrough) { + rc = bs_passthrough(&bs, STDIN_FILENO, STDOUT_FILENO); + close(s); + return rc; + } + + muxrpc_call(&bs, method, argument, type, typestr, 1); + + if (raw) { + struct termios raw_tc; + rc = tcgetattr(STDIN_FILENO, &orig_tc); + if (rc < 0) warn("tcgetattr"); + raw_tc = orig_tc; + raw_tc.c_lflag &= ~(ICANON | ECHO); + rc = tcsetattr(STDIN_FILENO, TCSANOW, &raw_tc); + if (rc < 0) warn("tcgetattr"); + rc = atexit(reset_termios); + if (rc < 0) warn("atexit"); + } + + switch (type) { + case muxrpc_type_async: + rc = muxrpc_read_async(&bs, STDOUT_FILENO, 1, no_newline); + break; + case muxrpc_type_source: + rc = muxrpc_read_source(&bs, STDOUT_FILENO, 1, no_newline); + break; + case muxrpc_type_sink: + if (!strcmp(methodstr, "blobs.add")) { + rc = muxrpc_write_blob_add(&bs, STDIN_FILENO, STDOUT_FILENO, 1, no_newline); + } else { + rc = muxrpc_write_sink(&bs, STDIN_FILENO, ptype, 1, no_newline); + } + break; + case muxrpc_type_duplex: + rc = muxrpc_duplex(&bs, STDIN_FILENO, STDOUT_FILENO, ptype, 1, no_newline); + break; + } + + ps_goodbye(&bs); + bs_end(&bs); + close(s); + return rc; +} diff --git a/.install/sbotc/test-shs-inner.sh b/.install/sbotc/test-shs-inner.sh new file mode 100755 index 0000000..622e241 --- /dev/null +++ b/.install/sbotc/test-shs-inner.sh @@ -0,0 +1,8 @@ +#!/bin/sh +cap_hex=${1?shs cap key} +pk_hex=${2?server public key} + +cap_b64="$(echo -n "$cap_hex" | xxd -r -p | base64)" +pk_b64="$(echo -n "$pk_hex" | xxd -r -p | base64)" + +exec sbotc -T -c "$cap_b64" -k "$pk_b64"