Unlocking LUKS volumes works
First complete technical round-trip complete, can unlock the LUKS volumes described in the server/client databases successfully.
This commit is contained in:
parent
849e3a5949
commit
3478fa4555
7
Makefile
7
Makefile
@ -10,6 +10,7 @@ CFLAGS += -ggdb3 -DDEBUG -fsanitize=address -fsanitize=undefined -fsanitize=leak
|
|||||||
PYPGMOPTS := ../Python/pypgmopts/pypgmopts
|
PYPGMOPTS := ../Python/pypgmopts/pypgmopts
|
||||||
|
|
||||||
LDFLAGS := `pkg-config --libs openssl`
|
LDFLAGS := `pkg-config --libs openssl`
|
||||||
|
TEST_PREFIX := local
|
||||||
|
|
||||||
OBJS := \
|
OBJS := \
|
||||||
argparse_client.o \
|
argparse_client.o \
|
||||||
@ -18,9 +19,11 @@ OBJS := \
|
|||||||
blacklist.o \
|
blacklist.o \
|
||||||
client.o \
|
client.o \
|
||||||
editor.o \
|
editor.o \
|
||||||
|
exec.o \
|
||||||
file_encryption.o \
|
file_encryption.o \
|
||||||
keydb.o \
|
keydb.o \
|
||||||
log.o \
|
log.o \
|
||||||
|
luks.o \
|
||||||
luksrku.o \
|
luksrku.o \
|
||||||
openssl.o \
|
openssl.o \
|
||||||
pgmopts.o \
|
pgmopts.o \
|
||||||
@ -45,10 +48,10 @@ clean:
|
|||||||
rm -f $(OBJS) $(OBJS_CFG) luksrku
|
rm -f $(OBJS) $(OBJS_CFG) luksrku
|
||||||
|
|
||||||
test_s: luksrku
|
test_s: luksrku
|
||||||
./luksrku server -vv testdata/server.bin
|
./luksrku server -vv testdata/$(TEST_PREFIX)_server.bin
|
||||||
|
|
||||||
test_c: luksrku
|
test_c: luksrku
|
||||||
./luksrku client -vv --no-luks testdata/client.bin
|
./luksrku client -vv --no-luks testdata/$(TEST_PREFIX)_client.bin
|
||||||
|
|
||||||
.c.o:
|
.c.o:
|
||||||
$(CC) $(CFLAGS) -c -o $@ $<
|
$(CC) $(CFLAGS) -c -o $@ $<
|
||||||
|
@ -31,14 +31,14 @@
|
|||||||
static struct blacklist_entry_t blacklist[BLACKLIST_ENTRY_COUNT];
|
static struct blacklist_entry_t blacklist[BLACKLIST_ENTRY_COUNT];
|
||||||
|
|
||||||
static bool blacklist_entry_expired(int index) {
|
static bool blacklist_entry_expired(int index) {
|
||||||
return now() > blacklist[index].entered + BLACKLIST_ENTRY_TIMEOUT_SECS;
|
return now() > blacklist[index].timeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
void blacklist_ip(uint32_t ip) {
|
void blacklist_ip(uint32_t ip, unsigned int timeout_seconds) {
|
||||||
for (int i = 0; i < BLACKLIST_ENTRY_COUNT; i++) {
|
for (int i = 0; i < BLACKLIST_ENTRY_COUNT; i++) {
|
||||||
if (blacklist_entry_expired(i)) {
|
if (blacklist_entry_expired(i)) {
|
||||||
blacklist[i].ip = ip;
|
blacklist[i].ip = ip;
|
||||||
blacklist[i].entered = now();
|
blacklist[i].timeout = now() + timeout_seconds;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,15 +28,14 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
#define BLACKLIST_ENTRY_COUNT 32
|
#define BLACKLIST_ENTRY_COUNT 32
|
||||||
#define BLACKLIST_ENTRY_TIMEOUT_SECS 15
|
|
||||||
|
|
||||||
struct blacklist_entry_t {
|
struct blacklist_entry_t {
|
||||||
uint32_t ip;
|
uint32_t ip;
|
||||||
double entered;
|
double timeout;
|
||||||
};
|
};
|
||||||
|
|
||||||
/*************** AUTO GENERATED SECTION FOLLOWS ***************/
|
/*************** AUTO GENERATED SECTION FOLLOWS ***************/
|
||||||
void blacklist_ip(uint32_t ip);
|
void blacklist_ip(uint32_t ip, unsigned int timeout_seconds);
|
||||||
bool is_ip_blacklisted(uint32_t ip);
|
bool is_ip_blacklisted(uint32_t ip);
|
||||||
/*************** AUTO GENERATED SECTION ENDS ***************/
|
/*************** AUTO GENERATED SECTION ENDS ***************/
|
||||||
|
|
||||||
|
71
client.c
71
client.c
@ -43,6 +43,7 @@
|
|||||||
#include "keydb.h"
|
#include "keydb.h"
|
||||||
#include "uuid.h"
|
#include "uuid.h"
|
||||||
#include "udp.h"
|
#include "udp.h"
|
||||||
|
#include "luks.h"
|
||||||
|
|
||||||
struct keyclient_t {
|
struct keyclient_t {
|
||||||
const struct pgmopts_client_t *opts;
|
const struct pgmopts_client_t *opts;
|
||||||
@ -60,17 +61,26 @@ static int psk_client_callback(SSL *ssl, const EVP_MD *md, const unsigned char *
|
|||||||
return openssl_tls13_psk_establish_session(ssl, key_client->keydb->hosts[0].tls_psk, PSK_SIZE_BYTES, EVP_sha256(), sessptr);
|
return openssl_tls13_psk_establish_session(ssl, key_client->keydb->hosts[0].tls_psk, PSK_SIZE_BYTES, EVP_sha256(), sessptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool do_unlock_luks_volume(const struct volume_entry_t *volume, const struct msg_t *unlock_msg) {
|
static bool unlock_luks_volume(const struct volume_entry_t *volume, const struct msg_t *unlock_msg) {
|
||||||
return true;
|
bool success = true;
|
||||||
|
char luks_passphrase[LUKS_PASSPHRASE_TEXT_SIZE_BYTES];
|
||||||
|
if (ascii_encode(luks_passphrase, sizeof(luks_passphrase), unlock_msg->luks_passphrase_raw, sizeof(unlock_msg->luks_passphrase_raw))) {
|
||||||
|
success = open_luks_device_pw(volume->volume_uuid, volume->devmapper_name, luks_passphrase, strlen(luks_passphrase));
|
||||||
|
} else {
|
||||||
|
log_msg(LLVL_FATAL, "Failed to transcribe raw LUKS passphrase to text form.");
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
OPENSSL_cleanse(luks_passphrase, sizeof(luks_passphrase));
|
||||||
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool unlock_luks_volume(struct keyclient_t *keyclient, const struct msg_t *unlock_msg) {
|
static bool attempt_unlock_luks_volume(struct keyclient_t *keyclient, const struct msg_t *unlock_msg) {
|
||||||
const struct host_entry_t *host = &keyclient->keydb->hosts[0];
|
const struct host_entry_t *host = &keyclient->keydb->hosts[0];
|
||||||
const struct volume_entry_t* volume = keydb_get_volume_by_uuid(host, unlock_msg->volume_uuid);
|
const struct volume_entry_t* volume = keydb_get_volume_by_uuid(host, unlock_msg->volume_uuid);
|
||||||
|
char volume_uuid_str[ASCII_UUID_BUFSIZE];
|
||||||
|
sprintf_uuid(volume_uuid_str, unlock_msg->volume_uuid);
|
||||||
if (!volume) {
|
if (!volume) {
|
||||||
char volume_uuid_str[ASCII_UUID_BUFSIZE];
|
log_msg(LLVL_WARNING, "Keyserver provided key for unlocking volume UUID %s, but this volume is not known on the client side.", volume_uuid_str);
|
||||||
sprintf_uuid(volume_uuid_str, unlock_msg->volume_uuid);
|
|
||||||
log_msg(LLVL_WARNING, "Keyserver provided key for unlocking volume UUID %s, but this volume does not need unlocking on the client side.", volume_uuid_str);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +90,16 @@ static bool unlock_luks_volume(struct keyclient_t *keyclient, const struct msg_t
|
|||||||
if (keyclient->opts->no_luks) {
|
if (keyclient->opts->no_luks) {
|
||||||
keyclient->volume_unlocked[volume_index] = true;
|
keyclient->volume_unlocked[volume_index] = true;
|
||||||
} else {
|
} else {
|
||||||
keyclient->volume_unlocked[volume_index] = do_unlock_luks_volume(volume, unlock_msg);
|
if (!keyclient->volume_unlocked[volume_index]) {
|
||||||
|
bool success = unlock_luks_volume(volume, unlock_msg);
|
||||||
|
keyclient->volume_unlocked[volume_index] = success;
|
||||||
|
if (!success) {
|
||||||
|
log_msg(LLVL_ERROR, "Unlocking of volume %s / %s failed with the server-provided passphrase.", volume->devmapper_name, volume_uuid_str);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_msg(LLVL_WARNING, "Volume %s / %s already unlocked, not attemping to unlock again.", volume->devmapper_name, volume_uuid_str);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log_msg(LLVL_FATAL, "Error calculating volume offset for volume %p from base %p.", volume, host->volumes);
|
log_msg(LLVL_FATAL, "Error calculating volume offset for volume %p from base %p.", volume, host->volumes);
|
||||||
@ -118,7 +137,7 @@ static bool contact_keyserver_socket(struct keyclient_t *keyclient, int sd) {
|
|||||||
char uuid_str[ASCII_UUID_BUFSIZE];
|
char uuid_str[ASCII_UUID_BUFSIZE];
|
||||||
sprintf_uuid(uuid_str, msg.volume_uuid);
|
sprintf_uuid(uuid_str, msg.volume_uuid);
|
||||||
log_msg(LLVL_TRACE, "Received LUKS key to unlock volume with UUID %s", uuid_str);
|
log_msg(LLVL_TRACE, "Received LUKS key to unlock volume with UUID %s", uuid_str);
|
||||||
if (unlock_luks_volume(keyclient, &msg)) {
|
if (attempt_unlock_luks_volume(keyclient, &msg)) {
|
||||||
log_msg(LLVL_DEBUG, "Successfully unlocked volume with UUID %s", uuid_str);
|
log_msg(LLVL_DEBUG, "Successfully unlocked volume with UUID %s", uuid_str);
|
||||||
} else {
|
} else {
|
||||||
log_msg(LLVL_ERROR, "Failed to unlocked volume with UUID %s", uuid_str);
|
log_msg(LLVL_ERROR, "Failed to unlocked volume with UUID %s", uuid_str);
|
||||||
@ -187,14 +206,19 @@ static bool contact_keyserver_hostname(struct keyclient_t *keyclient, const char
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool all_volumes_unlocked(struct keyclient_t *keyclient) {
|
static unsigned int locked_volume_count(struct keyclient_t *keyclient) {
|
||||||
|
unsigned int count = 0;
|
||||||
const unsigned int volume_count = keyclient->keydb->hosts[0].volume_count;
|
const unsigned int volume_count = keyclient->keydb->hosts[0].volume_count;
|
||||||
for (unsigned int i = 0; i < volume_count; i++) {
|
for (unsigned int i = 0; i < volume_count; i++) {
|
||||||
if (!keyclient->volume_unlocked[i]) {
|
if (!keyclient->volume_unlocked[i]) {
|
||||||
return false;
|
count++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool all_volumes_unlocked(struct keyclient_t *keyclient) {
|
||||||
|
return locked_volume_count(keyclient) == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool abort_searching_for_keyserver(struct keyclient_t *keyclient) {
|
static bool abort_searching_for_keyserver(struct keyclient_t *keyclient) {
|
||||||
@ -206,7 +230,7 @@ static bool abort_searching_for_keyserver(struct keyclient_t *keyclient) {
|
|||||||
if (keyclient->opts->timeout_seconds) {
|
if (keyclient->opts->timeout_seconds) {
|
||||||
double time_passed = now() - keyclient->broadcast_start_time;
|
double time_passed = now() - keyclient->broadcast_start_time;
|
||||||
if (time_passed >= keyclient->opts->timeout_seconds) {
|
if (time_passed >= keyclient->opts->timeout_seconds) {
|
||||||
log_msg(LLVL_WARNING, "Could not unlock all volumes after %u seconds, giving up.", keyclient->opts->timeout_seconds);
|
log_msg(LLVL_WARNING, "Could not unlock all volumes after %u seconds, giving up. %d volumes still locked.", keyclient->opts->timeout_seconds, locked_volume_count(keyclient));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,9 +258,14 @@ static bool broadcast_for_keyserver(struct keyclient_t *keyclient) {
|
|||||||
};
|
};
|
||||||
struct udp_response_t response;
|
struct udp_response_t response;
|
||||||
if (wait_udp_response(sd, &response, &src)) {
|
if (wait_udp_response(sd, &response, &src)) {
|
||||||
log_msg(LLVL_DEBUG, "Potential keyserver found at %d.%d.%d.%d", PRINTF_FORMAT_IP(&src));
|
if (!is_ip_blacklisted(src.sin_addr.s_addr)) {
|
||||||
if (!contact_keyserver_ipv4(keyclient, &src, keyclient->opts->port)) {
|
log_msg(LLVL_INFO, "Keyserver found at %d.%d.%d.%d", PRINTF_FORMAT_IP(&src));
|
||||||
log_msg(LLVL_WARNING, "Keyserver announced at %d.%d.%d.%d, but connection to it failed.", PRINTF_FORMAT_IP(&src));
|
blacklist_ip(src.sin_addr.s_addr, BLACKLIST_TIMEOUT_CLIENT);
|
||||||
|
if (!contact_keyserver_ipv4(keyclient, &src, keyclient->opts->port)) {
|
||||||
|
log_msg(LLVL_WARNING, "Keyserver announced at %d.%d.%d.%d, but connection to it failed.", PRINTF_FORMAT_IP(&src));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log_msg(LLVL_DEBUG, "Potential keyserver at %d.%d.%d.%d ignored, blacklist in effect.", PRINTF_FORMAT_IP(&src));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -281,10 +310,20 @@ bool keyclient_start(const struct pgmopts_client_t *opts) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Determine which of these volumes are already unlocked */
|
||||||
|
for (unsigned int i = 0; i < host->volume_count; i++) {
|
||||||
|
keyclient.volume_unlocked[i] = is_luks_device_opened(host->volumes[i].devmapper_name);
|
||||||
|
}
|
||||||
|
if (all_volumes_unlocked(&keyclient)) {
|
||||||
|
log_msg(LLVL_INFO, "All %u volumes are unlocked already, not contacting luksrku key server.", host->volume_count);
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
log_msg(LLVL_DEBUG, "%u of %u volumes are currently locked.", locked_volume_count(&keyclient), host->volume_count);
|
||||||
|
}
|
||||||
|
|
||||||
/* Transcribe the host UUID to ASCII so we only have to do this once */
|
/* Transcribe the host UUID to ASCII so we only have to do this once */
|
||||||
sprintf_uuid((char*)keyclient.identifier, host->host_uuid);
|
sprintf_uuid((char*)keyclient.identifier, host->host_uuid);
|
||||||
|
|
||||||
log_msg(LLVL_DEBUG, "Attempting to unlock %d volumes of host \"%s\".", host->volume_count, host->host_name);
|
|
||||||
if (opts->hostname) {
|
if (opts->hostname) {
|
||||||
if (!contact_keyserver_hostname(&keyclient, opts->hostname)) {
|
if (!contact_keyserver_hostname(&keyclient, opts->hostname)) {
|
||||||
log_msg(LLVL_ERROR, "Failed to contact key server: %s", opts->hostname);
|
log_msg(LLVL_ERROR, "Failed to contact key server: %s", opts->hostname);
|
||||||
|
4
global.h
4
global.h
@ -24,6 +24,10 @@
|
|||||||
#ifndef __GLOBAL_H__
|
#ifndef __GLOBAL_H__
|
||||||
#define __GLOBAL_H__
|
#define __GLOBAL_H__
|
||||||
|
|
||||||
|
/* Blacklisting timeouts in seconds */
|
||||||
|
#define BLACKLIST_TIMEOUT_CLIENT 3600
|
||||||
|
#define BLACKLIST_TIMEOUT_SERVER 15
|
||||||
|
|
||||||
/* Size in bytes of the PSK that is used for TLS */
|
/* Size in bytes of the PSK that is used for TLS */
|
||||||
#define PSK_SIZE_BYTES 32
|
#define PSK_SIZE_BYTES 32
|
||||||
|
|
||||||
|
9
luks.c
9
luks.c
@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
luksrku - Tool to remotely unlock LUKS disks using TLS.
|
luksrku - Tool to remotely unlock LUKS disks using TLS.
|
||||||
Copyright (C) 2016-2016 Johannes Bauer
|
Copyright (C) 2016-2019 Johannes Bauer
|
||||||
|
|
||||||
This file is part of luksrku.
|
This file is part of luksrku.
|
||||||
|
|
||||||
@ -34,6 +34,7 @@
|
|||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "exec.h"
|
#include "exec.h"
|
||||||
#include "util.h"
|
#include "util.h"
|
||||||
|
#include "uuid.h"
|
||||||
|
|
||||||
bool is_luks_device_opened(const char *mapping_name) {
|
bool is_luks_device_opened(const char *mapping_name) {
|
||||||
const char *command[] = {
|
const char *command[] = {
|
||||||
@ -42,7 +43,7 @@ bool is_luks_device_opened(const char *mapping_name) {
|
|||||||
mapping_name,
|
mapping_name,
|
||||||
NULL,
|
NULL,
|
||||||
};
|
};
|
||||||
struct runresult_t runresult = exec_command(command, should_log(LLVL_DEBUG));
|
struct runresult_t runresult = exec_command(command, should_log(LLVL_TRACE));
|
||||||
return runresult.success && (runresult.returncode == 0);
|
return runresult.success && (runresult.returncode == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +87,7 @@ static bool wipe_passphrase_file(const char *filename, int length) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char *write_passphrase_file(const uint8_t *passphrase, int passphrase_length) {
|
static const char *write_passphrase_file(const void *passphrase, int passphrase_length) {
|
||||||
//const char *filename = "/dev/shm/luksrku_passphrase.bin"; /* TODO make this variable */
|
//const char *filename = "/dev/shm/luksrku_passphrase.bin"; /* TODO make this variable */
|
||||||
const char *filename = "/tmp/luksrku_passphrase.bin";
|
const char *filename = "/tmp/luksrku_passphrase.bin";
|
||||||
int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0600);
|
int fd = open(filename, O_CREAT | O_WRONLY | O_TRUNC, 0600);
|
||||||
@ -106,7 +107,7 @@ static const char *write_passphrase_file(const uint8_t *passphrase, int passphra
|
|||||||
return filename;
|
return filename;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool open_luks_device_pw(const uint8_t *encrypted_device_uuid, const char *mapping_name, const uint8_t *passphrase, int passphrase_length) {
|
bool open_luks_device_pw(const uint8_t *encrypted_device_uuid, const char *mapping_name, const char *passphrase, unsigned int passphrase_length) {
|
||||||
const char *pw_filename = write_passphrase_file(passphrase, passphrase_length);
|
const char *pw_filename = write_passphrase_file(passphrase, passphrase_length);
|
||||||
if (!pw_filename) {
|
if (!pw_filename) {
|
||||||
return false;
|
return false;
|
||||||
|
2
luks.h
2
luks.h
@ -30,7 +30,7 @@
|
|||||||
/*************** AUTO GENERATED SECTION FOLLOWS ***************/
|
/*************** AUTO GENERATED SECTION FOLLOWS ***************/
|
||||||
bool is_luks_device_opened(const char *mapping_name);
|
bool is_luks_device_opened(const char *mapping_name);
|
||||||
bool open_luks_device(const uint8_t *encrypted_device_uuid, const char *mapping_name, const char *passphrase_file);
|
bool open_luks_device(const uint8_t *encrypted_device_uuid, const char *mapping_name, const char *passphrase_file);
|
||||||
bool open_luks_device_pw(const uint8_t *encrypted_device_uuid, const char *mapping_name, const uint8_t *passphrase, int passphrase_length);
|
bool open_luks_device_pw(const uint8_t *encrypted_device_uuid, const char *mapping_name, const char *passphrase, unsigned int passphrase_length);
|
||||||
/*************** AUTO GENERATED SECTION ENDS ***************/
|
/*************** AUTO GENERATED SECTION ENDS ***************/
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
2
server.c
2
server.c
@ -187,7 +187,7 @@ static void udp_handler_thread(void *vctx) {
|
|||||||
if (is_ip_blacklisted(ipv4)) {
|
if (is_ip_blacklisted(ipv4)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
blacklist_ip(ipv4);
|
blacklist_ip(ipv4, BLACKLIST_TIMEOUT_SERVER);
|
||||||
|
|
||||||
/* Check if we have this host in our database */
|
/* Check if we have this host in our database */
|
||||||
if (keydb_get_host_by_uuid(client->keydb, rx_msg.host_uuid)) {
|
if (keydb_get_host_by_uuid(client->keydb, rx_msg.host_uuid)) {
|
||||||
|
BIN
testdata/local_client.bin
vendored
Normal file
BIN
testdata/local_client.bin
vendored
Normal file
Binary file not shown.
BIN
testdata/local_server.bin
vendored
Normal file
BIN
testdata/local_server.bin
vendored
Normal file
Binary file not shown.
2
udp.c
2
udp.c
@ -75,10 +75,8 @@ int create_udp_socket(unsigned int listen_port, bool send_broadcast, unsigned in
|
|||||||
return sd;
|
return sd;
|
||||||
}
|
}
|
||||||
bool wait_udp_message(int sd, void *data, unsigned int length, struct sockaddr_in *source) {
|
bool wait_udp_message(int sd, void *data, unsigned int length, struct sockaddr_in *source) {
|
||||||
fprintf(stderr, "RECV...\n");
|
|
||||||
socklen_t socklen = sizeof(struct sockaddr_in);
|
socklen_t socklen = sizeof(struct sockaddr_in);
|
||||||
ssize_t rx_bytes = recvfrom(sd,data, length, 0, (struct sockaddr*)source, &socklen);
|
ssize_t rx_bytes = recvfrom(sd,data, length, 0, (struct sockaddr*)source, &socklen);
|
||||||
fprintf(stderr, "RECV %ld\n", rx_bytes);
|
|
||||||
return rx_bytes == length;
|
return rx_bytes == length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user