mirror of
git://git.gnupg.org/gnupg.git
synced 2024-12-22 10:19:57 +01:00
Experiment to use KEM interface for Curve25519 ECDH.
Works now with hard-coded, but not checked things. Signed-off-by: NIIBE Yutaka <gniibe@fsij.org>
This commit is contained in:
parent
eaa3be7ff2
commit
3114d16bf2
@ -537,6 +537,10 @@ gpg_error_t agent_pksign (ctrl_t ctrl, const char *cache_nonce,
|
|||||||
gpg_error_t agent_pkdecrypt (ctrl_t ctrl, const char *desc_text,
|
gpg_error_t agent_pkdecrypt (ctrl_t ctrl, const char *desc_text,
|
||||||
const unsigned char *ciphertext, size_t ciphertextlen,
|
const unsigned char *ciphertext, size_t ciphertextlen,
|
||||||
membuf_t *outbuf, int *r_padding);
|
membuf_t *outbuf, int *r_padding);
|
||||||
|
gpg_error_t agent_kem_decap (ctrl_t ctrl, const char *desc_text,
|
||||||
|
const unsigned char *ciphertext, size_t ciphertextlen,
|
||||||
|
membuf_t *outbuf,
|
||||||
|
const unsigned char *option, size_t optionlen);
|
||||||
|
|
||||||
/*-- genkey.c --*/
|
/*-- genkey.c --*/
|
||||||
#define CHECK_CONSTRAINTS_NOT_EMPTY 1
|
#define CHECK_CONSTRAINTS_NOT_EMPTY 1
|
||||||
|
@ -1010,10 +1010,12 @@ cmd_pksign (assuan_context_t ctx, char *line)
|
|||||||
|
|
||||||
|
|
||||||
static const char hlp_pkdecrypt[] =
|
static const char hlp_pkdecrypt[] =
|
||||||
"PKDECRYPT [<options>]\n"
|
"PKDECRYPT [--kem]\n"
|
||||||
"\n"
|
"\n"
|
||||||
"Perform the actual decrypt operation. Input is not\n"
|
"Perform the actual decrypt operation. Input is not\n"
|
||||||
"sensitive to eavesdropping.";
|
"sensitive to eavesdropping.\n"
|
||||||
|
"If the --kem option is used, decryption is done with the KEM,\n"
|
||||||
|
"inquiring upper-layer option, when needed.";
|
||||||
static gpg_error_t
|
static gpg_error_t
|
||||||
cmd_pkdecrypt (assuan_context_t ctx, char *line)
|
cmd_pkdecrypt (assuan_context_t ctx, char *line)
|
||||||
{
|
{
|
||||||
@ -1022,20 +1024,33 @@ cmd_pkdecrypt (assuan_context_t ctx, char *line)
|
|||||||
unsigned char *value;
|
unsigned char *value;
|
||||||
size_t valuelen;
|
size_t valuelen;
|
||||||
membuf_t outbuf;
|
membuf_t outbuf;
|
||||||
int padding;
|
int padding = -1;
|
||||||
|
int is_kem;
|
||||||
|
unsigned char *option = NULL;
|
||||||
|
size_t optionlen = 0;
|
||||||
|
|
||||||
(void)line;
|
is_kem = has_option (line, "--kem");
|
||||||
|
|
||||||
/* First inquire the data to decrypt */
|
/* First inquire the data to decrypt */
|
||||||
rc = print_assuan_status (ctx, "INQUIRE_MAXLEN", "%u", MAXLEN_CIPHERTEXT);
|
rc = print_assuan_status (ctx, "INQUIRE_MAXLEN", "%u", MAXLEN_CIPHERTEXT);
|
||||||
if (!rc)
|
if (!rc)
|
||||||
rc = assuan_inquire (ctx, "CIPHERTEXT",
|
rc = assuan_inquire (ctx, "CIPHERTEXT",
|
||||||
&value, &valuelen, MAXLEN_CIPHERTEXT);
|
&value, &valuelen, MAXLEN_CIPHERTEXT);
|
||||||
|
if (!rc && is_kem)
|
||||||
|
rc = assuan_inquire (ctx, "OPTION",
|
||||||
|
&option, &optionlen, MAXLEN_CIPHERTEXT); /* FIXME for maxlen? */
|
||||||
if (rc)
|
if (rc)
|
||||||
return rc;
|
return rc;
|
||||||
|
|
||||||
init_membuf (&outbuf, 512);
|
init_membuf (&outbuf, 512);
|
||||||
|
|
||||||
|
if (is_kem)
|
||||||
|
{
|
||||||
|
rc = agent_kem_decap (ctrl, ctrl->server_local->keydesc,
|
||||||
|
value, valuelen, &outbuf, option, optionlen);
|
||||||
|
xfree (option);
|
||||||
|
}
|
||||||
|
else
|
||||||
rc = agent_pkdecrypt (ctrl, ctrl->server_local->keydesc,
|
rc = agent_pkdecrypt (ctrl, ctrl->server_local->keydesc,
|
||||||
value, valuelen, &outbuf, &padding);
|
value, valuelen, &outbuf, &padding);
|
||||||
xfree (value);
|
xfree (value);
|
||||||
|
@ -157,3 +157,169 @@ agent_pkdecrypt (ctrl_t ctrl, const char *desc_text,
|
|||||||
xfree (shadow_info);
|
xfree (shadow_info);
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
reverse_buffer (unsigned char *buffer, unsigned int length)
|
||||||
|
{
|
||||||
|
unsigned int tmp, i;
|
||||||
|
|
||||||
|
for (i=0; i < length/2; i++)
|
||||||
|
{
|
||||||
|
tmp = buffer[i];
|
||||||
|
buffer[i] = buffer[length-1-i];
|
||||||
|
buffer[length-1-i] = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gpg_error_t
|
||||||
|
agent_kem_decap (ctrl_t ctrl, const char *desc_text,
|
||||||
|
const unsigned char *ciphertext, size_t ciphertextlen,
|
||||||
|
membuf_t *outbuf,
|
||||||
|
const unsigned char *option, size_t optionlen)
|
||||||
|
{
|
||||||
|
gcry_sexp_t s_skey = NULL, s_cipher = NULL;
|
||||||
|
unsigned char *shadow_info = NULL;
|
||||||
|
gpg_error_t err = 0;
|
||||||
|
int no_shadow_info = 0;
|
||||||
|
|
||||||
|
if (!ctrl->have_keygrip)
|
||||||
|
{
|
||||||
|
log_error ("speculative decryption not yet supported\n");
|
||||||
|
err = gpg_error (GPG_ERR_NO_SECKEY);
|
||||||
|
goto leave;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = gcry_sexp_sscan (&s_cipher, NULL, (char*)ciphertext, ciphertextlen);
|
||||||
|
if (err)
|
||||||
|
{
|
||||||
|
log_error ("failed to convert ciphertext: %s\n", gpg_strerror (err));
|
||||||
|
err = gpg_error (GPG_ERR_INV_DATA);
|
||||||
|
goto leave;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DBG_CRYPTO)
|
||||||
|
{
|
||||||
|
log_printhex (ctrl->keygrip, 20, "keygrip:");
|
||||||
|
log_printhex (ciphertext, ciphertextlen, "cipher: ");
|
||||||
|
}
|
||||||
|
err = agent_key_from_file (ctrl, NULL, desc_text,
|
||||||
|
NULL, &shadow_info,
|
||||||
|
CACHE_MODE_NORMAL, NULL, &s_skey, NULL, NULL);
|
||||||
|
if (gpg_err_code (err) == GPG_ERR_NO_SECKEY)
|
||||||
|
no_shadow_info = 1;
|
||||||
|
else if (err)
|
||||||
|
{
|
||||||
|
log_error ("failed to read the secret key\n");
|
||||||
|
goto leave;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shadow_info || no_shadow_info)
|
||||||
|
{ /* divert operation to the smartcard */
|
||||||
|
err = gpg_error (GPG_ERR_NO_SECKEY);
|
||||||
|
goto leave;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{ /* No smartcard, but a private key */
|
||||||
|
unsigned int nbits;
|
||||||
|
gcry_mpi_t seckey_mpi;
|
||||||
|
gcry_mpi_t ephemkey_mpi;
|
||||||
|
gcry_mpi_t encrypted_sessionkey_mpi;
|
||||||
|
const unsigned char *p;
|
||||||
|
size_t len;
|
||||||
|
unsigned char seckey[32];
|
||||||
|
size_t seckeylen;
|
||||||
|
const unsigned char *ephemkey;
|
||||||
|
size_t ephemkeylen;
|
||||||
|
const unsigned char *encrypted_sessionkey;
|
||||||
|
size_t encrypted_sessionkey_len;
|
||||||
|
unsigned char kekkey[32]; /* FIXME */
|
||||||
|
size_t kekkeylen;
|
||||||
|
gcry_cipher_hd_t hd;
|
||||||
|
unsigned char sessionkey_encoded[256];
|
||||||
|
size_t sessionkey_encoded_len;
|
||||||
|
|
||||||
|
|
||||||
|
gcry_sexp_extract_param (s_skey, NULL, "/d", &seckey_mpi, NULL);
|
||||||
|
gcry_sexp_extract_param (s_cipher, NULL, "/e/s",
|
||||||
|
&ephemkey_mpi,
|
||||||
|
&encrypted_sessionkey_mpi, NULL);
|
||||||
|
|
||||||
|
p = gcry_mpi_get_opaque (seckey_mpi, &nbits);
|
||||||
|
len = (nbits+7)/8;
|
||||||
|
memset (seckey, 0, 32);
|
||||||
|
memcpy (seckey + 32 - len, p, len);
|
||||||
|
seckeylen = 32;
|
||||||
|
reverse_buffer (seckey, seckeylen);
|
||||||
|
|
||||||
|
ephemkey = gcry_mpi_get_opaque (ephemkey_mpi, &nbits);
|
||||||
|
ephemkeylen = (nbits+7)/8;
|
||||||
|
/* Remove the 0x40 prefix*/
|
||||||
|
ephemkey++;
|
||||||
|
ephemkeylen--;
|
||||||
|
encrypted_sessionkey = gcry_mpi_get_opaque (encrypted_sessionkey_mpi, &nbits);
|
||||||
|
encrypted_sessionkey_len = (nbits+7)/8;
|
||||||
|
/*FIXME make sure the lengths are all correct. */
|
||||||
|
|
||||||
|
encrypted_sessionkey_len--;
|
||||||
|
if (encrypted_sessionkey[0] != encrypted_sessionkey_len)
|
||||||
|
{
|
||||||
|
err = GPG_ERR_INV_DATA;
|
||||||
|
goto leave;
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypted_sessionkey++;
|
||||||
|
|
||||||
|
/*FIXME: check the internal of optional to determine the KEK-algo and KEKKEYLEN. */
|
||||||
|
kekkeylen = 16;
|
||||||
|
|
||||||
|
err = gcry_kem_decap (GCRY_KEM_PGP_X25519,
|
||||||
|
seckey, seckeylen,
|
||||||
|
ephemkey, ephemkeylen,
|
||||||
|
kekkey, kekkeylen,
|
||||||
|
option, optionlen);
|
||||||
|
|
||||||
|
mpi_release (seckey_mpi);
|
||||||
|
mpi_release (ephemkey_mpi);
|
||||||
|
|
||||||
|
if (DBG_CRYPTO)
|
||||||
|
{
|
||||||
|
log_printhex (kekkey, kekkeylen, "KEK key: ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*FIXME*/
|
||||||
|
err = gcry_cipher_open (&hd, GCRY_CIPHER_AES, GCRY_CIPHER_MODE_AESWRAP, 0);
|
||||||
|
if (err)
|
||||||
|
{
|
||||||
|
log_error ("ecdh failed to initialize AESWRAP: %s\n",
|
||||||
|
gpg_strerror (err));
|
||||||
|
mpi_release (encrypted_sessionkey_mpi);
|
||||||
|
goto leave;
|
||||||
|
}
|
||||||
|
|
||||||
|
err = gcry_cipher_setkey (hd, kekkey, kekkeylen);
|
||||||
|
|
||||||
|
sessionkey_encoded_len = encrypted_sessionkey_len - 8;
|
||||||
|
gcry_cipher_decrypt (hd, sessionkey_encoded, sessionkey_encoded_len,
|
||||||
|
encrypted_sessionkey, encrypted_sessionkey_len);
|
||||||
|
gcry_cipher_close (hd);
|
||||||
|
|
||||||
|
mpi_release (encrypted_sessionkey_mpi);
|
||||||
|
|
||||||
|
if (err)
|
||||||
|
{
|
||||||
|
log_error ("KEM decap failed: %s\n", gpg_strerror (err));
|
||||||
|
goto leave;
|
||||||
|
}
|
||||||
|
|
||||||
|
put_membuf_printf (outbuf, "(5:value%u:", (unsigned int)sessionkey_encoded_len);
|
||||||
|
put_membuf (outbuf, sessionkey_encoded, sessionkey_encoded_len);
|
||||||
|
put_membuf (outbuf, ")", 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
leave:
|
||||||
|
gcry_sexp_release (s_skey);
|
||||||
|
gcry_sexp_release (s_cipher);
|
||||||
|
xfree (shadow_info);
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
@ -75,6 +75,8 @@ struct cipher_parm_s
|
|||||||
assuan_context_t ctx;
|
assuan_context_t ctx;
|
||||||
unsigned char *ciphertext;
|
unsigned char *ciphertext;
|
||||||
size_t ciphertextlen;
|
size_t ciphertextlen;
|
||||||
|
const unsigned char *option;
|
||||||
|
size_t optionlen;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct writecert_parm_s
|
struct writecert_parm_s
|
||||||
@ -2748,6 +2750,13 @@ inq_ciphertext_cb (void *opaque, const char *line)
|
|||||||
parm->ciphertext, parm->ciphertextlen);
|
parm->ciphertext, parm->ciphertextlen);
|
||||||
assuan_end_confidential (parm->ctx);
|
assuan_end_confidential (parm->ctx);
|
||||||
}
|
}
|
||||||
|
else if (has_leading_keyword (line, "OPTION"))
|
||||||
|
{
|
||||||
|
assuan_begin_confidential (parm->ctx);
|
||||||
|
rc = assuan_send_data (parm->dflt->ctx,
|
||||||
|
parm->option, parm->optionlen);
|
||||||
|
assuan_end_confidential (parm->ctx);
|
||||||
|
}
|
||||||
else
|
else
|
||||||
rc = default_inq_cb (parm->dflt, line);
|
rc = default_inq_cb (parm->dflt, line);
|
||||||
|
|
||||||
@ -2782,7 +2791,8 @@ gpg_error_t
|
|||||||
agent_pkdecrypt (ctrl_t ctrl, const char *keygrip, const char *desc,
|
agent_pkdecrypt (ctrl_t ctrl, const char *keygrip, const char *desc,
|
||||||
u32 *keyid, u32 *mainkeyid, int pubkey_algo,
|
u32 *keyid, u32 *mainkeyid, int pubkey_algo,
|
||||||
gcry_sexp_t s_ciphertext,
|
gcry_sexp_t s_ciphertext,
|
||||||
unsigned char **r_buf, size_t *r_buflen, int *r_padding)
|
unsigned char **r_buf, size_t *r_buflen, int *r_padding,
|
||||||
|
int use_kem, const unsigned char *option, size_t optionlen)
|
||||||
{
|
{
|
||||||
gpg_error_t err;
|
gpg_error_t err;
|
||||||
char line[ASSUAN_LINELENGTH];
|
char line[ASSUAN_LINELENGTH];
|
||||||
@ -2837,7 +2847,10 @@ agent_pkdecrypt (ctrl_t ctrl, const char *keygrip, const char *desc,
|
|||||||
err = make_canon_sexp (s_ciphertext, &parm.ciphertext, &parm.ciphertextlen);
|
err = make_canon_sexp (s_ciphertext, &parm.ciphertext, &parm.ciphertextlen);
|
||||||
if (err)
|
if (err)
|
||||||
return err;
|
return err;
|
||||||
err = assuan_transact (agent_ctx, "PKDECRYPT",
|
parm.option = option;
|
||||||
|
parm.optionlen = optionlen;
|
||||||
|
snprintf (line, sizeof line, "PKDECRYPT%s", use_kem? " --kem" : "");
|
||||||
|
err = assuan_transact (agent_ctx, line,
|
||||||
put_membuf_cb, &data,
|
put_membuf_cb, &data,
|
||||||
inq_ciphertext_cb, &parm,
|
inq_ciphertext_cb, &parm,
|
||||||
padding_info_cb, r_padding);
|
padding_info_cb, r_padding);
|
||||||
|
@ -219,7 +219,8 @@ gpg_error_t agent_pkdecrypt (ctrl_t ctrl, const char *keygrip, const char *desc,
|
|||||||
u32 *keyid, u32 *mainkeyid, int pubkey_algo,
|
u32 *keyid, u32 *mainkeyid, int pubkey_algo,
|
||||||
gcry_sexp_t s_ciphertext,
|
gcry_sexp_t s_ciphertext,
|
||||||
unsigned char **r_buf, size_t *r_buflen,
|
unsigned char **r_buf, size_t *r_buflen,
|
||||||
int *r_padding);
|
int *r_padding, int use_kem,
|
||||||
|
const unsigned char *option, size_t optionlen);
|
||||||
|
|
||||||
/* Retrieve a key encryption key. */
|
/* Retrieve a key encryption key. */
|
||||||
gpg_error_t agent_keywrap_key (ctrl_t ctrl, int forexport,
|
gpg_error_t agent_keywrap_key (ctrl_t ctrl, int forexport,
|
||||||
|
@ -142,7 +142,7 @@ extract_secret_x (byte **r_secret_x,
|
|||||||
master key fingerprint". For v5 key, it is considered "adequate"
|
master key fingerprint". For v5 key, it is considered "adequate"
|
||||||
(in terms of NIST SP 800 56A, see 5.8.2 FixedInfo) to use the first
|
(in terms of NIST SP 800 56A, see 5.8.2 FixedInfo) to use the first
|
||||||
20 octets of its 32 octets fingerprint. */
|
20 octets of its 32 octets fingerprint. */
|
||||||
static gpg_error_t
|
gpg_error_t
|
||||||
build_kdf_params (unsigned char kdf_params[256], size_t *r_size,
|
build_kdf_params (unsigned char kdf_params[256], size_t *r_size,
|
||||||
gcry_mpi_t *pkey, const byte pk_fp[MAX_FINGERPRINT_LEN])
|
gcry_mpi_t *pkey, const byte pk_fp[MAX_FINGERPRINT_LEN])
|
||||||
{
|
{
|
||||||
|
@ -471,6 +471,7 @@ pk_encrypt (pubkey_algo_t algo, gcry_mpi_t *resarr, gcry_mpi_t data,
|
|||||||
int kdf_encr_algo;
|
int kdf_encr_algo;
|
||||||
gcry_cipher_hd_t hd;
|
gcry_cipher_hd_t hd;
|
||||||
|
|
||||||
|
/*FIXME use build_kdf_params! */
|
||||||
oid = gcry_mpi_get_opaque (pkey[0], &nbits);
|
oid = gcry_mpi_get_opaque (pkey[0], &nbits);
|
||||||
oidlen = (nbits + 7) / 8;
|
oidlen = (nbits + 7) / 8;
|
||||||
|
|
||||||
|
@ -37,6 +37,9 @@ int pk_check_secret_key (pubkey_algo_t algo, gcry_mpi_t *skey);
|
|||||||
|
|
||||||
|
|
||||||
/*-- ecdh.c --*/
|
/*-- ecdh.c --*/
|
||||||
|
gpg_error_t
|
||||||
|
build_kdf_params (unsigned char kdf_params[256], size_t *r_size,
|
||||||
|
gcry_mpi_t *pkey, const byte pk_fp[MAX_FINGERPRINT_LEN]);
|
||||||
gcry_mpi_t pk_ecdh_default_params (unsigned int qbits);
|
gcry_mpi_t pk_ecdh_default_params (unsigned int qbits);
|
||||||
gpg_error_t pk_ecdh_generate_ephemeral_key (gcry_mpi_t *pkey, gcry_mpi_t *r_k);
|
gpg_error_t pk_ecdh_generate_ephemeral_key (gcry_mpi_t *pkey, gcry_mpi_t *r_k);
|
||||||
gpg_error_t pk_ecdh_encrypt_with_shared_point
|
gpg_error_t pk_ecdh_encrypt_with_shared_point
|
||||||
|
@ -244,13 +244,48 @@ get_it (ctrl_t ctrl,
|
|||||||
goto leave;
|
goto leave;
|
||||||
|
|
||||||
if (sk->pubkey_algo == PUBKEY_ALGO_ECDH)
|
if (sk->pubkey_algo == PUBKEY_ALGO_ECDH)
|
||||||
|
{
|
||||||
|
int with_ecdh_cv25519;
|
||||||
|
|
||||||
fingerprint_from_pk (sk, fp, NULL);
|
fingerprint_from_pk (sk, fp, NULL);
|
||||||
|
with_ecdh_cv25519 = openpgp_oid_is_cv25519 (sk->pkey[0]);
|
||||||
|
|
||||||
|
if (with_ecdh_cv25519)
|
||||||
|
{
|
||||||
|
unsigned char kdf_params[256];
|
||||||
|
size_t kdf_params_size;
|
||||||
|
|
||||||
|
log_info ("ECDH KEM\n");
|
||||||
|
|
||||||
|
build_kdf_params (kdf_params, &kdf_params_size,
|
||||||
|
sk->pkey, fp);
|
||||||
|
|
||||||
|
log_printhex (kdf_params, kdf_params_size, "KDF (option):");
|
||||||
|
log_printsexp ("sexp data:", s_data);
|
||||||
|
|
||||||
|
/* Do PKDECRYPT with --kem. */
|
||||||
|
desc = gpg_format_keydesc (ctrl, sk, FORMAT_KEYDESC_NORMAL, 1);
|
||||||
|
err = agent_pkdecrypt (NULL, keygrip,
|
||||||
|
desc, sk->keyid, sk->main_keyid, sk->pubkey_algo,
|
||||||
|
s_data, &frame, &nframe, &padding,
|
||||||
|
1, kdf_params, kdf_params_size);
|
||||||
|
xfree (desc);
|
||||||
|
gcry_sexp_release (s_data);
|
||||||
|
|
||||||
|
log_printhex (frame, nframe, "DEK frame:");
|
||||||
|
if (err)
|
||||||
|
goto leave;
|
||||||
|
|
||||||
|
goto decryption_done;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Decrypt. */
|
/* Decrypt. */
|
||||||
desc = gpg_format_keydesc (ctrl, sk, FORMAT_KEYDESC_NORMAL, 1);
|
desc = gpg_format_keydesc (ctrl, sk, FORMAT_KEYDESC_NORMAL, 1);
|
||||||
err = agent_pkdecrypt (NULL, keygrip,
|
err = agent_pkdecrypt (NULL, keygrip,
|
||||||
desc, sk->keyid, sk->main_keyid, sk->pubkey_algo,
|
desc, sk->keyid, sk->main_keyid, sk->pubkey_algo,
|
||||||
s_data, &frame, &nframe, &padding);
|
s_data, &frame, &nframe, &padding,
|
||||||
|
0, NULL, 0);
|
||||||
xfree (desc);
|
xfree (desc);
|
||||||
gcry_sexp_release (s_data);
|
gcry_sexp_release (s_data);
|
||||||
if (err)
|
if (err)
|
||||||
@ -293,6 +328,8 @@ get_it (ctrl_t ctrl,
|
|||||||
if (err)
|
if (err)
|
||||||
goto leave;
|
goto leave;
|
||||||
|
|
||||||
|
decryption_done:
|
||||||
|
|
||||||
/* Now the frame are the bytes decrypted but padded session key. */
|
/* Now the frame are the bytes decrypted but padded session key. */
|
||||||
if (!nframe || nframe <= 8
|
if (!nframe || nframe <= 8
|
||||||
|| frame[nframe-1] > nframe)
|
|| frame[nframe-1] > nframe)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user