patch 9.0.1481: decrypting with libsodium may fail if the library changes

Problem:    Decrypting with libsodium may fail if the library changes.
Solution:   Add parameters used to the encrypted file header. (Christian
            Brabandt, closes #12279)
diff --git a/src/blowfish.c b/src/blowfish.c
index 88d4bce..6a33914 100644
--- a/src/blowfish.c
+++ b/src/blowfish.c
@@ -641,11 +641,8 @@
     int
 crypt_blowfish_init(
     cryptstate_T	*state,
-    char_u*		key,
-    char_u*		salt,
-    int			salt_len,
-    char_u*		seed,
-    int			seed_len)
+    char_u		*key,
+    crypt_arg_T		*arg)
 {
     bf_state_T	*bfs = ALLOC_CLEAR_ONE(bf_state_T);
 
@@ -660,8 +657,8 @@
     if (blowfish_self_test() == FAIL)
 	return FAIL;
 
-    bf_key_init(bfs, key, salt, salt_len);
-    bf_cfb_init(bfs, seed, seed_len);
+    bf_key_init(bfs, key, arg->cat_salt, arg->cat_salt_len);
+    bf_cfb_init(bfs, arg->cat_seed, arg->cat_seed_len);
 
     return OK;
 }
diff --git a/src/buffer.c b/src/buffer.c
index 6d4c435..98cca6c 100644
--- a/src/buffer.c
+++ b/src/buffer.c
@@ -2362,8 +2362,8 @@
 #endif
 #ifdef FEAT_CRYPT
 # ifdef FEAT_SODIUM
-    if ((buf->b_p_key != NULL) && (*buf->b_p_key != NUL) &&
-				(crypt_get_method_nr(buf) == CRYPT_M_SOD))
+    if (buf->b_p_key != NULL && *buf->b_p_key != NUL
+			   && crypt_method_is_sodium(crypt_get_method_nr(buf)))
 	crypt_sodium_munlock(buf->b_p_key, STRLEN(buf->b_p_key));
 # endif
     clear_string_option(&buf->b_p_key);
diff --git a/src/crypt.c b/src/crypt.c
index a66c004..ca7a3e7 100644
--- a/src/crypt.c
+++ b/src/crypt.c
@@ -34,6 +34,8 @@
     char    *magic;	// magic bytes stored in file header
     int	    salt_len;	// length of salt, or 0 when not using salt
     int	    seed_len;	// length of seed, or 0 when not using seed
+    int	    add_len;	// additional length in the header needed for storing
+			// custom data
 #ifdef CRYPT_NOT_INPLACE
     int	    works_inplace; // encryption/decryption can be done in-place
 #endif
@@ -44,7 +46,7 @@
 
     // Function pointer for initializing encryption/decryption.
     int (* init_fn)(cryptstate_T *state, char_u *key,
-		      char_u *salt, int salt_len, char_u *seed, int seed_len);
+		crypt_arg_T *arg);
 
     // Function pointers for encoding/decoding from one buffer into another.
     // Optional, however, these or the _buffer ones should be configured.
@@ -73,9 +75,12 @@
 							char_u *p2, int last);
 } cryptmethod_T;
 
-static int crypt_sodium_init_(cryptstate_T *state, char_u *key, char_u *salt, int salt_len, char_u *seed, int seed_len);
+static int crypt_sodium_init_(cryptstate_T *state, char_u *key, crypt_arg_T *arg);
 static long crypt_sodium_buffer_decode(cryptstate_T *state, char_u *from, size_t len, char_u **buf_out, int last);
 static long crypt_sodium_buffer_encode(cryptstate_T *state, char_u *from, size_t len, char_u **buf_out, int last);
+#if defined(FEAT_EVAL) && defined(FEAT_SODIUM)
+static void crypt_sodium_report_hash_params( unsigned long long opslimit, unsigned long long ops_def, size_t memlimit, size_t mem_def, int alg, int alg_def);
+#endif
 
 // index is method_nr of cryptstate_T, CRYPT_M_*
 static cryptmethod_T cryptmethods[CRYPT_M_COUNT] = {
@@ -85,6 +90,7 @@
 	"VimCrypt~01!",
 	0,
 	0,
+	0,
 #ifdef CRYPT_NOT_INPLACE
 	TRUE,
 #endif
@@ -102,6 +108,7 @@
 	"VimCrypt~02!",
 	8,
 	8,
+	0,
 #ifdef CRYPT_NOT_INPLACE
 	TRUE,
 #endif
@@ -119,6 +126,7 @@
 	"VimCrypt~03!",
 	8,
 	8,
+	0,
 #ifdef CRYPT_NOT_INPLACE
 	TRUE,
 #endif
@@ -130,7 +138,7 @@
 	crypt_blowfish_encode, crypt_blowfish_decode,
     },
 
