diff --git a/changelogs/unreleased/support-md5-authentication b/changelogs/unreleased/support-md5-authentication
new file mode 100644
index 0000000000000000000000000000000000000000..471456d423e365d46c7f449d767941a87cbd1efe
--- /dev/null
+++ b/changelogs/unreleased/support-md5-authentication
@@ -0,0 +1,5 @@
+## feat/box
+* Support md5 authentication method.
+  It prevents password sniffing and avoids storing passwords on the
+  server in plain text but provides no protection if an attacker
+  manages to steal the password hash from the server.
diff --git a/src/box/CMakeLists.txt b/src/box/CMakeLists.txt
index 0532fb10059888859539fc23decaa84726fa5dcb..90164a0ea31a785769a983aa8796ec94dd42a4a3 100644
--- a/src/box/CMakeLists.txt
+++ b/src/box/CMakeLists.txt
@@ -203,6 +203,7 @@ set(box_sources
     user.cc
     authentication.c
     auth_chap_sha1.c
+    auth_md5.c
     replication.cc
     recovery.cc
     xstream.cc
diff --git a/src/box/auth_md5.c b/src/box/auth_md5.c
new file mode 100644
index 0000000000000000000000000000000000000000..17ce39087ccb323cd098b0088d00b416a6cf4f53
--- /dev/null
+++ b/src/box/auth_md5.c
@@ -0,0 +1,245 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright 2010-2022, Tarantool AUTHORS, please see AUTHORS file.
+ */
+#include "auth_md5.h"
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "authentication.h"
+#include "crypt.h"
+#include "diag.h"
+#include "errcode.h"
+#include "error.h"
+#include "fiber.h"
+#include "msgpuck.h"
+#include "small/region.h"
+#include "trivia/util.h"
+
+/**
+ * These are the core bits of the md5 authentication.
+ * The algorithm is the same as postgres md5 authentication:
+ *
+ * SERVER:  shadow_pass = md5(password, user)
+ *          salt = create_random_string()
+ *          send(salt)
+ *
+ * CLIENT:  recv(salt)
+ *          shadow_pass = md5(password, user)
+ *          client_pass = md5(shadow_pass, salt)
+ *          send(client_pass)
+ *
+ * SERVER:  recv(client_pass)
+ *          check(client_pass == md5(shadow_pass, salt))
+ */
+
+#define AUTH_MD5_NAME "md5"
+
+/** md5 authenticator implementation. */
+struct auth_md5_authenticator {
+	/** Base class. */
+	struct authenticator base;
+	/** md5(password, user). */
+	char shadow_pass[MD5_PASSWD_LEN];
+};
+
+/**
+ * Prepare a client password to send over the wire to the server
+ * for authentication. password may be not null-terminated.
+ * client_pass and salt sizes are regulated by MD5_PASSWD_LEN and MD5_SALT_LEN.
+ * Never fails.
+ */
+static void
+client_password_prepare(char *client_pass,
+			const void *password, size_t password_len,
+			const char *user,
+			const char *salt)
+{
+	char shadow_pass[MD5_PASSWD_LEN];
+	md5_encrypt(password, password_len, user, strlen(user), shadow_pass);
+	md5_encrypt(shadow_pass + strlen("md5"), MD5_PASSWD_LEN - strlen("md5"),
+		    salt, MD5_SALT_LEN, client_pass);
+}
+
+/**
+ * Verify a password.
+ * salt size must be at least MD5_SALT_LEN.
+ *
+ * @retval true passwords match
+ * @retval false passwords do not match or error occurred
+ */
+static bool
+client_password_check(const char *client_pass,
+		      const char *shadow_pass,
+		      const char *salt)
+{
+	char candidate[MD5_PASSWD_LEN];
+	/* Compute the correct answer for the MD5 challenge. */
+	md5_encrypt(shadow_pass + strlen("md5"), MD5_PASSWD_LEN - strlen("md5"),
+		    salt, MD5_SALT_LEN, candidate);
+	return memcmp(client_pass, candidate, MD5_PASSWD_LEN) == 0;
+}
+
+/** auth_method::auth_method_delete */
+static void
+auth_md5_delete(struct auth_method *method)
+{
+	TRASH(method);
+	free(method);
+}
+
+/** auth_method::auth_data_prepare */
+static void
+auth_md5_data_prepare(const struct auth_method *method,
+		      const char *password, int password_len,
+		      const char *user,
+		      const char **auth_data,
+		      const char **auth_data_end)
+{
+	(void)method;
+	struct region *region = &fiber()->gc;
+	size_t size = mp_sizeof_str(MD5_PASSWD_LEN);
+	char *p = xregion_alloc(region, size);
+	*auth_data = p;
+	*auth_data_end = p + size;
+	char *shadow_pass = mp_encode_strl(p, MD5_PASSWD_LEN);
+
+	md5_encrypt(password, password_len, user, strlen(user), shadow_pass);
+}
+
+/** auth_method::auth_request_prepare */
+static void
+auth_md5_request_prepare(const struct auth_method *method,
+			 const char *password, int password_len,
+			 const char *user,
+			 const char *salt,
+			 const char **auth_request,
+			 const char **auth_request_end)
+{
+	(void)method;
+	struct region *region = &fiber()->gc;
+	size_t size = mp_sizeof_str(MD5_PASSWD_LEN);
+	char *p = xregion_alloc(region, size);
+	*auth_request = p;
+	*auth_request_end = p + size;
+	char *client_password = mp_encode_strl(p, MD5_PASSWD_LEN);
+	client_password_prepare(client_password,
+				password, password_len, user, salt);
+}
+
+/** auth_method::auth_request_check */
+static int
+auth_md5_request_check(const struct auth_method *method,
+		       const char *auth_request,
+		       const char *auth_request_end)
+{
+	(void)method;
+	uint32_t client_pass_len;
+	if (mp_typeof(*auth_request) == MP_STR) {
+		client_pass_len = mp_decode_strl(&auth_request);
+	} else if (mp_typeof(*auth_request) == MP_BIN) {
+		/*
+		 * Password is not a character stream, so some codecs
+		 * automatically pack it as MP_BIN.
+		 */
+		client_pass_len = mp_decode_binl(&auth_request);
+	} else {
+		diag_set(ClientError, ER_INVALID_AUTH_REQUEST,
+			 AUTH_MD5_NAME, "client password must be a string");
+		return -1;
+	}
+	assert(auth_request + client_pass_len == auth_request_end);
+	(void)auth_request_end;
+	if (client_pass_len != MD5_PASSWD_LEN) {
+		diag_set(ClientError, ER_INVALID_AUTH_REQUEST,
+			 AUTH_MD5_NAME, "invalid client password size");
+		return -1;
+	}
+	return 0;
+}
+
+/** auth_method::authenticator_new */
+static struct authenticator *
+auth_md5_authenticator_new(const struct auth_method *method,
+			   const char *auth_data,
+			   const char *auth_data_end)
+{
+	if (mp_typeof(*auth_data) != MP_STR) {
+		diag_set(ClientError, ER_INVALID_AUTH_DATA,
+			 AUTH_MD5_NAME, "password must be a string");
+		return NULL;
+	}
+
+	uint32_t shadow_pass_len;
+	const char *shadow_pass = mp_decode_str(&auth_data, &shadow_pass_len);
+	assert(auth_data == auth_data_end);
+	(void)auth_data_end;
+
+	if (shadow_pass_len != MD5_PASSWD_LEN) {
+		diag_set(ClientError, ER_INVALID_AUTH_DATA,
+			 AUTH_MD5_NAME, "invalid shadow password size");
+		return NULL;
+	}
+
+	struct auth_md5_authenticator *auth = xcalloc(1, sizeof(*auth));
+	auth->base.method = method;
+	memcpy(auth->shadow_pass, shadow_pass, MD5_PASSWD_LEN);
+	return (struct authenticator *)auth;
+}
+
+/** auth_method::authenticator_delete */
+static void
+auth_md5_authenticator_delete(struct authenticator *auth_)
+{
+	struct auth_md5_authenticator *auth =
+		(struct auth_md5_authenticator *)auth_;
+	TRASH(auth);
+	free(auth);
+}
+
+/** auth_method::authenticator_check_request */
+static bool
+auth_md5_authenticate_request(const struct authenticator *auth_,
+			      const char *salt,
+			      const char *auth_request,
+			      const char *auth_request_end)
+{
+	const struct auth_md5_authenticator *auth =
+		(const struct auth_md5_authenticator *)auth_;
+	uint32_t client_pass_len;
+	const char *client_pass;
+	if (mp_typeof(*auth_request) == MP_STR) {
+		client_pass = mp_decode_str(&auth_request, &client_pass_len);
+	} else if (mp_typeof(*auth_request) == MP_BIN) {
+		client_pass = mp_decode_bin(&auth_request, &client_pass_len);
+	} else {
+		unreachable();
+	}
+
+	assert(auth_request == auth_request_end);
+	(void)auth_request_end;
+	assert(client_pass_len == MD5_PASSWD_LEN);
+	return client_password_check(client_pass, auth->shadow_pass, salt);
+}
+
+struct auth_method *
+auth_md5_new(void)
+{
+	struct auth_method *method = xmalloc(sizeof(*method));
+	method->name = AUTH_MD5_NAME;
+	method->flags = 0;
+	method->auth_method_delete = auth_md5_delete;
+	method->auth_method_delete = auth_md5_delete;
+	method->auth_data_prepare = auth_md5_data_prepare;
+	method->auth_request_prepare = auth_md5_request_prepare;
+	method->auth_request_check = auth_md5_request_check;
+	method->authenticator_new = auth_md5_authenticator_new;
+	method->authenticator_delete = auth_md5_authenticator_delete;
+	method->authenticate_request = auth_md5_authenticate_request;
+	return method;
+}
diff --git a/src/box/auth_md5.h b/src/box/auth_md5.h
new file mode 100644
index 0000000000000000000000000000000000000000..1c32009cf4b864e4fa509e41c84c1889412d027e
--- /dev/null
+++ b/src/box/auth_md5.h
@@ -0,0 +1,23 @@
+/*
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright 2010-2022, Tarantool AUTHORS, please see AUTHORS file.
+ */
+#pragma once
+
+#if defined(__cplusplus)
+extern "C" {
+#endif /* defined(__cplusplus) */
+
+struct auth_method;
+
+/**
+ * Allocates and initializes 'md5' authentication method.
+ * This function never fails.
+ */
+struct auth_method *
+auth_md5_new(void);
+
+#if defined(__cplusplus)
+} /* extern "C" */
+#endif /* defined(__cplusplus) */
diff --git a/src/box/authentication.c b/src/box/authentication.c
index fadd4defdc251460202419599c6d5d0170c55776..602e59d2934eb1a039f3d0e7f749e60d066d0b7b 100644
--- a/src/box/authentication.c
+++ b/src/box/authentication.c
@@ -13,6 +13,7 @@
 
 #include "assoc.h"
 #include "auth_chap_sha1.h"
+#include "auth_md5.h"
 #include "base64.h"
 #include "diag.h"
 #include "errcode.h"
@@ -169,9 +170,11 @@ void
 auth_init(void)
 {
 	auth_methods = mh_strnptr_new();
-	struct auth_method *method = auth_chap_sha1_new();
-	AUTH_METHOD_DEFAULT = method;
-	auth_method_register(method);
+	struct auth_method *chap_sha1_method = auth_chap_sha1_new();
+	AUTH_METHOD_DEFAULT = chap_sha1_method;
+	auth_method_register(chap_sha1_method);
+	struct auth_method *md5_method = auth_md5_new();
+	auth_method_register(md5_method);
 }
 
 void
diff --git a/src/lib/core/CMakeLists.txt b/src/lib/core/CMakeLists.txt
index f9cfdabae24ee724a983cae87b357eb3bf44ef32..9a32e1969e1e26ba67c30874197fbdb3a320893b 100644
--- a/src/lib/core/CMakeLists.txt
+++ b/src/lib/core/CMakeLists.txt
@@ -45,6 +45,9 @@ set(core_sources
     mp_util.c
     cord_on_demand.cc
     tweaks.c
+    md5.c
+    cryptohash.c
+    crypt.c
 )
 
 if (ENABLE_BACKTRACE)
diff --git a/src/lib/core/crypt.c b/src/lib/core/crypt.c
new file mode 100644
index 0000000000000000000000000000000000000000..412e70a8cbea52f4d6f49f0e926d9ffa01fc5a97
--- /dev/null
+++ b/src/lib/core/crypt.c
@@ -0,0 +1,98 @@
+/**
+ * PostgreSQL Database Management System
+ * (formerly known as Postgres, then as Postgres95)
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * Portions Copyright (c) 1994, The Regents of the University of California
+ *
+ * Permission to use, copy, modify, and distribute this software and its
+ * documentation for any purpose, without fee, and without a written agreement
+ * is hereby granted, provided that the above copyright notice and this
+ * paragraph and the following two paragraphs appear in all copies.
+ *
+ * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR
+ * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING
+ * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
+ * DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS
+ * ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO
+ * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+ */
+#include "crypt.h"
+#include "md5.h"
+#include "cryptohash.h"
+#include "trivia/util.h"
+#include "diag.h"
+
+#include <string.h>
+
+/** Convert bytes to hex values.  */
+static void
+bytes_to_hex(uint8_t b[16], char *s)
+{
+	static const char *hex = "0123456789abcdef";
+	int q, w;
+
+	for (q = 0, w = 0; q < 16; q++) {
+		s[w++] = hex[(b[q] >> 4) & 0x0F];
+		s[w++] = hex[b[q] & 0x0F];
+	}
+	s[w] = '\0';
+}
+
+/**
+ * Calculates the MD5 sum of the bytes in a buffer.
+ *
+ * @param buff the buffer containing the bytes that you want the MD5 sum of.
+ * @param len  number of bytes in the buffer.
+ *
+ * @param hexsum the MD5 sum as a '\0'-terminated string of
+ *		 hexadecimal digits.  an MD5 sum is 16 bytes long.
+ *		 each byte is represented by two hexadecimal
+ *		 characters.  you thus need to provide an array
+ *		 of 33 characters, including the trailing '\0'.
+ *		 errstr  filled with a constant-string error message
+ *		 on failure return; NULL on success.
+ *
+ * @note MD5 is described in RFC 1321.
+ *
+ * @author Sverre H. Huseby <sverrehu@online.no>
+ *
+ */
+static void
+md5_hash(const void *buff, size_t len, char *hexsum)
+{
+	uint8_t sum[MD5_DIGEST_LENGTH];
+
+	struct cryptohash_ctx *ctx = cryptohash_create(CRYPTOHASH_MD5);
+	cryptohash_init(ctx);
+	cryptohash_update(ctx, buff, len);
+	cryptohash_final(ctx, sum, sizeof(sum));
+	bytes_to_hex(sum, hexsum);
+	cryptohash_free(ctx);
+}
+
+void
+md5_encrypt(const char *password, size_t password_len,
+	    const char *salt, size_t salt_len, char *buf)
+{
+	assert(password_len + salt_len > 0);
+
+	char *crypt_buf = xmalloc(password_len + salt_len);
+	/*
+	 * Place salt at the end because it may be known by users trying to
+	 * crack the MD5 output.
+	 */
+	memcpy(crypt_buf, password, password_len);
+	memcpy(crypt_buf + password_len, salt, salt_len);
+
+	memcpy(buf, "md5", strlen("md5"));
+	md5_hash(crypt_buf, password_len + salt_len, buf + strlen("md5"));
+
+	free(crypt_buf);
+}
diff --git a/src/lib/core/crypt.h b/src/lib/core/crypt.h
new file mode 100644
index 0000000000000000000000000000000000000000..86122ee00a86b8cb56a59c966e49c72134f70503
--- /dev/null
+++ b/src/lib/core/crypt.h
@@ -0,0 +1,44 @@
+/**
+ * PostgreSQL Database Management System
+ * (formerly known as Postgres, then as Postgres95)
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * Portions Copyright (c) 1994, The Regents of the University of California
+ *
+ * Permission to use, copy, modify, and distribute this software and its
+ * documentation for any purpose, without fee, and without a written agreement
+ * is hereby granted, provided that the above copyright notice and this
+ * paragraph and the following two paragraphs appear in all copies.
+ *
+ * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR
+ * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING
+ * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
+ * DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS
+ * ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO
+ * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+ */
+#pragma once
+
+#include <stddef.h>
+#include "md5.h"
+
+/**
+ * Computes MD5 checksum of a password followed by salt.
+ *
+ * @param password password to be encrypted (need not be null-terminated)
+ * @param password_len length of the password
+ * @param salt salt used for encryption (need not be null-terminated)
+ * @param salt_len len of salt string. May differ from MD5_SALT_LEN if
+ * user name is used as a salt when computing a shadow password.
+ * @param buf buffer for encrypted password with length of MD5_PASSWD_LEN.
+ * Output format is "md5" followed by a 32-hex-digit MD5 checksum.
+ */
+void
+md5_encrypt(const char *password, size_t password_len,
+	    const char *salt, size_t salt_len, char *buf);
diff --git a/src/lib/core/cryptohash.c b/src/lib/core/cryptohash.c
new file mode 100644
index 0000000000000000000000000000000000000000..d017ece27c18a4045a2dbfc6f1b217a660bb4f8a
--- /dev/null
+++ b/src/lib/core/cryptohash.c
@@ -0,0 +1,106 @@
+/**
+ * PostgreSQL Database Management System
+ * (formerly known as Postgres, then as Postgres95)
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * Portions Copyright (c) 1994, The Regents of the University of California
+ *
+ * Permission to use, copy, modify, and distribute this software and its
+ * documentation for any purpose, without fee, and without a written agreement
+ * is hereby granted, provided that the above copyright notice and this
+ * paragraph and the following two paragraphs appear in all copies.
+ *
+ * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR
+ * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING
+ * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
+ * DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS
+ * ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO
+ * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+ */
+#include "cryptohash.h"
+#include "md5.h"
+
+#include "trivia/util.h"
+
+/**
+ * Internal cryptohash_ctx structure.
+ * It may seem overcomplicated since only one method is supported at the moment,
+ * but more methods are planned to be added, for instance, SHA-256.
+ */
+struct cryptohash_ctx {
+	/* which algorithm to use */
+	enum cryptohash_type type;
+
+	/* algorithm context, depending on the type field */
+	union {
+		/* md5 context */
+		struct md5_ctx md5;
+	} data;
+};
+
+struct cryptohash_ctx *
+cryptohash_create(enum cryptohash_type type)
+{
+	/*
+	 * Note that this always allocates enough space for the largest hash.
+	 * The small extra amount of memory does not make it worth complicating
+	 * this code.
+	 */
+	struct cryptohash_ctx *ctx = xcalloc(1, sizeof(*ctx));
+	ctx->type = type;
+	return ctx;
+}
+
+void
+cryptohash_init(struct cryptohash_ctx *ctx)
+{
+	switch (ctx->type) {
+	case CRYPTOHASH_MD5:
+		md5_init(&ctx->data.md5);
+		break;
+	default:
+		unreachable();
+	}
+}
+
+void
+cryptohash_update(struct cryptohash_ctx *ctx, const uint8_t *data, size_t len)
+{
+	switch (ctx->type) {
+	case CRYPTOHASH_MD5:
+		md5_update(&ctx->data.md5, data, len);
+		break;
+	default:
+		unreachable();
+	}
+}
+
+void
+cryptohash_final(struct cryptohash_ctx *ctx, uint8_t *dest, size_t len)
+{
+	(void)len;
+	switch (ctx->type) {
+	case CRYPTOHASH_MD5:
+		assert(len >= MD5_DIGEST_LENGTH);
+		md5_final(&ctx->data.md5, dest);
+		break;
+	default:
+		unreachable();
+	}
+}
+
+void
+cryptohash_free(struct cryptohash_ctx *ctx)
+{
+	if (ctx == NULL)
+		return;
+
+	TRASH(ctx);
+	free(ctx);
+}
diff --git a/src/lib/core/cryptohash.h b/src/lib/core/cryptohash.h
new file mode 100644
index 0000000000000000000000000000000000000000..10269e2f627e283f67f084f873b2f37e741dae2c
--- /dev/null
+++ b/src/lib/core/cryptohash.h
@@ -0,0 +1,57 @@
+/**
+ * PostgreSQL Database Management System
+ * (formerly known as Postgres, then as Postgres95)
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * Portions Copyright (c) 1994, The Regents of the University of California
+ *
+ * Permission to use, copy, modify, and distribute this software and its
+ * documentation for any purpose, without fee, and without a written agreement
+ * is hereby granted, provided that the above copyright notice and this
+ * paragraph and the following two paragraphs appear in all copies.
+ *
+ * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR
+ * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING
+ * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
+ * DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS
+ * ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO
+ * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+ */
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+
+/** Type of cryptohash context. It is used by cryptohash_create. */
+enum cryptohash_type {
+	/** Context for MD5 algorithm. */
+	CRYPTOHASH_MD5 = 0,
+};
+
+struct cryptohash_ctx;
+
+/* Allocate a cryptohash context. Never fails. */
+struct cryptohash_ctx *
+cryptohash_create(enum cryptohash_type type);
+
+/** Initialize a cryptohash context. Never fails. */
+void
+cryptohash_init(struct cryptohash_ctx *ctx);
+
+/** Update a cryptohash context. Never fails. */
+void
+cryptohash_update(struct cryptohash_ctx *ctx, const uint8_t *data, size_t len);
+
+/** Finalize a cryptohash context. Never fails. */
+void
+cryptohash_final(struct cryptohash_ctx *ctx, uint8_t *dest, size_t len);
+
+/** Free cryptohash context resources. */
+void
+cryptohash_free(struct cryptohash_ctx *ctx);
diff --git a/src/lib/core/md5.c b/src/lib/core/md5.c
new file mode 100644
index 0000000000000000000000000000000000000000..1e98490937739397e7ba4a160b1ebd9ebf6f0d74
--- /dev/null
+++ b/src/lib/core/md5.c
@@ -0,0 +1,298 @@
+/**
+ * PostgreSQL Database Management System
+ * (formerly known as Postgres, then as Postgres95)
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * Portions Copyright (c) 1994, The Regents of the University of California
+ *
+ * Permission to use, copy, modify, and distribute this software and its
+ * documentation for any purpose, without fee, and without a written agreement
+ * is hereby granted, provided that the above copyright notice and this
+ * paragraph and the following two paragraphs appear in all copies.
+ *
+ * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR
+ * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING
+ * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
+ * DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS
+ * ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO
+ * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+ */
+#include "md5.h"
+
+#include "trivia/util.h"
+
+#define md5_n    md5_count.md5_count64
+#define md5_n8   md5_count.md5_count8
+#define md5_sta  md5_st.md5_state32[0]
+#define md5_stb  md5_st.md5_state32[1]
+#define md5_stc  md5_st.md5_state32[2]
+#define md5_std  md5_st.md5_state32[3]
+#define md5_st8  md5_st.md5_state8
+
+#define SHIFT(X, s) (((X) << (s)) | ((X) >> (32 - (s))))
+
+#define F(X, Y, Z) (((X) & (Y)) | ((~X) & (Z)))
+#define G(X, Y, Z) (((X) & (Z)) | ((Y) & (~(Z))))
+#define H(X, Y, Z) ((X) ^ (Y) ^ (Z))
+#define I(X, Y, Z) ((Y) ^ ((X) | (~(Z))))
+
+#define ROUND1(a, b, c, d, k, s, i) \
+do { \
+	(a) = (a) + F((b), (c), (d)) + X[(k)] + T[(i)]; \
+	(a) = SHIFT((a), (s)); \
+	(a) = (a) + (b); \
+} while (0)
+
+#define ROUND2(a, b, c, d, k, s, i) \
+do { \
+	(a) = (a) + G((b), (c), (d)); \
+	(a) = (a) + X[(k)]; \
+	(a) = (a) + T[(i)]; \
+	(a) = SHIFT((a), (s)); \
+	(a) = (a) + (b); \
+} while (0)
+
+#define ROUND3(a, b, c, d, k, s, i) \
+do { \
+	(a) = (a) + H((b), (c), (d)); \
+	(a) = (a) + X[(k)]; \
+	(a) = (a) + T[(i)]; \
+	(a) = SHIFT((a), (s)); \
+	(a) = (a) + (b); \
+} while (0)
+
+#define ROUND4(a, b, c, d, k, s, i) \
+do { \
+	(a) = (a) + I((b), (c), (d)); \
+	(a) = (a) + X[(k)]; \
+	(a) = (a) + T[(i)]; \
+	(a) = SHIFT((a), (s)); \
+	(a) = (a) + (b); \
+} while (0)
+
+#define Sa	 7
+#define Sb	12
+#define Sc	17
+#define Sd	22
+
+#define Se	 5
+#define Sf	 9
+#define Sg	14
+#define Sh	20
+
+#define Si	 4
+#define Sj	11
+#define Sk	16
+#define Sl	23
+
+#define Sm	 6
+#define Sn	10
+#define So	15
+#define Sp	21
+
+#define MD5_A0	0x67452301
+#define MD5_B0	0xefcdab89
+#define MD5_C0	0x98badcfe
+#define MD5_D0	0x10325476
+
+/* Integer part of 4294967296 times abs(sin(i)), where i is in radians. */
+static const uint32_t T[65] = {
+	0,
+	0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
+	0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
+	0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
+	0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
+
+	0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
+	0xd62f105d, 0x2441453, 0xd8a1e681, 0xe7d3fbc8,
+	0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
+	0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
+
+	0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
+	0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
+	0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x4881d05,
+	0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
+
+	0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
+	0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
+	0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
+	0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391,
+};
+
+static const uint8_t md5_paddat[MD5_BUFLEN] = {
+	0x80, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0,
+	0, 0, 0, 0, 0, 0, 0, 0,
+};
+
+static_assert(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__,
+	      "md5 hash calculation only supports little endian architecture");
+
+/** Calculate a single md5 hash interation. */
+static void
+md5_calc(const uint8_t *b64, struct md5_ctx *ctx)
+{
+	uint32_t A = ctx->md5_sta;
+	uint32_t B = ctx->md5_stb;
+	uint32_t C = ctx->md5_stc;
+	uint32_t D = ctx->md5_std;
+
+	const uint32_t *X = (const uint32_t *)b64;
+	ROUND1(A, B, C, D, 0, Sa, 1);
+	ROUND1(D, A, B, C, 1, Sb, 2);
+	ROUND1(C, D, A, B, 2, Sc, 3);
+	ROUND1(B, C, D, A, 3, Sd, 4);
+	ROUND1(A, B, C, D, 4, Sa, 5);
+	ROUND1(D, A, B, C, 5, Sb, 6);
+	ROUND1(C, D, A, B, 6, Sc, 7);
+	ROUND1(B, C, D, A, 7, Sd, 8);
+	ROUND1(A, B, C, D, 8, Sa, 9);
+	ROUND1(D, A, B, C, 9, Sb, 10);
+	ROUND1(C, D, A, B, 10, Sc, 11);
+	ROUND1(B, C, D, A, 11, Sd, 12);
+	ROUND1(A, B, C, D, 12, Sa, 13);
+	ROUND1(D, A, B, C, 13, Sb, 14);
+	ROUND1(C, D, A, B, 14, Sc, 15);
+	ROUND1(B, C, D, A, 15, Sd, 16);
+
+	ROUND2(A, B, C, D, 1, Se, 17);
+	ROUND2(D, A, B, C, 6, Sf, 18);
+	ROUND2(C, D, A, B, 11, Sg, 19);
+	ROUND2(B, C, D, A, 0, Sh, 20);
+	ROUND2(A, B, C, D, 5, Se, 21);
+	ROUND2(D, A, B, C, 10, Sf, 22);
+	ROUND2(C, D, A, B, 15, Sg, 23);
+	ROUND2(B, C, D, A, 4, Sh, 24);
+	ROUND2(A, B, C, D, 9, Se, 25);
+	ROUND2(D, A, B, C, 14, Sf, 26);
+	ROUND2(C, D, A, B, 3, Sg, 27);
+	ROUND2(B, C, D, A, 8, Sh, 28);
+	ROUND2(A, B, C, D, 13, Se, 29);
+	ROUND2(D, A, B, C, 2, Sf, 30);
+	ROUND2(C, D, A, B, 7, Sg, 31);
+	ROUND2(B, C, D, A, 12, Sh, 32);
+
+	ROUND3(A, B, C, D, 5, Si, 33);
+	ROUND3(D, A, B, C, 8, Sj, 34);
+	ROUND3(C, D, A, B, 11, Sk, 35);
+	ROUND3(B, C, D, A, 14, Sl, 36);
+	ROUND3(A, B, C, D, 1, Si, 37);
+	ROUND3(D, A, B, C, 4, Sj, 38);
+	ROUND3(C, D, A, B, 7, Sk, 39);
+	ROUND3(B, C, D, A, 10, Sl, 40);
+	ROUND3(A, B, C, D, 13, Si, 41);
+	ROUND3(D, A, B, C, 0, Sj, 42);
+	ROUND3(C, D, A, B, 3, Sk, 43);
+	ROUND3(B, C, D, A, 6, Sl, 44);
+	ROUND3(A, B, C, D, 9, Si, 45);
+	ROUND3(D, A, B, C, 12, Sj, 46);
+	ROUND3(C, D, A, B, 15, Sk, 47);
+	ROUND3(B, C, D, A, 2, Sl, 48);
+
+	ROUND4(A, B, C, D, 0, Sm, 49);
+	ROUND4(D, A, B, C, 7, Sn, 50);
+	ROUND4(C, D, A, B, 14, So, 51);
+	ROUND4(B, C, D, A, 5, Sp, 52);
+	ROUND4(A, B, C, D, 12, Sm, 53);
+	ROUND4(D, A, B, C, 3, Sn, 54);
+	ROUND4(C, D, A, B, 10, So, 55);
+	ROUND4(B, C, D, A, 1, Sp, 56);
+	ROUND4(A, B, C, D, 8, Sm, 57);
+	ROUND4(D, A, B, C, 15, Sn, 58);
+	ROUND4(C, D, A, B, 6, So, 59);
+	ROUND4(B, C, D, A, 13, Sp, 60);
+	ROUND4(A, B, C, D, 4, Sm, 61);
+	ROUND4(D, A, B, C, 11, Sn, 62);
+	ROUND4(C, D, A, B, 2, So, 63);
+	ROUND4(B, C, D, A, 9, Sp, 64);
+
+	ctx->md5_sta += A;
+	ctx->md5_stb += B;
+	ctx->md5_stc += C;
+	ctx->md5_std += D;
+}
+
+/** Add padding to md5 buffer. */
+static void
+md5_pad(struct md5_ctx *ctx)
+{
+	unsigned int gap;
+
+	/* Don't count up padding. Keep md5_n. */
+	gap = MD5_BUFLEN - ctx->md5_i;
+	if (gap > 8) {
+		memmove(ctx->md5_buf + ctx->md5_i, md5_paddat,
+			gap - sizeof(ctx->md5_n));
+	} else {
+		/* including gap == 8 */
+		memmove(ctx->md5_buf + ctx->md5_i, md5_paddat, gap);
+		md5_calc(ctx->md5_buf, ctx);
+		memmove(ctx->md5_buf, md5_paddat + gap,
+			MD5_BUFLEN - sizeof(ctx->md5_n));
+	}
+
+	/* 8 byte word */
+	memmove(&ctx->md5_buf[56], &ctx->md5_n8[0], 8);
+
+	md5_calc(ctx->md5_buf, ctx);
+}
+
+static void
+md5_result(uint8_t *digest, struct md5_ctx *ctx)
+{
+	/* 4 byte words */
+	memmove(digest, &ctx->md5_st8[0], 16);
+}
+
+void
+md5_init(struct md5_ctx *ctx)
+{
+	ctx->md5_n = 0;
+	ctx->md5_i = 0;
+	ctx->md5_sta = MD5_A0;
+	ctx->md5_stb = MD5_B0;
+	ctx->md5_stc = MD5_C0;
+	ctx->md5_std = MD5_D0;
+	memset(ctx->md5_buf, 0, sizeof(ctx->md5_buf));
+}
+
+void
+md5_update(struct md5_ctx *ctx, const uint8_t *data, size_t len)
+{
+	unsigned int gap, i;
+
+	ctx->md5_n += len * 8;		/* byte to bit */
+	gap = MD5_BUFLEN - ctx->md5_i;
+
+	if (len >= gap) {
+		memmove(ctx->md5_buf + ctx->md5_i, data, gap);
+		md5_calc(ctx->md5_buf, ctx);
+
+		for (i = gap; i + MD5_BUFLEN <= len; i += MD5_BUFLEN)
+			md5_calc(data + i, ctx);
+
+		ctx->md5_i = len - i;
+		memmove(ctx->md5_buf, data + i, ctx->md5_i);
+	} else {
+		memmove(ctx->md5_buf + ctx->md5_i, data, len);
+		ctx->md5_i += len;
+	}
+}
+
+void
+md5_final(struct md5_ctx *ctx, uint8_t *dest)
+{
+	md5_pad(ctx);
+	md5_result(dest, ctx);
+}
diff --git a/src/lib/core/md5.h b/src/lib/core/md5.h
new file mode 100644
index 0000000000000000000000000000000000000000..03ae0f1705ee608c49bd3ac77fb6a05ccbe11b39
--- /dev/null
+++ b/src/lib/core/md5.h
@@ -0,0 +1,84 @@
+/**
+ * PostgreSQL Database Management System
+ * (formerly known as Postgres, then as Postgres95)
+ *
+ * Portions Copyright (c) 1996-2023, PostgreSQL Global Development Group
+ *
+ * Portions Copyright (c) 1994, The Regents of the University of California
+ *
+ * Permission to use, copy, modify, and distribute this software and its
+ * documentation for any purpose, without fee, and without a written agreement
+ * is hereby granted, provided that the above copyright notice and this
+ * paragraph and the following two paragraphs appear in all copies.
+ *
+ * IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR
+ * DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING
+ * LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS
+ * DOCUMENTATION, EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ * THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS FOR A PARTICULAR PURPOSE.  THE SOFTWARE PROVIDED HEREUNDER IS
+ * ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS TO
+ * PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
+ */
+#pragma once
+
+#include <stdint.h>
+#include <stddef.h>
+
+#include "box/authentication.h" /* for AUTH_SALT_SIZE */
+
+enum {
+	/* len of a buffer used by md5 hash context. */
+	MD5_BUFLEN = 64,
+	/* Size of result generated by MD5 computation */
+	MD5_DIGEST_LENGTH = 16,
+	/**
+	 * Length of a buffer that can store a password hashed by md5 algorithm.
+	 * "md5" + 32-hex-digit MD5 checksum.
+	 */
+	MD5_PASSWD_LEN = 3 + 32,
+	/** length of salt sent to remote side. */
+	MD5_SALT_LEN = 4
+};
+
+/** Allowed symbols set in md5 checksum. */
+#define MD5_PASSWD_CHARSET "0123456789abcdef"
+
+static_assert((int)MD5_SALT_LEN <= (int)AUTH_SALT_SIZE,
+	      "MD5 password salt is too big");
+
+/* Context data for MD5 */
+struct md5_ctx {
+	/** Current md5 state. */
+	union {
+		uint32_t md5_state32[4];
+		uint8_t md5_state8[16];
+	} md5_st;
+
+	/** md5 counter */
+	union {
+		/** counter as a 64-bit integer */
+		uint64_t md5_count64;
+		/** counter as an array of 1-byte integers */
+		uint8_t md5_count8[8];
+	} md5_count;
+
+	/** processed len of the buffer */
+	unsigned int md5_i;
+	uint8_t md5_buf[MD5_BUFLEN];
+};
+
+/** Initialize a MD5 context. */
+void
+md5_init(struct md5_ctx *ctx);
+
+/** Update a MD5 context. */
+void
+md5_update(struct md5_ctx *ctx, const uint8_t *data, size_t len);
+
+/** Finalize a MD5 context. */
+void
+md5_final(struct md5_ctx *ctx, uint8_t *dest);
diff --git a/test/box-luatest/md5_auth_test.lua b/test/box-luatest/md5_auth_test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..cbd813d1998f31e9397d7b53db09ffe61682228d
--- /dev/null
+++ b/test/box-luatest/md5_auth_test.lua
@@ -0,0 +1,107 @@
+local net = require('net.box')
+local server = require('luatest.server')
+local t = require('luatest')
+local urilib = require('uri')
+
+local g = t.group()
+
+g.before_all(function(cg)
+    cg.server = server:new({alias = 'master'})
+    cg.server:start()
+    cg.server:exec(function()
+        box.cfg{auth_type = 'md5'}
+        box.schema.user.create('test', {password = 'secret'})
+        box.session.su('admin', box.schema.user.grant, 'test', 'super')
+    end)
+end)
+
+g.after_all(function(cg)
+    cg.server:drop()
+end)
+
+g.test_box_cfg = function(cg)
+    cg.server:exec(function()
+        t.assert_equals(box.cfg.auth_type, 'md5')
+        t.assert_error_msg_equals(
+            "Incorrect value for option 'auth_type': should be of type string",
+            box.cfg, {auth_type = 42})
+        t.assert_error_msg_equals(
+            "Incorrect value for option 'auth_type': md55",
+            box.cfg, {auth_type = 'md55'})
+    end)
+end
+
+g.test_net_box = function(cg)
+    local parsed_uri = urilib.parse(cg.server.net_box_uri)
+    parsed_uri.login = 'test'
+    parsed_uri.password = 'secret'
+    parsed_uri.params = parsed_uri.params or {}
+    parsed_uri.params.auth_type = {'md6'}
+    local uri = urilib.format(parsed_uri, true)
+    t.assert_error_msg_equals(
+        "Unknown authentication method 'md6'",
+        net.connect, uri)
+    parsed_uri.params.auth_type = {'md5'}
+    uri = urilib.format(parsed_uri, true)
+    local conn = net.connect(cg.server.net_box_uri, uri)
+    t.assert_equals(conn.error, nil)
+    conn:close()
+    t.assert_error_msg_equals(
+        "Unknown authentication method 'md6'",
+        net.connect, uri, {auth_type = 'md6'})
+    t.assert_error_msg_equals(
+        "Unknown authentication method 'md6'",
+        net.connect, cg.server.net_box_uri, {
+            user = 'test',
+            password = 'secret',
+            auth_type = 'md6',
+        })
+    conn = net.connect(cg.server.net_box_uri, {
+        user = 'test',
+        password = 'secret',
+        auth_type = 'md5',
+    })
+    t.assert_equals(conn.error, nil)
+    conn:close()
+    conn = net.connect(cg.server.net_box_uri, {
+        user = 'test',
+        password = 'not-secret',
+        auth_type = 'md5',
+    })
+    t.assert_not_equals(conn.error, nil)
+end
+
+g.before_test('test_replication', function(cg)
+    cg.replica = server:new({
+        alias = 'replica',
+        box_cfg = {
+            replication = server.build_listen_uri('master', cg.server.id),
+        },
+    })
+    cg.replica:start()
+end)
+
+g.after_test('test_replication', function(cg)
+    cg.replica:drop()
+end)
+
+g.test_replication = function(cg)
+    cg.replica:exec(function(uri)
+        local urilib = require('uri')
+        local parsed_uri = urilib.parse(uri)
+        parsed_uri.login = 'test'
+        parsed_uri.password = 'secret'
+        parsed_uri.params = parsed_uri.params or {}
+        parsed_uri.params.auth_type = {'md6'}
+        uri = urilib.format(parsed_uri, true)
+        box.cfg({replication = {}})
+        t.assert_error_msg_matches(
+            "Incorrect value for option 'replication': " ..
+            "bad URI '.*%?auth_type=md6': " ..
+            "unknown authentication method",
+            box.cfg, {replication = uri})
+        parsed_uri.params.auth_type = {'md5'}
+        uri = urilib.format(parsed_uri, true)
+        box.cfg({replication = uri})
+    end, {server.build_listen_uri('master')})
+end