# Get some libraries
UCW::Configure::Pkg::PkgConfig("libucw") or Fail("libucw is required");
UCW::Configure::Pkg::PkgConfig("libucw-json") or Fail("libucw-json is required");
+UCW::Configure::Pkg::TrivConfig("libgcrypt", script => "libgcrypt-config", minversion => '1.6') or Fail("libgcrypt is required");
Finish();
Log "\nConfigured, run `make' to build everything.\n";
CONFIGS+=subauthd
$(o)/server/subauthd: $(addprefix $(o)/server/, subauthd.o cmd.o auth.o)
+$(o)/server/subauthd: LIBS+=$(LIBGCRYPT_LIBS)
+$(o)/server/auth.o: CFLAGS+=$(LIBGCRYPT_CFLAGS)
*/
#include <ucw/lib.h>
+#include <ucw/base64.h>
#include <ucw/gary.h>
#include <ucw/stkstring.h>
+#include <ucw/string.h>
+#include <ucw/unaligned.h>
#include <errno.h>
#include <fcntl.h>
+#include <gcrypt.h>
#include <time.h>
#include <unistd.h>
#include "subauthd.h"
+struct auth_token *auth_fake_token;
+
#define HASH_NODE struct auth_user
#define HASH_PREFIX(x) user_hash_##x
#define HASH_KEY_ENDSTRING login
return aa;
}
+struct auth_token *auth_find_token_passwd(struct auth_acct *aa)
+{
+ CLIST_FOR_EACH(struct auth_token *, at, aa->tokens)
+ if (at->type == TOKEN_PASSWORD)
+ return at;
+ return NULL;
+}
+
+struct auth_token *auth_find_token_generated(struct auth_acct *aa, char *ident)
+{
+ CLIST_FOR_EACH(struct auth_token *, at, aa->tokens)
+ if (at->type == TOKEN_GENERATED && !strcmp(at->ident, ident))
+ return at;
+ return NULL;
+}
+
void auth_delete_user(struct auth_user *au)
{
struct auth_acct *aa;
return at;
}
+static void auth_hash(char *hash_text, struct auth_token *at, const char *passwd)
+{
+ // This is PBKDF2 based on SHA-256-HMAC
+
+ gcry_md_hd_t md;
+ if (gcry_md_open(&md, GCRY_MD_SHA256, GCRY_MD_FLAG_HMAC) != GPG_ERR_NO_ERROR)
+ die("gcry_md_open(CGRY_MD_SHA256) failed");
+ if (gcry_md_setkey(md, passwd, strlen(passwd)) != GPG_ERR_NO_ERROR)
+ die("gcry_md_setkey() failed");
+
+ byte current_hash[HASH_BYTES];
+ gcry_md_write(md, at->salt, strlen(at->salt));
+ byte zeroes[4] = { 0, 0, 0, 0 };
+ gcry_md_write(md, zeroes, 4);
+ memcpy(current_hash, gcry_md_read(md, 0), HASH_BYTES);
+
+ byte accumulator[HASH_BYTES];
+ memcpy(accumulator, current_hash, HASH_BYTES);
+
+ for (uint i=1; i < at->iterations; i++)
+ {
+ gcry_md_reset(md);
+ gcry_md_write(md, current_hash, HASH_BYTES);
+ memcpy(current_hash, gcry_md_read(md, 0), HASH_BYTES);
+ for (uint j=0; j<HASH_BYTES; j++)
+ accumulator[j] ^= current_hash[j];
+ }
+
+ gcry_md_close(md);
+
+ uint out_len = base64_encode(hash_text, accumulator, HASH_BYTES);
+ hash_text[out_len] = 0;
+}
+
+static void set_salt(struct auth_token *at)
+{
+ byte salt_bytes[DEFAULT_SALT_BYTES];
+ gcry_randomize(salt_bytes, DEFAULT_SALT_BYTES, GCRY_STRONG_RANDOM);
+
+ char salt_text[2*DEFAULT_SALT_BYTES + 1];
+ uint salt_len = base64_encode(salt_text, salt_bytes, DEFAULT_SALT_BYTES);
+ salt_text[salt_len] = 0;
+
+ at->salt = xstrdup(salt_text);
+ at->iterations = DEFAULT_HASH_ITERATIONS;
+}
+
void auth_set_token_passwd(struct auth_token *at, const char *passwd)
{
at->type = TOKEN_PASSWORD;
- at->salt = xstrdup("???");
- at->hash = xstrdup("???");
+ set_salt(at);
+
+ char hash[MAX_TEXT_HASH_SIZE];
+ auth_hash(hash, at, passwd);
+ at->hash = xstrdup(hash);
+}
+
+char *auth_set_token_generated(struct auth_token *at, const char *comment)
+{
+ char ident_text[2*DEFAULT_IDENT_BYTES + 1];
+ do
+ {
+ byte ident_bytes[DEFAULT_IDENT_BYTES];
+ gcry_randomize(ident_bytes, DEFAULT_IDENT_BYTES, GCRY_STRONG_RANDOM);
+ mem_to_hex(ident_text, ident_bytes, DEFAULT_IDENT_BYTES, 0);
+ }
+ while (auth_find_token_generated(at->acct, ident_text));
+
+ at->type = TOKEN_GENERATED;
+ at->ident = xstrdup(ident_text);
+ at->comment = xstrdup(comment);
+ set_salt(at);
+
+ byte token_bytes[DEFAULT_GENERATED_BYTES];
+ gcry_randomize(token_bytes, DEFAULT_GENERATED_BYTES, GCRY_STRONG_RANDOM);
+
+ uint token_len = 2*DEFAULT_IDENT_BYTES + 1 + 2*DEFAULT_GENERATED_BYTES + 1;
+ char *token_text = xmalloc(token_len);
+ memcpy(token_text, ident_text, 2*DEFAULT_IDENT_BYTES);
+ char *token_rhs = token_text + 2*DEFAULT_IDENT_BYTES;
+ *token_rhs++ = '-';
+ uint out_len = base64_encode(token_rhs, token_bytes, DEFAULT_GENERATED_BYTES);
+ if (out_len > 0 && token_rhs[out_len-1] == '=')
+ out_len--;
+ token_rhs[out_len] = 0;
+
+ char hash[MAX_TEXT_HASH_SIZE];
+ auth_hash(hash, at, token_rhs);
+ at->hash = xstrdup(hash);
+
+ return token_text;
+}
+
+bool auth_check_token(struct auth_token *at, const char *passwd)
+{
+ char hash[MAX_TEXT_HASH_SIZE];
+ auth_hash(hash, at, passwd);
+ return !strcmp(at->hash, hash);
+}
+
+static void auth_create_fake_token(void)
+{
+ struct auth_token *at = xmalloc_zero(sizeof(*at));
+ auth_set_token_passwd(at, "TheKeyIsUnderTheFlyingCarpet");
+ auth_fake_token = at;
}
static void NONRET db_corrupted(const char *reason)
const char *salt = get_string(jt, "s");
const char *hash = get_string(jt, "h");
const char *cmt = get_string(jt, "c");
+ const char *ident = get_string(jt, "i");
if (!salt || !hash)
db_corrupted("Token must have salt and hash");
- uint lastmod;
- if (!get_uint(jt, "m", &lastmod))
- db_corrupted("Token has invalid last modified time");
-
struct auth_token *at = auth_create_token(aa);
at->type = type;
at->salt = xstrdup(salt);
at->hash = xstrdup(hash);
at->comment = xstrdup(cmt);
+ at->ident = xstrdup(ident);
+
+ uint lastmod;
+ if (!get_uint(jt, "m", &lastmod))
+ db_corrupted("Token has invalid last modified time");
at->last_modified = lastmod;
+
+ if (!get_uint(jt, "n", &at->iterations))
+ db_corrupted("Token has invalid number of hash iterations");
}
}
}
json_object_set(jt, "h", json_new_string_ref(js, at->hash));
if (at->comment)
json_object_set(jt, "c", json_new_string_ref(js, at->comment));
+ if (at->ident)
+ json_object_set(jt, "i", json_new_string_ref(js, at->ident));
+ json_object_set(jt, "n", json_new_number(js, at->iterations));
json_object_set(jt, "m", json_new_number(js, at->last_modified));
}
if (GARY_SIZE(jts->elements))
void auth_init(void)
{
+ if (!gcry_check_version(GCRYPT_VERSION))
+ die("libgcrypt version mismatch");
+ gcry_control (GCRYCTL_DISABLE_SECMEM, 0);
+ gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0);
+
user_hash_init();
db_read();
+ auth_create_fake_token();
}
if (!aa->zone->allow_passwd)
cmd_error(c, "This zone does not allow authentication by password");
+ if (strchr(passwd, '-'))
+ cmd_error(c, "The minus sign is forbidden in passwords");
+
msg(L_INFO, "Set password: login=<%s> zone=<%s>", aa->user->login, aa->zone->name);
- CLIST_FOR_EACH(struct auth_token *, at, aa->tokens)
- if (at->type == TOKEN_PASSWORD)
- {
- auth_delete_token(at);
- break;
- }
+ struct auth_token *at_old = auth_find_token_passwd(aa);
+ if (at_old)
+ auth_delete_token(at_old);
struct auth_token *at = auth_create_token(aa);
auth_set_token_passwd(at, passwd);
- // FIXME
-
db_write();
cmd_ok(c);
}
-static void cmd_verify(struct client *c)
+static void cmd_create_token(struct client *c)
{
struct auth_acct *aa = cmd_need_target_acct(c);
- const char *passwd = cmd_need_string(c, "passwd");
- // FIXME
+ if (!aa->zone->allow_tokens)
+ cmd_error(c, "This zone does not allow authentication by tokens");
+
+ if (clist_size(&aa->tokens) >= aa->zone->allow_tokens)
+ cmd_error(c, "Maximum number of tokens was reached");
+
+ struct auth_token *at = auth_create_token(aa);
+ char *tok = auth_set_token_generated(at, get_string(c->request, "comment"));
+ json_object_set(c->reply, "token", json_new_string(c->json, tok));
+ xfree(tok);
+
+ msg(L_INFO, "Created token: login=<%s> zone=<%s> id=<%s>", aa->user->login, aa->zone->name, at->ident);
+
+ db_write();
+ cmd_ok(c);
+}
+
+static void cmd_login_fake(struct client *c, const char *passwd)
+{
+ auth_check_token(auth_fake_token, passwd);
+ cmd_error(c, "Invalid password");
+}
+
+static void cmd_login(struct client *c)
+{
+ struct auth_zone *az = cmd_need_zone(c);
+ const char *given_passwd = cmd_need_string(c, "passwd");
+
+ // Split password string to token ident and the password proper
+ char passbuf[strlen(given_passwd) + 1];
+ strcpy(passbuf, given_passwd);
+ char *ident, *passwd;
+ char *sep = strchr(passbuf, '-');
+ if (sep)
+ {
+ *sep = 0;
+ ident = passbuf;
+ passwd = sep + 1;
+ }
+ else
+ {
+ ident = NULL;
+ passwd = passbuf;
+ }
+
+ const char *login = cmd_need_string(c, "login");
+ struct auth_user *au = auth_find_user(login, 0);
+ if (!au)
+ {
+ msg(L_INFO, "Login failed: No user=<%s>", login);
+ return cmd_login_fake(c, passwd);
+ }
+
+ struct auth_acct *aa = auth_find_acct(au, az, 0);
+ if (!aa)
+ {
+ msg(L_INFO, "Login failed: No account user=<%s> zone=<%s>", login, az->name);
+ return cmd_login_fake(c, passwd);
+ }
+
+ struct auth_token *at;
+ if (ident)
+ at = auth_find_token_generated(aa, ident);
+ else
+ at = auth_find_token_passwd(aa);
+ if (!at)
+ {
+ msg(L_INFO, "Login failed: No token user=<%s> zone=<%s> ident=<%s>", login, az->name, (ident ? : ""));
+ return cmd_login_fake(c, passwd);
+ }
+
+ if (!auth_check_token(at, passwd))
+ {
+ msg(L_INFO, "Login failed: Wrong password for user=<%s> zone=<%s> ident=<%s>", login, az->name, (ident ? : ""));
+ cmd_error(c, "Invalid password");
+ }
+
+ msg(L_INFO, "Login successful: user=<%s> zone=<%s> ident=<%s>", login, az->name, (ident ? : ""));
+ cmd_ok(c);
+}
+
+static void cmd_list(struct client *c)
+{
+ const char *login = cmd_need_target_login(c);
+
+ struct auth_user *au = auth_find_user(login, 0);
+ if (!au)
+ cmd_error(c, "No such user");
+
+ struct json_context *js = c->json;
+ json_object_set(c->reply, "login", json_new_string(js, au->login));
+ struct json_node *jas = json_new_array(js);
+ json_object_set(c->reply, "accounts", jas);
+
+ CLIST_FOR_EACH(struct auth_acct *, aa, au->accounts)
+ {
+ struct json_node *ja = json_new_object(js);
+ json_array_append(jas, ja);
+ json_object_set(ja, "zone", json_new_string(js, aa->zone->name));
+
+ struct json_node *jts = json_new_array(js);
+ json_object_set(ja, "tokens", jts);
+
+ CLIST_FOR_EACH(struct auth_token *, at, aa->tokens)
+ {
+ struct json_node *jt = json_new_object(js);
+ json_array_append(jts, jt);
+
+ const char *type;
+ switch (at->type)
+ {
+ case TOKEN_PASSWORD: type = "passwd"; break;
+ case TOKEN_GENERATED: type = "token"; break;
+ default: type = "unknown"; break;
+ }
+ json_object_set(jt, "type", json_new_string(js, type));
+
+ if (at->ident)
+ json_object_set(jt, "ident", json_new_string(js, at->ident));
+ if (at->comment)
+ json_object_set(jt, "comment", json_new_string(js, at->comment));
+ json_object_set(jt, "lastmod", json_new_number(js, at->last_modified));
+ }
+ }
cmd_ok(c);
}
{ "nop", cmd_nop },
{ "create-acct", cmd_create_acct },
{ "delete-acct", cmd_delete_acct },
+ { "create-token", cmd_create_token },
{ "set-passwd", cmd_set_passwd },
- { "verify", cmd_verify },
+ { "login", cmd_login },
+ { "list", cmd_list },
};
void cmd_dispatch(struct client *c)
* (c) 2017 Martin Mares <mj@ucw.cz>
*/
+#undef LOCAL_DEBUG
+
#include <ucw/lib.h>
#include <ucw/conf.h>
#include <ucw/log.h>
static void client_close(struct client *c)
{
- msg(L_INFO, "Closing connection");
+ DBG("Closing connection");
file_del(&c->socket);
timer_del(&c->timer);
close(c->socket.fd);
TRANS_END;
int len = fbbuf_count_written(&fb);
- msg(L_INFO, "Sending reply of %d bytes", len);
+ DBG("Sending reply of %d bytes", len);
if (send(c->socket.fd, packet_buffer, len, 0) < 0)
{
if (errno == EAGAIN || errno == EINTR)
{
- msg(L_INFO, "Postponed send");
+ DBG("Postponed send");
c->socket.write_handler = socket_write_handler;
file_chg(&c->socket);
}
}
else
{
- msg(L_INFO, "Reply sent");
+ DBG("Reply sent");
c->socket.read_handler = socket_read_handler;
c->socket.write_handler = NULL;
file_chg(&c->socket);
return HOOK_RETRY;
}
- msg(L_INFO, "Got message from UID %d", (int) cred->uid);
+ DBG("Got message from UID %d", (int) cred->uid);
c->uid = cred->uid;
fi->read_handler = NULL;
}
num_connections++;
- msg(L_INFO, "Accepted connection");
+ DBG("Accepted connection");
if (fcntl(new_sk, F_SETFL, fcntl(new_sk, F_GETFL) | O_NONBLOCK) < 0)
die("Cannot set O_NONBLOCK: %m");
/* auth.c */
+#define DEFAULT_SALT_BYTES 8
+#define DEFAULT_IDENT_BYTES 2
+#define DEFAULT_GENERATED_BYTES 8
+#define HASH_BYTES 32 // We are using SHA-256
+#define DEFAULT_HASH_ITERATIONS 64 // Number of hash function iterations per PBKDF2
+#define MAX_TEXT_HASH_SIZE 256
+
struct auth_zone {
cnode n;
char *name;
};
enum token_type {
+ TOKEN_UNDEFINED,
TOKEN_PASSWORD,
TOKEN_GENERATED,
TOKEN_NUM_TYPES,
enum token_type type;
char *salt;
char *hash;
+ char *ident;
char *comment;
time_t last_modified;
+ uint iterations;
};
void auth_init(void);
struct auth_zone *auth_find_zone(const char *name);
struct auth_user *auth_find_user(const char *login, bool create);
struct auth_acct *auth_find_acct(struct auth_user *au, struct auth_zone *az, bool create);
+struct auth_token *auth_find_token_passwd(struct auth_acct *aa);
+struct auth_token *auth_find_token_generated(struct auth_acct *aa, char *ident);
void auth_delete_user(struct auth_user *au);
void auth_delete_acct(struct auth_acct *aa);
void auth_delete_token(struct auth_token *at);
struct auth_token *auth_create_token(struct auth_acct *aa);
void auth_set_token_passwd(struct auth_token *at, const char *passwd);
+char *auth_set_token_generated(struct auth_token *at, const char *comment);
+bool auth_check_token(struct auth_token *at, const char *passwd);
+
+extern struct auth_token *auth_fake_token;