-    // XChaCha20 using libsodium
+    // XChaCha20 using libsodium; implementation issues
     {
 	"xchacha20",
 	"VimCrypt~04!",
@@ -140,6 +148,29 @@
 	16,
 #endif
 	8,
+	0,
+#ifdef CRYPT_NOT_INPLACE
+	FALSE,
+#endif
+	FALSE,
+	NULL,
+	crypt_sodium_init_,
+	NULL, NULL,
+	crypt_sodium_buffer_encode, crypt_sodium_buffer_decode,
+	NULL, NULL,
+    },
+    // XChaCha20 using libsodium; stores parameters in header
+    {
+	"xchacha20v2",
+	"VimCrypt~05!",
+#ifdef FEAT_SODIUM
+	crypto_pwhash_argon2id_SALTBYTES, // 16
+#else
+	16,
+#endif
+	8,
+	// sizeof(crypto_pwhash_OPSLIMIT_INTERACTIVE + crypto_pwhash_MEMLIMIT_INTERACTIVE + crypto_pwhash_ALG_DEFAULT)
+	20,
 #ifdef CRYPT_NOT_INPLACE
 	FALSE,
 #endif
@@ -370,6 +401,15 @@
 }
 
 /*
+ * Returns True for Sodium Encryption.
+ */
+    int
+crypt_method_is_sodium(int method)
+{
+    return method == CRYPT_M_SOD || method == CRYPT_M_SOD2;
+}
+
+/*
  * Return TRUE when the buffer uses an encryption method that encrypts the
  * whole undo file, not only the text.
  */
@@ -387,7 +427,8 @@
 {
     return CRYPT_MAGIC_LEN
 	+ cryptmethods[method_nr].salt_len
-	+ cryptmethods[method_nr].seed_len;
+	+ cryptmethods[method_nr].seed_len
+	+ cryptmethods[method_nr].add_len;
 }
 
 
