diff --git a/common/rpc-service.c b/common/rpc-service.c index b86dbbe..b7633b0 100644 --- a/common/rpc-service.c +++ b/common/rpc-service.c @@ -15,6 +15,7 @@ #include "seafile-error.h" #include "seafile-rpc.h" #include "mq-mgr.h" +#include "password-hash.h" #ifdef SEAFILE_SERVER #include "web-accesstoken-mgr.h" @@ -1131,6 +1132,181 @@ out: return ret; } +static void +set_pwd_hash_to_commit (SeafCommit *commit, + SeafRepo *repo, + const char *pwd_hash, + const char *pwd_hash_algo, + const char *pwd_hash_params) +{ + commit->repo_name = g_strdup (repo->name); + commit->repo_desc = g_strdup (repo->desc); + commit->encrypted = repo->encrypted; + commit->repaired = repo->repaired; + if (commit->encrypted) { + commit->enc_version = repo->enc_version; + if (commit->enc_version == 2) { + commit->random_key = g_strdup (repo->random_key); + } else if (commit->enc_version == 3) { + commit->random_key = g_strdup (repo->random_key); + commit->salt = g_strdup (repo->salt); + } else if (commit->enc_version == 4) { + commit->random_key = g_strdup (repo->random_key); + commit->salt = g_strdup (repo->salt); + } + commit->pwd_hash = g_strdup (pwd_hash); + commit->pwd_hash_algo = g_strdup (pwd_hash_algo); + commit->pwd_hash_params = g_strdup (pwd_hash_params); + } + commit->no_local_history = repo->no_local_history; + commit->version = repo->version; +} + +int +seafile_upgrade_repo_enc_algorithm (const char *repo_id, + const char *user, + const char *passwd, + const char *pwd_hash_algo, + const char *pwd_hash_params, + GError **error) +{ + SeafRepo *repo = NULL; + SeafCommit *commit = NULL, *parent = NULL; + int ret = 0; + + if (!user) { + g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, + "No user given"); + return -1; + } + + if (!passwd || passwd[0] == 0) { + g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, + "Empty passwd"); + return -1; + } + + if (!is_uuid_valid (repo_id)) { + g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Invalid repo id"); + return -1; + } + + if (!pwd_hash_algo) { + g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Invalid pwd hash algorithm"); + return -1; + } + + if (g_strcmp0 (pwd_hash_algo, PWD_HASH_PDKDF2) != 0 && + g_strcmp0 (pwd_hash_algo, PWD_HASH_ARGON2ID) != 0) { + g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Unsupported pwd hash algorithm"); + return -1; + } + + if (!pwd_hash_params) { + g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Invalid pwd hash params"); + return -1; + } + +retry: + repo = seaf_repo_manager_get_repo (seaf->repo_mgr, repo_id); + if (!repo) { + g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "No such library"); + return -1; + } + + if (g_strcmp0 (pwd_hash_algo, repo->pwd_hash_algo) == 0 && + g_strcmp0 (pwd_hash_params, repo->pwd_hash_params) == 0) { + goto out; + } + + if (!repo->encrypted) { + g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Repo not encrypted"); + ret = -1; + goto out; + } + + if (repo->pwd_hash_algo) { + if (seafile_pwd_hash_verify_repo_passwd (repo->enc_version, repo_id, passwd, repo->salt, + repo->pwd_hash, repo->pwd_hash_algo, repo->pwd_hash_params) < 0) { + g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Incorrect password"); + ret = 1; + goto out; + } + } else { + if (seafile_verify_repo_passwd (repo_id, passwd, repo->magic, + repo->enc_version, repo->salt) < 0) { + g_set_error (error, SEAFILE_DOMAIN, SEAF_ERR_BAD_ARGS, "Incorrect password"); + ret = -1; + goto out; + } + } + + parent = seaf_commit_manager_get_commit (seaf->commit_mgr, + repo->id, repo->version, + repo->head->commit_id); + if (!parent) { + seaf_warning ("Failed to get commit %s:%s.\n", + repo->id, repo->head->commit_id); + ret = -1; + goto out; + } + + char new_pwd_hash[65]= {0}; + + seafile_generate_pwd_hash (repo->enc_version, repo_id, passwd, repo->salt, + pwd_hash_algo, pwd_hash_params, new_pwd_hash); + + // To prevent clients that have already synced this repo from overwriting the modified encryption algorithm, + // delete all sync tokens. + if (seaf_delete_repo_tokens (repo) < 0) { + seaf_warning ("Failed to delete repo sync tokens, abort change pwd hash algorithm.\n"); + ret = -1; + goto out; + } + + memcpy (repo->pwd_hash, new_pwd_hash, 64); + + commit = seaf_commit_new (NULL, + repo->id, + parent->root_id, + user, + EMPTY_SHA1, + "Changed library password hash algorithm", + 0); + commit->parent_id = g_strdup(parent->commit_id); + set_pwd_hash_to_commit (commit, repo, new_pwd_hash, pwd_hash_algo, pwd_hash_params); + + if (seaf_commit_manager_add_commit (seaf->commit_mgr, commit) < 0) { + ret = -1; + goto out; + } + + seaf_branch_set_commit (repo->head, commit->commit_id); + if (seaf_branch_manager_test_and_update_branch (seaf->branch_mgr, + repo->head, + parent->commit_id, + FALSE, NULL, NULL, NULL) < 0) { + seaf_repo_unref (repo); + seaf_commit_unref (commit); + seaf_commit_unref (parent); + repo = NULL; + commit = NULL; + parent = NULL; + goto retry; + } + + if (seaf_passwd_manager_is_passwd_set (seaf->passwd_mgr, repo_id, user)) + seaf_passwd_manager_set_passwd (seaf->passwd_mgr, repo_id, + user, passwd, error); + +out: + seaf_commit_unref (commit); + seaf_commit_unref (parent); + seaf_repo_unref (repo); + + return ret; +} + int seafile_is_repo_owner (const char *email, const char *repo_id, diff --git a/common/seaf-utils.c b/common/seaf-utils.c index 0a00b84..32ca1af 100644 --- a/common/seaf-utils.c +++ b/common/seaf-utils.c @@ -512,3 +512,75 @@ split_filename (const char *filename, char **name, char **ext) *ext = NULL; } } + +static gboolean +collect_token_list (SeafDBRow *row, void *data) +{ + GList **p_tokens = data; + const char *token; + + token = seaf_db_row_get_column_text (row, 0); + *p_tokens = g_list_prepend (*p_tokens, g_strdup(token)); + + return TRUE; +} + +int +seaf_delete_repo_tokens (SeafRepo *repo) +{ + int ret = 0; + const char *template; + GList *token_list = NULL; + GList *ptr; + GString *token_list_str = g_string_new (""); + GString *sql = g_string_new (""); + int rc; + + template = "SELECT u.token FROM RepoUserToken as u WHERE u.repo_id=?"; + rc = seaf_db_statement_foreach_row (seaf->db, template, + collect_token_list, &token_list, + 1, "string", repo->id); + if (rc < 0) { + goto out; + } + + if (rc == 0) + goto out; + + for (ptr = token_list; ptr; ptr = ptr->next) { + const char *token = (char *)ptr->data; + seaf_message ("delete token: %s\n", token); + if (token_list_str->len == 0) + g_string_append_printf (token_list_str, "'%s'", token); + else + g_string_append_printf (token_list_str, ",'%s'", token); + } + + /* Note that there is a size limit on sql query. In MySQL it's 1MB by default. + * Normally the token_list won't be that long. + */ + g_string_printf (sql, "DELETE FROM RepoUserToken WHERE token in (%s)", + token_list_str->str); + rc = seaf_db_statement_query (seaf->db, sql->str, 0); + if (rc < 0) { + goto out; + } + + g_string_printf (sql, "DELETE FROM RepoTokenPeerInfo WHERE token in (%s)", + token_list_str->str); + rc = seaf_db_statement_query (seaf->db, sql->str, 0); + if (rc < 0) { + goto out; + } + +out: + g_string_free (token_list_str, TRUE); + g_string_free (sql, TRUE); + g_list_free_full (token_list, (GDestroyNotify)g_free); + + if (rc < 0) { + ret = -1; + } + + return ret; +} diff --git a/common/seaf-utils.h b/common/seaf-utils.h index 2401bed..46f62bc 100644 --- a/common/seaf-utils.h +++ b/common/seaf-utils.h @@ -28,4 +28,7 @@ seaf_parse_auth_token (const char *auth_token); void split_filename (const char *filename, char **name, char **ext); +int +seaf_delete_repo_tokens (SeafRepo *repo); + #endif diff --git a/include/seafile-rpc.h b/include/seafile-rpc.h index 6d6bd4d..b987852 100644 --- a/include/seafile-rpc.h +++ b/include/seafile-rpc.h @@ -150,6 +150,14 @@ seafile_change_repo_passwd (const char *repo_id, const char *user, GError **error); +int +seafile_upgrade_repo_enc_algorithm (const char *repo_id, + const char *user, + const char *passwd, + const char *pwd_hash_algo, + const char *pwd_hash_params, + GError **error); + /** * seafile_repo_size: * diff --git a/python/seafile/rpcclient.py b/python/seafile/rpcclient.py index f73e40d..400a14d 100644 --- a/python/seafile/rpcclient.py +++ b/python/seafile/rpcclient.py @@ -640,6 +640,12 @@ class SeafServerThreadedRpcClient(NamedPipeClient): pass change_repo_passwd = seafile_change_repo_passwd + # Upgrade repo enc algorithm + @searpc_func("int", ["string", "string", "string", "string", "string"]) + def seafile_upgrade_repo_enc_algorithm (repo_id, user, passwd, pwd_hash_algo, pwd_hash_params): + pass + upgrade_repo_enc_algorithm = seafile_upgrade_repo_enc_algorithm + # Clean trash @searpc_func("int", ["string", "int"]) def clean_up_repo_history(repo_id, keep_days): diff --git a/python/seaserv/api.py b/python/seaserv/api.py index 78309bb..381a780 100644 --- a/python/seaserv/api.py +++ b/python/seaserv/api.py @@ -72,6 +72,10 @@ class SeafileAPI(object): def change_repo_passwd(self, repo_id, old_passwd, new_passwd, user): return seafserv_threaded_rpc.change_repo_passwd(repo_id, old_passwd, new_passwd, user) + + def upgrade_repo_enc_algorithm (self, repo_id, user, passwd, pwd_hash_algo, pwd_hash_params): + return seafserv_threaded_rpc.upgrade_repo_enc_algorithm (repo_id, user, passwd, + pwd_hash_algo, pwd_hash_params) def check_passwd(self, repo_id, magic): return seafserv_threaded_rpc.check_passwd(repo_id, magic) diff --git a/server/gc/fsck.c b/server/gc/fsck.c index 1a1f298..ec2a793 100644 --- a/server/gc/fsck.c +++ b/server/gc/fsck.c @@ -281,77 +281,6 @@ out: return dir_id; } -static gboolean -collect_token_list (SeafDBRow *row, void *data) -{ - GList **p_tokens = data; - const char *token; - - token = seaf_db_row_get_column_text (row, 0); - *p_tokens = g_list_prepend (*p_tokens, g_strdup(token)); - - return TRUE; -} - -int -delete_repo_tokens (SeafRepo *repo) -{ - int ret = 0; - const char *template; - GList *token_list = NULL; - GList *ptr; - GString *token_list_str = g_string_new (""); - GString *sql = g_string_new (""); - int rc; - - template = "SELECT u.token FROM RepoUserToken as u WHERE u.repo_id=?"; - rc = seaf_db_statement_foreach_row (seaf->db, template, - collect_token_list, &token_list, - 1, "string", repo->id); - if (rc < 0) { - goto out; - } - - if (rc == 0) - goto out; - - for (ptr = token_list; ptr; ptr = ptr->next) { - const char *token = (char *)ptr->data; - if (token_list_str->len == 0) - g_string_append_printf (token_list_str, "'%s'", token); - else - g_string_append_printf (token_list_str, ",'%s'", token); - } - - /* Note that there is a size limit on sql query. In MySQL it's 1MB by default. - * Normally the token_list won't be that long. - */ - g_string_printf (sql, "DELETE FROM RepoUserToken WHERE token in (%s)", - token_list_str->str); - rc = seaf_db_statement_query (seaf->db, sql->str, 0); - if (rc < 0) { - goto out; - } - - g_string_printf (sql, "DELETE FROM RepoTokenPeerInfo WHERE token in (%s)", - token_list_str->str); - rc = seaf_db_statement_query (seaf->db, sql->str, 0); - if (rc < 0) { - goto out; - } - -out: - g_string_free (token_list_str, TRUE); - g_string_free (sql, TRUE); - g_list_free_full (token_list, (GDestroyNotify)g_free); - - if (rc < 0) { - ret = -1; - } - - return ret; -} - static char * gen_repair_commit_desc (GList *repaired_files, GList *repaired_folders) { @@ -385,7 +314,7 @@ static void reset_commit_to_repair (SeafRepo *repo, SeafCommit *parent, char *new_root_id, GList *repaired_files, GList *repaired_folders) { - if (delete_repo_tokens (repo) < 0) { + if (seaf_delete_repo_tokens (repo) < 0) { seaf_warning ("Failed to delete repo sync tokens, abort repair.\n"); return; } diff --git a/server/seaf-server.c b/server/seaf-server.c index eeabc04..8847834 100644 --- a/server/seaf-server.c +++ b/server/seaf-server.c @@ -120,6 +120,11 @@ static void start_rpc_service (const char *seafile_dir, "seafile_change_repo_passwd", searpc_signature_int__string_string_string_string()); + searpc_server_register_function ("seafserv-threaded-rpcserver", + seafile_upgrade_repo_enc_algorithm, + "seafile_upgrade_repo_enc_algorithm", + searpc_signature_int__string_string_string_string_string()); + searpc_server_register_function ("seafserv-threaded-rpcserver", seafile_is_repo_owner, "seafile_is_repo_owner", diff --git a/tests/test_password/test_password.py b/tests/test_password/test_password.py index 966ed61..09d799f 100644 --- a/tests/test_password/test_password.py +++ b/tests/test_password/test_password.py @@ -79,3 +79,36 @@ def test_pwd_hash(rpc, enc_version, algo, params): assert api.is_password_set(repo.id, USER) == 0 api.remove_repo(repo_id) + +@pytest.mark.parametrize('enc_version, algo, params', + [(2, 'pbkdf2_sha256', '1000'), (3, 'pbkdf2_sha256', '1000'), ( 4, 'pbkdf2_sha256', '1000'), + (2, 'argon2id', '2,102400,8'), (3, 'argon2id', '2,102400,8'), (4, 'argon2id', '2,102400,8')]) +def test_upgrade_pwd_hash(enc_version, algo, params): + test_repo_name = 'test_enc_repo' + test_repo_desc = 'test_enc_repo' + test_repo_passwd = 'test_enc_repo' + repo_id = api.create_repo(test_repo_name, test_repo_desc, USER, + test_repo_passwd, enc_version) + assert repo_id + + repo = api.get_repo(repo_id) + assert repo + assert repo.enc_version == enc_version + assert len(repo.random_key) == 96 + if enc_version > 2: + assert len(repo.salt) == 64 + + api.upgrade_repo_enc_algorithm (repo.repo_id, USER, test_repo_passwd, algo, params) == 0 + + repo = api.get_repo(repo_id) + assert repo.pwd_hash_algo == algo; + assert repo.pwd_hash_params == params; + assert repo.pwd_hash + + assert api.set_passwd(repo.id, USER, test_repo_passwd) == 0 + assert api.get_decrypt_key(repo.id, USER) + assert api.is_password_set(repo.id, USER) + assert api.unset_passwd(repo.id, USER) == 0 + assert api.is_password_set(repo.id, USER) == 0 + + api.remove_repo(repo_id)