From beb573d6ef59d2573f579fe3383846dfca9972db Mon Sep 17 00:00:00 2001 From: cel Date: Sat, 3 Jun 2017 23:09:26 -1000 Subject: [PATCH] Initial commit --- Makefile | 28 ++ README.md | 24 ++ base64.c | 75 +++++ base64.h | 5 + jsmn.c | 278 ++++++++++++++++++ jsmn.h | 67 +++++ sbotc.1 | 91 ++++++ sbotc.c | 856 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 1424 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 base64.c create mode 100644 base64.h create mode 100644 jsmn.c create mode 100644 jsmn.h create mode 100644 sbotc.1 create mode 100644 sbotc.c diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b9bab17 --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +BIN = sbotc +LDLIBS = -lsodium + +PREFIX = /usr/local +BINDIR = $(PREFIX)/bin +MANDIR = $(PREFIX)/share/man + +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 -rsvf $(BIN) $(DESTDIR)$(BINDIR) + @ln -rsvf $(BIN).1 $(DESTDIR)$(MANDIR)/man1 + +uninstall: + @rm -vf \ + $(DESTDIR)$(BINDIR)/$(BIN) \ + $(DESTDIR)$(MANDIR)/man1/$(BIN).1 + +clean: + @rm -vf $(BIN) diff --git a/README.md b/README.md new file mode 100644 index 0000000..b226f16 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# sbotc + +A command-line SSB client in C. Use like the `sbot` command. + +## Install + +Install the dependency, *sodium*. On Debian: `sudo apt-get install libsodium-dev` + +Compile and install the program: + +```sh +make +sudo make install +``` + +## Usage + +```sh +sbotc [-s ] [-p ] [-k ] [-t ] [...] +``` + +Arguments must be explicitly JSON-encoded. + +For more information, see the manual page `sbotc(1)`. diff --git a/base64.c b/base64.c new file mode 100644 index 0000000..32e26c9 --- /dev/null +++ b/base64.c @@ -0,0 +1,75 @@ +/* + + 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; +} diff --git a/base64.h b/base64.h new file mode 100644 index 0000000..65c77c1 --- /dev/null +++ b/base64.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +int base64_decode(const char *s, size_t str_len, void *data, size_t data_len); diff --git a/jsmn.c b/jsmn.c new file mode 100644 index 0000000..591185f --- /dev/null +++ b/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 >= 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/jsmn.h b/jsmn.h new file mode 100644 index 0000000..03b2c1a --- /dev/null +++ b/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/sbotc.1 b/sbotc.1 new file mode 100644 index 0000000..2ebdceb --- /dev/null +++ b/sbotc.1 @@ -0,0 +1,91 @@ +.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 s Ar host +.Op Fl p Ar port +.Op Fl k Ar key +.Op Fl t Ar type +.Ar method +.Op Ar argument ... +.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 s Ar host +The hostname to connect to. Default is localhost. +.It Fl p Ar port +The port to connect to. Default is 8008. +.It Fl k Ar key +The key to connect to. Default is your public key. +.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. +.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 host and port to use if the +.Ar -s +or +.Ar -p +options are not given. +.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/sbotc.c b/sbotc.c new file mode 100644 index 0000000..f0630b4 --- /dev/null +++ b/sbotc.c @@ -0,0 +1,856 @@ +/* + * 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 "base64.h" +#include "jsmn.h" + +#define BOXS_MAXLEN 4096 + +#define write_buf(fd, buf) \ + write_all(fd, buf, sizeof(buf)) + +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; +}; + +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, +}; + +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 +}; + +static void usage() { + fputs("usage: sbotc [-s ] [-p ] [-k ] [-t ] " + " [...]\n", stderr); + exit(EXIT_FAILURE); +} + +static int auth_keypair(unsigned char *pk, unsigned char *sk, unsigned char *seed) { + unsigned char pk_ed[32], sk_ed[32]; + int rc = crypto_sign_seed_keypair(pk_ed, sk_ed, seed); + rc |= crypto_sign_ed25519_pk_to_curve25519(pk, pk_ed); + rc |= crypto_sign_ed25519_sk_to_curve25519(sk, sk_ed); + return rc; +} + +static int tcp_connect(const char *host, const char *port) { + struct addrinfo hints; + struct addrinfo *result, *rp; + int s; + int fd; + int err; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + 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); + return fd; +} + +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 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 s, 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]; + unsigned char seed[32]; + randombytes_buf(seed, sizeof(seed)); + rc = auth_keypair(kx_pk, kx_sk, seed); + 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(s, buf, sizeof(buf)); + if (rc < 0) err(1, "failed to send challenge"); + + // recv challenge + unsigned char remote_kx_pk[32]; + rc = read_all(s, 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(s, boxed_auth, sizeof(boxed_auth)); + if (rc < 0) errx(1, "failed to send auth"); + + // verify accept + + unsigned char boxed_okay[80]; + rc = read_all(s, 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 = s; +} + +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 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 ? end - 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 (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 (rc >= len) { errno = ENAMETOOLONG; return -1; } + + rc = stat(buf, &st); + if (rc < 0) return -1; + if (st.st_size > 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_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 int bs_read_packet(struct boxs *bs, void *buf, size_t *lenp) { + unsigned char boxed_header[34]; + struct boxs_header header; + int 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) { + 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 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) { + // 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", 0) == 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)) { + if (write_buf(errfd, "\n") < 0) return -1; + } + return 1; +} + +static void bs_write(struct boxs *bs, const unsigned char *buf, size_t len) { + while (len > 0) { + size_t l = len > BOXS_MAXLEN ? BOXS_MAXLEN : len; + bs_write_packet(bs, buf, l); + len -= l; + buf += l; + } +} + +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 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[8096]; + 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 (reqlen >= sizeof(req)) errx(1, "request too large"); + + ps_write(bs, req, reqlen, pkt_type_json, req_id, !is_request, false); +} + +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. + 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) { + 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) { + 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); + 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)) { + 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) { + enum stream_state state; + while ((state = muxrpc_read_source_1(bs, outfd, req_id)) == 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) { + 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); + 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)) { + 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, 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, pkt_type_buffer, req_id, true, false); + return stream_state_open; +} + +static int muxrpc_write_sink(struct boxs *bs, int infd, int req_id) { + 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, req_id); + if (FD_ISSET(sfd, &rd)) out = muxrpc_read_source_1(bs, -1, req_id); + } + + 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, int req_id) { + 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 + || (in == stream_state_open && out != stream_state_ended_error)) { + 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, req_id); + if (FD_ISSET(sfd, &rd)) out = muxrpc_read_source_1(bs, outfd, req_id); + } + + 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[]) { + int i = 0; + int len = 3; // "[]\0" + for (i = 0; i < argc; i++) + len += strlen(argv[i])+1; + return len; +} + +static int args_to_json(char *out, size_t outlen, unsigned int argc, char *argv[]) { + size_t i = 0; + size_t j; + if (i+1 > outlen) return -1; + out[i++] = '['; + for (j = 0; j < argc; j++) { + 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; + } + if (i+2 > outlen) return -1; + out[i++] = ']'; + out[i++] = '\0'; + return i; +} + +int main(int argc, char *argv[]) { + int i, s, rc; + const char *key = NULL; + const char *host = NULL; + const char *port = "8008"; + const char *typestr = NULL, *methodstr; + size_t argument_len; + unsigned char private_key[64]; + unsigned char public_key[32]; + unsigned char remote_key[32]; + enum muxrpc_type type; + char method[256]; + char app_dir[_POSIX_PATH_MAX]; + ssize_t len; + + 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 host_len = json_get_value(config_buf, "host", &host); + ssize_t port_len = json_get_value(config_buf, "port", &port); + if (host_len >= 0) ((char *)host)[host_len] = '\0'; + if (port_len >= 0) ((char *)port)[port_len] = '\0'; + } else if (len < 0 && errno != ENOENT) { + err(1, "failed to read config"); + } + + for (i = 1; (i + 1 < argc) && (argv[i][0] == '-'); i++) { + switch (argv[i][1]) { + case 's': host = argv[++i]; break; + case 'k': key = argv[++i]; break; + case 'p': port = argv[++i]; break; + case 't': typestr = argv[++i]; break; + default: usage(); + } + } + if (i < argc) methodstr = argv[i++]; else usage(); + + argument_len = args_to_json_length(argc-i, argv+i); + char argument[argument_len]; + rc = args_to_json(argument, sizeof(argument), argc-i, argv+i); + if (rc < 0) errx(0, "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"); + ((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"); + + read_private_key(app_dir, private_key); + + memcpy(public_key, private_key+32, 32); + 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); + } + + s = tcp_connect(host, port); + if (s < 0) err(1, "tcp_connect"); + + struct boxs bs; + shs_connect(s, public_key, private_key, ssb_cap, remote_key, &bs); + + muxrpc_call(&bs, method, argument, type, typestr, 1); + + switch (type) { + case muxrpc_type_async: + rc = muxrpc_read_async(&bs, STDOUT_FILENO, 1); + break; + case muxrpc_type_source: + rc = muxrpc_read_source(&bs, STDOUT_FILENO, 1); + break; + case muxrpc_type_sink: + rc = muxrpc_write_sink(&bs, STDIN_FILENO, 1); + break; + case muxrpc_type_duplex: + rc = muxrpc_duplex(&bs, STDIN_FILENO, STDOUT_FILENO, 1); + break; + } + + close(s); + return rc; +}