@@ -445,10 +486,7 @@
 crypt_create(
     int		method_nr,
     char_u	*key,
-    char_u	*salt,
-    int		salt_len,
-    char_u	*seed,
-    int		seed_len)
+    crypt_arg_T *crypt_arg)
 {
     cryptstate_T *state = ALLOC_ONE(cryptstate_T);
 
@@ -456,8 +494,7 @@
 	return state;
 
     state->method_nr = method_nr;
-    if (cryptmethods[method_nr].init_fn(
-	state, key, salt, salt_len, seed, seed_len) == FAIL)
+    if (cryptmethods[method_nr].init_fn(state, key, crypt_arg) == FAIL)
     {
 	vim_free(state);
 	return NULL;
@@ -476,17 +513,22 @@
     char_u	*key,
     char_u	*header)
 {
-    char_u	*salt = NULL;
-    char_u	*seed = NULL;
-    int		salt_len = cryptmethods[method_nr].salt_len;
-    int		seed_len = cryptmethods[method_nr].seed_len;
+    crypt_arg_T arg;
 
-    if (salt_len > 0)
-	salt = header + CRYPT_MAGIC_LEN;
-    if (seed_len > 0)
-	seed = header + CRYPT_MAGIC_LEN + salt_len;
+    CLEAR_FIELD(arg);
+    arg.cat_init_from_file = TRUE;
 
-    return crypt_create(method_nr, key, salt, salt_len, seed, seed_len);
+    arg.cat_salt_len = cryptmethods[method_nr].salt_len;
+    arg.cat_seed_len = cryptmethods[method_nr].seed_len;
+    arg.cat_add_len = cryptmethods[method_nr].add_len;
+    if (arg.cat_salt_len > 0)
+	arg.cat_salt = header + CRYPT_MAGIC_LEN;
+    if (arg.cat_seed_len > 0)
+	arg.cat_seed = header + CRYPT_MAGIC_LEN + arg.cat_salt_len;
+    if (arg.cat_add_len > 0)
+	arg.cat_add = header + CRYPT_MAGIC_LEN + arg.cat_salt_len + arg.cat_seed_len;
+
+    return crypt_create(method_nr, key, &arg);
 }
 
 /*
@@ -540,24 +582,29 @@
     int	    *header_len)
 {
     int	    len = crypt_get_header_len(method_nr);
-    char_u  *salt = NULL;
-    char_u  *seed = NULL;
-    int	    salt_len = cryptmethods[method_nr].salt_len;
-    int	    seed_len = cryptmethods[method_nr].seed_len;
+    crypt_arg_T arg;
     cryptstate_T *state;
 
+    CLEAR_FIELD(arg);
+    arg.cat_salt_len = cryptmethods[method_nr].salt_len;
+    arg.cat_seed_len = cryptmethods[method_nr].seed_len;
+    arg.cat_add_len  = cryptmethods[method_nr].add_len;
+    arg.cat_init_from_file = FALSE;
+
     *header_len = len;
     *header = alloc(len);
     if (*header == NULL)
 	return NULL;
 
     mch_memmove(*header, cryptmethods[method_nr].magic, CRYPT_MAGIC_LEN);
-    if (salt_len > 0 || seed_len > 0)
+    if (arg.cat_salt_len > 0 || arg.cat_seed_len > 0 || arg.cat_add_len > 0)
     {
-	if (salt_len > 0)
-	    salt = *header + CRYPT_MAGIC_LEN;
-	if (seed_len > 0)
-	    seed = *header + CRYPT_MAGIC_LEN + salt_len;
+	if (arg.cat_salt_len > 0)
+	    arg.cat_salt = *header + CRYPT_MAGIC_LEN;
+	if (arg.cat_seed_len > 0)
+	    arg.cat_seed = *header + CRYPT_MAGIC_LEN + arg.cat_salt_len;
+	if (arg.cat_add_len > 0)
+	    arg.cat_add = *header + CRYPT_MAGIC_LEN + arg.cat_salt_len + arg.cat_seed_len;
 
 	// TODO: Should this be crypt method specific? (Probably not worth
 	// it).  sha2_seed is pretty bad for large amounts of entropy, so make
@@ -565,16 +612,16 @@
 #ifdef FEAT_SODIUM
 	if (sodium_init() >= 0)
 	{
-	    if (salt_len > 0)
-		randombytes_buf(salt, salt_len);
-	    if (seed_len > 0)
-		randombytes_buf(seed, seed_len);
+	    if (arg.cat_salt_len > 0)
+		randombytes_buf(arg.cat_salt, arg.cat_salt_len);
+	    if (arg.cat_seed_len > 0)
+		randombytes_buf(arg.cat_seed, arg.cat_seed_len);
 	}
 	else
 #endif
-	    sha2_seed(salt, salt_len, seed, seed_len);
+	    sha2_seed(arg.cat_salt, arg.cat_salt_len, arg.cat_seed, arg.cat_seed_len);
     }
-    state = crypt_create(method_nr, key, salt, salt_len, seed, seed_len);
+    state = crypt_create(method_nr, key, &arg);
     if (state == NULL)
 	VIM_CLEAR(*header);
     return state;
@@ -587,7 +634,7 @@
 crypt_free_state(cryptstate_T *state)
 {
 #ifdef FEAT_SODIUM
-    if (state->method_nr == CRYPT_M_SOD)
+    if (crypt_method_is_sodium(state->method_nr))
     {
 	sodium_munlock(((sodium_state_T *)state->method_state)->key,
 							 crypto_box_SEEDBYTES);
@@ -742,7 +789,7 @@
     void
 crypt_check_method(int method)
 {
-    if (method < CRYPT_M_BF2)
+    if (method < CRYPT_M_BF2 || method == CRYPT_M_SOD)
     {
 	msg_scroll = TRUE;
 	msg(_("Warning: Using a weak encryption method; see :help 'cm'"));
@@ -754,7 +801,7 @@
 crypt_check_swapfile_curbuf(void)
 {
     int method = crypt_get_method_nr(curbuf);
-    if (method == CRYPT_M_SOD)
+    if (crypt_method_is_sodium(method))
     {
 	// encryption uses padding and MAC, that does not work very well with
 	// swap and undo files, so disable them
@@ -827,7 +874,7 @@
     }
 
     // since the user typed this, no need to wait for return
-    if (crypt_get_method_nr(curbuf) != CRYPT_M_SOD)
+    if (!crypt_method_is_sodium(crypt_get_method_nr(curbuf)))
     {
 	if (msg_didout)
 	    msg_putchar('\n');
@@ -861,16 +908,16 @@
 crypt_sodium_init_(
     cryptstate_T	*state UNUSED,
     char_u		*key UNUSED,
-    char_u		*salt UNUSED,
-    int			salt_len UNUSED,
-    char_u		*seed UNUSED,
-    int			seed_len UNUSED)
+    crypt_arg_T		*arg UNUSED)
 {
 # ifdef FEAT_SODIUM
     // crypto_box_SEEDBYTES ==  crypto_secretstream_xchacha20poly1305_KEYBYTES
     unsigned char	dkey[crypto_box_SEEDBYTES]; // 32
     sodium_state_T	*sd_state;
     int			retval = 0;
+    unsigned long long	opslimit;
+    size_t		memlimit;
+    int			alg;
 
     if (sodium_init() < 0)
 	return FAIL;
@@ -878,25 +925,98 @@
     sd_state = (sodium_state_T *)sodium_malloc(sizeof(sodium_state_T));
     sodium_memzero(sd_state, sizeof(sodium_state_T));
 
-    // derive a key from the password
-    if (crypto_pwhash(dkey, sizeof(dkey), (const char *)key, STRLEN(key), salt,
-	crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE,
-	crypto_pwhash_ALG_DEFAULT) != 0)
+    if ((state->method_nr == CRYPT_M_SOD2 && !arg->cat_init_from_file)
+	    || state->method_nr == CRYPT_M_SOD)
     {
-	// out of memory
-	sodium_free(sd_state);
-	return FAIL;
+	opslimit = crypto_pwhash_OPSLIMIT_INTERACTIVE;
+	memlimit = crypto_pwhash_MEMLIMIT_INTERACTIVE;
+	alg = crypto_pwhash_ALG_DEFAULT;
+
+#if 0
+	// For testing
+	if (state->method_nr == CRYPT_M_SOD2)
+	{
+	    opslimit = crypto_pwhash_OPSLIMIT_MODERATE;
+	    memlimit = crypto_pwhash_MEMLIMIT_MODERATE;
+	}
+#endif
+
+	// derive a key from the password
+	if (crypto_pwhash(dkey, sizeof(dkey), (const char *)key, STRLEN(key),
+				  arg->cat_salt, opslimit, memlimit, alg) != 0)
+	{
+	    // out of memory
+	    sodium_free(sd_state);
+	    return FAIL;
+	}
+	memcpy(sd_state->key, dkey, crypto_box_SEEDBYTES);
+
+	retval += sodium_mlock(sd_state->key, crypto_box_SEEDBYTES);
+	retval += sodium_mlock(key, STRLEN(key));
+
+	if (retval < 0)
+	{
+	    emsg(_(e_encryption_sodium_mlock_failed));
+	    sodium_free(sd_state);
+	    return FAIL;
+	}
+	if (state->method_nr == CRYPT_M_SOD2)
+	{
+	    memcpy(arg->cat_add, &opslimit, sizeof(opslimit));
+	    arg->cat_add += sizeof(opslimit);
+
+	    memcpy(arg->cat_add, &memlimit, sizeof(memlimit));
+	    arg->cat_add += sizeof(memlimit);
+
+	    memcpy(arg->cat_add, &alg, sizeof(alg));
+	    arg->cat_add += sizeof(alg);
+	}
     }
-    memcpy(sd_state->key, dkey, crypto_box_SEEDBYTES);
-
-    retval += sodium_mlock(sd_state->key, crypto_box_SEEDBYTES);
-    retval += sodium_mlock(key, STRLEN(key));
-
-    if (retval < 0)
+    else
     {
-	emsg(_(e_encryption_sodium_mlock_failed));
-	sodium_free(sd_state);
-	return FAIL;
+	// Reading parameters from file
+	if (arg->cat_add_len
+		    < (int)(sizeof(opslimit) + sizeof(memlimit) + sizeof(alg)))
+	{
+	    sodium_free(sd_state);
+	    return FAIL;
+	}
+
+	// derive the key from the file header
+	memcpy(&opslimit, arg->cat_add, sizeof(opslimit));
+	arg->cat_add += sizeof(opslimit);
+
+	memcpy(&memlimit, arg->cat_add, sizeof(memlimit));
+	arg->cat_add += sizeof(memlimit);
+
+	memcpy(&alg, arg->cat_add, sizeof(alg));
+	arg->cat_add += sizeof(alg);
+
+#ifdef FEAT_EVAL
+	crypt_sodium_report_hash_params(opslimit,
+					    crypto_pwhash_OPSLIMIT_INTERACTIVE,
+		memlimit, crypto_pwhash_MEMLIMIT_INTERACTIVE,
+		alg, crypto_pwhash_ALG_DEFAULT);
+#endif
+
+	if (crypto_pwhash(dkey, sizeof(dkey), (const char *)key, STRLEN(key),
+				  arg->cat_salt, opslimit, memlimit, alg) != 0)
+	{
+	    // out of memory
+	    sodium_free(sd_state);
+	    return FAIL;
+	}
+	memcpy(sd_state->key, dkey, crypto_box_SEEDBYTES);
+
+	retval += sodium_mlock(sd_state->key, crypto_box_SEEDBYTES);
+	retval += sodium_mlock(key, STRLEN(key));
+
+	if (retval < 0)
+	{
+	    emsg(_(e_encryption_sodium_mlock_failed));
+	    sodium_free(sd_state);
+	    return FAIL;
+	}
     }
     sd_state->count = 0;
     state->method_state = sd_state;
@@ -1100,6 +1220,14 @@
     sodium_state_T *sod_st = state->method_state;
     unsigned char  tag;
     unsigned long long out_len;
+
+    if (sod_st->count == 0
+	    && state->method_nr == CRYPT_M_SOD
+	    && len > WRITEBUFSIZE
+		+ crypto_secretstream_xchacha20poly1305_HEADERBYTES
+		+ crypto_secretstream_xchacha20poly1305_ABYTES)
+	len -= cryptmethods[CRYPT_M_SOD2].add_len;
+
     *buf_out = alloc_clear(len);
     if (*buf_out == NULL)
     {
@@ -1158,6 +1286,36 @@
 {
     return randombytes_random();
 }
+
+#if defined(FEAT_EVAL) || defined(PROTO)
+    static void
+crypt_sodium_report_hash_params(
+	unsigned long long opslimit,
+	unsigned long long ops_def,
+	size_t memlimit,
+	size_t mem_def,
+	int alg,
+	int alg_def)
+{
+    if (p_verbose > 0)
+    {
+	verbose_enter();
+	if (opslimit != ops_def)
+	    smsg(_("xchacha20v2: using custom opslimit \"%llu\" for Key derivation."), opslimit);
+	else
+	    smsg(_("xchacha20v2: using default opslimit \"%llu\" for Key derivation."), opslimit);
+	if (memlimit != mem_def)
+	    smsg(_("xchacha20v2: using custom memlimit \"%lu\" for Key derivation."), (unsigned long)memlimit);
+	else
+	    smsg(_("xchacha20v2: using default memlimit \"%lu\" for Key derivation."), (unsigned long)memlimit);
+	if (alg != alg_def)
+	    smsg(_("xchacha20v2: using custom algorithm \"%d\" for Key derivation."), alg);
+	else
+	    smsg(_("xchacha20v2: using default algorithm \"%d\" for Key derivation."), alg);
+	verbose_leave();
+    }
+}
+#endif
 # endif
 
 #endif // FEAT_CRYPT
diff --git a/src/crypt_zip.c b/src/crypt_zip.c
index 91bbd7b..89e4595 100644
--- a/src/crypt_zip.c
+++ b/src/crypt_zip.c
@@ -83,10 +83,7 @@
 crypt_zip_init(
     cryptstate_T    *state,
     char_u	    *key,
-    char_u	    *salt UNUSED,
-    int		    salt_len UNUSED,
-    char_u	    *seed UNUSED,
-    int		    seed_len UNUSED)
+    crypt_arg_T     *arg UNUSED)
 {
     char_u	*p;
     zip_state_T	*zs;
diff --git a/src/fileio.c b/src/fileio.c
index 1966420..8d3e6f5 100644
--- a/src/fileio.c
+++ b/src/fileio.c
@@ -218,6 +218,9 @@
     int		using_b_fname;
     static char *msg_is_a_directory = N_("is a directory");
     int		eof;
+#ifdef FEAT_SODIUM
+    int		may_need_lseek = FALSE;
+#endif
 
     au_did_filetype = FALSE; // reset before triggering any autocommands
 
@@ -1282,15 +1285,43 @@
 		     */
 # ifdef FEAT_SODIUM
 		    // Let the crypt layer work with a buffer size of 8192
+		    //
+		    // Sodium encryption requires a fixed block size to
+		    // successfully decrypt. However, unfortunately the file
+		    // header size changes between xchacha20 and xchacha20v2 by
+		    // 'add_len' bytes.
+		    // So we will now read the maximum header size + encryption
+		    // metadata, but after determining to read an xchacha20
+		    // encrypted file, we have to rewind the file descriptor by
+		    // 'add_len' bytes in the second round.
+		    //
+		    // Be careful with changing it, it needs to stay the same
+		    // for reading back previously encrypted files!
 		    if (filesize == 0)
+		    {
 			// set size to 8K + Sodium Crypt Metadata
 			size = WRITEBUFSIZE + crypt_get_max_header_len()
 		     + crypto_secretstream_xchacha20poly1305_HEADERBYTES
 		     + crypto_secretstream_xchacha20poly1305_ABYTES;
+			may_need_lseek = TRUE;
+		    }
 
-		    else if (filesize > 0 && (curbuf->b_cryptstate != NULL &&
-			 curbuf->b_cryptstate->method_nr == CRYPT_M_SOD))
+		    else if (filesize > 0 && (curbuf->b_cryptstate != NULL
+				&& crypt_method_is_sodium(
+					     curbuf->b_cryptstate->method_nr)))
+		    {
 			size = WRITEBUFSIZE + crypto_secretstream_xchacha20poly1305_ABYTES;
+			// need to rewind by - add_len from CRYPT_M_SOD2 (see
+			// description above)
+			if (curbuf->b_cryptstate->method_nr == CRYPT_M_SOD
+						     && !eof && may_need_lseek)
+			{
+			    lseek(fd, crypt_get_header_len(
+					       curbuf->b_cryptstate->method_nr)
+				       - crypt_get_max_header_len(), SEEK_CUR);
+			    may_need_lseek = FALSE;
+			}
+		    }
 # endif
 		    eof = size;
 		    size = read_eintr(fd, ptr, size);
diff --git a/src/memline.c b/src/memline.c
index ea08d32..8a3bd16 100644
--- a/src/memline.c
+++ b/src/memline.c
@@ -436,7 +436,7 @@
 	sha2_seed(buf->b_ml.ml_mfp->mf_seed, MF_SEED_LEN, NULL, 0);
     }
 #ifdef FEAT_SODIUM
-    else if (method_nr == CRYPT_M_SOD)
+    else if (crypt_method_is_sodium(method_nr))
 	crypt_sodium_randombytes_buf(buf->b_ml.ml_mfp->mf_seed,
 		MF_SEED_LEN);
 #endif
@@ -495,7 +495,7 @@
     old_method = crypt_method_nr_from_name(old_cm);
 
     // Swapfile encryption not supported by XChaCha20
-    if (crypt_get_method_nr(buf) == CRYPT_M_SOD && *buf->b_p_key != NUL)
+    if (crypt_method_is_sodium(crypt_get_method_nr(buf)) && *buf->b_p_key != NUL)
     {
 	// close the swapfile
 	mf_close_file(buf, TRUE);
@@ -5512,6 +5512,7 @@
 /*
  * Prepare for encryption/decryption, using the key, seed and offset.
  * Return an allocated cryptstate_T *.
+ * Note: Encryption not supported for SODIUM
  */
     static cryptstate_T *
 ml_crypt_prepare(memfile_T *mfp, off_T offset, int reading)
@@ -5520,21 +5521,23 @@
     char_u	salt[50];
     int		method_nr;
     char_u	*key;
-    char_u	*seed;
+    crypt_arg_T arg;
 
+    CLEAR_FIELD(arg);
     if (reading && mfp->mf_old_key != NULL)
     {
 	// Reading back blocks with the previous key/method/seed.
 	method_nr = mfp->mf_old_cm;
 	key = mfp->mf_old_key;
-	seed = mfp->mf_old_seed;
+	arg.cat_seed = mfp->mf_old_seed;
     }
     else
     {
 	method_nr = crypt_get_method_nr(buf);
 	key = buf->b_p_key;
-	seed = mfp->mf_seed;
+	arg.cat_seed = mfp->mf_seed;
     }
+
     if (*key == NUL)
 	return NULL;
 
@@ -5543,14 +5546,24 @@
 	// For PKzip: Append the offset to the key, so that we use a different
 	// key for every block.
 	vim_snprintf((char *)salt, sizeof(salt), "%s%ld", key, (long)offset);
-	return crypt_create(method_nr, salt, NULL, 0, NULL, 0);
+	arg.cat_seed = NULL;
+	arg.cat_init_from_file = FALSE;
+
+	return crypt_create(method_nr, salt, &arg);
     }
 
     // Using blowfish or better: add salt and seed. We use the byte offset
     // of the block for the salt.
     vim_snprintf((char *)salt, sizeof(salt), "%ld", (long)offset);
-    return crypt_create(method_nr, key, salt, (int)STRLEN(salt),
-							seed, MF_SEED_LEN);
+
+    arg.cat_salt = salt;
+    arg.cat_salt_len = (int)STRLEN(salt);
+    arg.cat_seed_len = MF_SEED_LEN;
+    arg.cat_add_len = 0;
+    arg.cat_add = NULL;
+    arg.cat_init_from_file = FALSE;
+
+    return crypt_create(method_nr, key, &arg);
 }
 
 #endif
diff --git a/src/option.c b/src/option.c
index 4d9da47..1a9ba25 100644
--- a/src/option.c
+++ b/src/option.c
@@ -4274,7 +4274,7 @@
 		&& !curbufIsChanged() && curbuf->b_ml.ml_mfp != NULL)
 	{
 #ifdef FEAT_CRYPT
-	    if (crypt_get_method_nr(curbuf) == CRYPT_M_SOD)
+	    if (crypt_method_is_sodium(crypt_get_method_nr(curbuf)))
 		continue;
 #endif
 	    u_compute_hash(hash);
diff --git a/src/optionstr.c b/src/optionstr.c
index 8aec385..311b069 100644
--- a/src/optionstr.c
+++ b/src/optionstr.c
@@ -29,7 +29,7 @@
 #ifdef FEAT_CRYPT
 static char *(p_cm_values[]) = {"zip", "blowfish", "blowfish2",
  # ifdef FEAT_SODIUM
-    "xchacha20",
+    "xchacha20", "xchacha20v2",
  # endif
     NULL};
 #endif
diff --git a/src/proto/blowfish.pro b/src/proto/blowfish.pro
index 6b2c454..fbaa3dc 100644
--- a/src/proto/blowfish.pro
+++ b/src/proto/blowfish.pro
@@ -1,6 +1,6 @@
 /* blowfish.c */
 void crypt_blowfish_encode(cryptstate_T *state, char_u *from, size_t len, char_u *to, int last);
 void crypt_blowfish_decode(cryptstate_T *state, char_u *from, size_t len, char_u *to, int last);
-int crypt_blowfish_init(cryptstate_T *state, char_u *key, char_u *salt, int salt_len, char_u *seed, int seed_len);
+int crypt_blowfish_init(cryptstate_T *state, char_u *key, crypt_arg_T *arg);
 int blowfish_self_test(void);
 /* vim: set ft=c : */
diff --git a/src/proto/crypt.pro b/src/proto/crypt.pro
index 560e30b..c067829 100644
--- a/src/proto/crypt.pro
+++ b/src/proto/crypt.pro
@@ -4,12 +4,13 @@
 int crypt_method_nr_from_magic(char *ptr, int len);
 int crypt_works_inplace(cryptstate_T *state);
 int crypt_get_method_nr(buf_T *buf);
+int crypt_method_is_sodium(int method);
 int crypt_whole_undofile(int method_nr);
 int crypt_get_header_len(int method_nr);
 int crypt_get_max_header_len(void);
 void crypt_set_cm_option(buf_T *buf, int method_nr);
 int crypt_self_test(void);
-cryptstate_T *crypt_create(int method_nr, char_u *key, char_u *salt, int salt_len, char_u *seed, int seed_len);
+cryptstate_T *crypt_create(int method_nr, char_u *key, crypt_arg_T *crypt_arg);
 cryptstate_T *crypt_create_from_header(int method_nr, char_u *key, char_u *header);
 cryptstate_T *crypt_create_from_file(FILE *fp, char_u *key);
 cryptstate_T *crypt_create_for_writing(int method_nr, char_u *key, char_u **header, int *header_len);
diff --git a/src/proto/crypt_zip.pro b/src/proto/crypt_zip.pro
index 626d985..eb29dbf 100644
--- a/src/proto/crypt_zip.pro
+++ b/src/proto/crypt_zip.pro
@@ -1,5 +1,5 @@
 /* crypt_zip.c */
-int crypt_zip_init(cryptstate_T *state, char_u *key, char_u *salt, int salt_len, char_u *seed, int seed_len);
+int crypt_zip_init(cryptstate_T *state, char_u *key, crypt_arg_T *arg);
 void crypt_zip_encode(cryptstate_T *state, char_u *from, size_t len, char_u *to, int last);
 void crypt_zip_decode(cryptstate_T *state, char_u *from, size_t len, char_u *to, int last);
 /* vim: set ft=c : */
diff --git a/src/structs.h b/src/structs.h
index 55a859b..7de0d9e 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -2771,11 +2771,24 @@
 # define CRYPT_M_BF	1
 # define CRYPT_M_BF2	2
 # define CRYPT_M_SOD    3
-# define CRYPT_M_COUNT	4 // number of crypt methods
+# define CRYPT_M_SOD2   4
+# define CRYPT_M_COUNT	5 // number of crypt methods
 
 // Currently all crypt methods work inplace.  If one is added that isn't then
 // define this.
 # define CRYPT_NOT_INPLACE 1
+
+// Struct for passing arguments down to the crypt_init functions
+typedef struct {
+    char_u	*cat_salt;
+    int		cat_salt_len;
+    char_u	*cat_seed;
+    int		cat_seed_len;
+    char_u	*cat_add;
+    int		cat_add_len;
+    int		cat_init_from_file;
+} crypt_arg_T;
+
 #endif
 
 #ifdef FEAT_PROP_POPUP
diff --git a/src/testdir/test_crypt.vim b/src/testdir/test_crypt.vim
index d1c645f..a25a880 100644
--- a/src/testdir/test_crypt.vim
+++ b/src/testdir/test_crypt.vim
@@ -81,6 +81,11 @@
   call Crypt_uncrypt('xchacha20')
 endfunc
 
+func Test_crypt_sodium_v2()
+  CheckFeature sodium
+  call Crypt_uncrypt('xchacha20v2')
+endfunc
+
 func Uncrypt_stable(method, crypted_text, key, uncrypted_text)
   split Xtest.txt
   set bin noeol key= fenc=latin1
@@ -96,13 +101,15 @@
   set key=
 endfunc
 
-func Uncrypt_stable_xxd(method, hex, key, uncrypted_text)
+func Uncrypt_stable_xxd(method, hex, key, uncrypted_text, verbose)
   if empty(s:xxd_cmd)
     throw 'Skipped: xxd program missing'
   endif
   " use xxd to write the binary content
   call system(s:xxd_cmd .. ' -r >Xtest.txt', a:hex)
-  call feedkeys(":split Xtest.txt\<CR>" . a:key . "\<CR>", 'xt')
+  let cmd = (a:verbose ? ':verbose' : '') ..
+        \ ":split Xtest.txt\<CR>" . a:key . "\<CR>"
+  call feedkeys(cmd, 'xt')
   call assert_equal(a:uncrypted_text, getline(1, len(a:uncrypted_text)))
   bwipe!
   call delete('Xtest.txt')
@@ -138,7 +145,40 @@
         \  '00000080: 72be 0136 84a1 d3                        r..6...']
   " the file should be in latin1 encoding, this makes sure that readfile()
   " retries several times converting the multi-byte characters
-  call Uncrypt_stable_xxd('xchacha20', hex, "sodium_crypt", ["abcdefghijklmnopqrstuvwxyzäöü", "ZZZ_äüöÄÜÖ_!@#$%^&*()_+=-`~"])
+  call Uncrypt_stable_xxd('xchacha20', hex, "sodium_crypt", ["abcdefghijklmnopqrstuvwxyzäöü", "ZZZ_äüöÄÜÖ_!@#$%^&*()_+=-`~"], 0)
+endfunc
+
+func Test_uncrypt_xchacha20v2_custom()
+  CheckFeature sodium
+  " Test, reading xchacha20v2 with custom encryption parameters
+  let hex = ['00000000: 5669 6d43 7279 7074 7e30 3521 934b f288  VimCrypt~05!.K..',
+        \ '00000010: 10ba 8bc9 25a0 8876 f85c f135 6fb8 518b  ....%..v.\.5o.Q.',
+        \ '00000020: b133 9af1 0300 0000 0000 0000 0000 0010  .3..............',
+        \ '00000030: 0000 0000 0200 0000 b973 5f33 80e9 54fc  .........s_3..T.',
+        \ '00000040: 138f ba3e 046b 3135 90b7 7783 5eac 7fe3  ...>.k15..w.^...',
+        \ '00000050: 0cd2 14df ed75 4b65 8763 8205 035c ec81  .....uKe.c...\..',
+        \ "00000060: a4cf 33d2 7507 ec38 ba62 a327 9068 d8ad  ..3.u..8.b.'.h..",
+        \ '00000070: 2607 3fa6 f95d 7ea8 9799 f997 4820 0c    &.?..]~.....H .']
+  call Uncrypt_stable_xxd('xchacha20v2', hex, "foobar", ["", "foo", "bar", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10"], 1)
+  call assert_match('xchacha20v2: using custom \w\+ "\d\+" for Key derivation.', execute(':messages'))
+endfunc
+
+func Test_uncrypt_xchacha20v2()
+  CheckFeature sodium
+  " Test, reading xchacha20v2
+  let hex = [
+        \ '00000000: 5669 6d43 7279 7074 7e30 3521 9f20 4e14  VimCrypt~05!. N.',
+        \ '00000010: c7da c1bd 7dea 8fbc db6c 38e6 7a77 6fef  ....}....l8.zwo.',
+        \ '00000020: 82dd 964b 0300 0000 0000 0000 0000 0010  ...K............',
+        \ '00000030: 0000 0000 0200 0000 a97c 2f00 0b9d 19eb  .........|/.....',
+        \ '00000040: 1d92 1ea5 3f22 c179 4b3e 870a eb19 6380  ....?".yK>....c.',
+        \ '00000050: 63f8 222d b5d1 3c73 7be5 d580 47ea 44cc  c."-..<s{...G.D.',
+        \ '00000060: 6c25 8078 3fd5 d836 c700 0122 bb30 7a59  l%.x?..6...".0zY',
+        \ '00000070: b184 2ae8 e7db 113a f732 938f 7a34 1333  ..*....:.2..z4.3',
+        \ '00000080: dc89 1491 51a0 67b9 0f3a b56c 1f9d 53b0  ....Q.g..:.l..S.',
+        \ '00000090: 2416 205a 8c4c 5fde 4dac 2611 8a48 24f0  $. Z.L_.M.&..H$.',
+        \ '000000a0: ba00 92c1 60                             ....`']
+  call Uncrypt_stable_xxd('xchacha20v2', hex, "foo1234", ["abcdefghijklmnopqrstuvwxyzäöü", 'ZZZ_äüöÄÜÖ_!@#$%^&*()_+=-`~"'], 0)
 endfunc
 
 func Test_uncrypt_xchacha20_invalid()
@@ -165,7 +205,7 @@
 
   sp Xcrypt_sodium.txt
   " Create a larger file, so that Vim will write in several blocks
-  call setline(1, range(1,4000))
+  call setline(1, range(1, 4000))
   call assert_equal(1, &swapfile)
   set cryptmethod=xchacha20
   call feedkeys(":X\<CR>sodium\<CR>sodium\<CR>", 'xt')
@@ -186,38 +226,73 @@
   bw!
   call delete('Xcrypt_sodium.txt')
   set cryptmethod&vim
+
+endfunc
+
+func Test_uncrypt_xchacha20v2_2()
+  CheckFeature sodium
+
+  sp Xcrypt_sodium_v2.txt
+  " Create a larger file, so that Vim will write in several blocks
+  call setline(1, range(1, 4000))
+  call assert_equal(1, &swapfile)
+  set cryptmethod=xchacha20v2
+  call feedkeys(":X\<CR>sodium\<CR>sodium\<CR>", 'xt')
+  " swapfile disabled
+  call assert_equal(0, &swapfile)
+  call assert_match("Note: Encryption of swapfile not supported, disabling swap file", execute(':messages'))
+  w!
+  " encrypted using xchacha20
+  call assert_match("\[xchachav2\]", execute(':messages'))
+  bw!
+  call feedkeys(":verbose :sp Xcrypt_sodium_v2.txt\<CR>sodium\<CR>", 'xt')
+  " successfully decrypted
+  call assert_equal(range(1, 4000)->map( {_, v -> string(v)}), getline(1,'$'))
+  call assert_match('xchacha20v2: using default \w\+ "\d\+" for Key derivation.', execute(':messages'))
+  set key=
+  w! ++ff=unix
+  " encryption removed (on MS-Windows the .* matches [unix])
+  call assert_match('"Xcrypt_sodium_v2.txt".*4000L, 18893B written', execute(':message'))
+  bw!
+  call delete('Xcrypt_sodium_v2.txt')
+  set cryptmethod&vim
+
 endfunc
 
 func Test_uncrypt_xchacha20_3_persistent_undo()
   CheckFeature sodium
   CheckFeature persistent_undo
 
-  sp Xcrypt_sodium_undo.txt
-  set cryptmethod=xchacha20 undofile
-  call feedkeys(":X\<CR>sodium\<CR>sodium\<CR>", 'xt')
-  call assert_equal(1, &undofile)
-  let ufile=undofile(@%)
-  call append(0, ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'])
-  call cursor(1, 1)
+  for meth in ['xchacha20', 'xchacha20v2']
 
-  set undolevels=100
-  normal dd
-  set undolevels=100
-  normal dd
-  set undolevels=100
-  normal dd
-  set undolevels=100
-  w!
-  call assert_equal(0, &undofile)
-  bw!
-  call feedkeys(":sp Xcrypt_sodium_undo.txt\<CR>sodium\<CR>", 'xt')
-  " should fail
-  norm! u
-  call assert_match('Already at oldest change', execute(':1mess'))
-  call assert_fails('verbose rundo ' .. fnameescape(ufile), 'E822')
-  bw!
-  set undolevels& cryptmethod& undofile&
-  call delete('Xcrypt_sodium_undo.txt')
+    sp Xcrypt_sodium_undo.txt
+    exe "set cryptmethod=" .. meth .. " undofile"
+    call feedkeys(":X\<CR>sodium\<CR>sodium\<CR>", 'xt')
+    call assert_equal(1, &undofile)
+    let ufile=undofile(@%)
+    call append(0, ['monday', 'tuesday', 'wednesday', 'thursday', 'friday'])
+    call cursor(1, 1)
+
+    set undolevels=100
+    normal dd
+    set undolevels=100
+    normal dd
+    set undolevels=100
+    normal dd
+    set undolevels=100
+    w!
+    call assert_equal(0, &undofile)
+    bw!
+    call feedkeys(":sp Xcrypt_sodium_undo.txt\<CR>sodium\<CR>", 'xt')
+    " should fail
+    norm! u
+    call assert_match('Already at oldest change', execute(':1mess'))
+    call assert_fails('verbose rundo ' .. fnameescape(ufile), 'E822')
+    bw!
+    set undolevels& cryptmethod& undofile&
+    call delete('Xcrypt_sodium_undo.txt')
+
+  endfor
 endfunc
 
 func Test_encrypt_xchacha20_missing()
@@ -226,6 +301,7 @@
   endif
   sp Xcrypt_sodium_undo.txt
   call assert_fails(':set cryptmethod=xchacha20', 'E474')
+  call assert_fails(':set cryptmethod=xchacha20v2', 'E474')
   bw!
   set cm&
 endfunc
diff --git a/src/version.c b/src/version.c
index c17109c..a87121f 100644
--- a/src/version.c
+++ b/src/version.c
@@ -696,6 +696,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1481,
+/**/
     1480,
 /**/
     1479,