IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> openssl之一:hmac算法分析 -> 正文阅读

[移动开发]openssl之一:hmac算法分析

1. 写在前面

??最近由于工作需要,深入系统的学习了openssl中hmac的实现方式,为了打牢hamc的根基,且能够帮助后来者,在这里记录了自己的一些调试心得。
??本文分析的openssl的代码版本为:openssl-1.1.1h
??hamc的路径:crypto/hmac,主要包含3个文件hmac.c \ hm_pmeth.c \ hmeth.c。
??详细的HMAC原理分析详见:加密算法 之二 HMAC

2. 主要结构

  • typedef struct hmac_ctx_st HMAC_CTX;
  • typedef struct evp_md_st EVP_MD;
  • typedef struct evp_md_ctx_st EVP_MD_CTX;
  • typedef struct evp_pkey_ctx_st EVP_PKEY_CTX;
  • typedef struct evp_pkey_st EVP_PKEY
  • typedef struct evp_pkey_method_st EVP_PKEY_METHOD;

2.1 HAMC_CTX(文件:ossl_typ.h >>> evp_local.h)

??该结构属于openssl软算法自定义的一个结构体,若使用openssl的软算法的话,会用到该结构体,但是若调用引擎(engine)硬件实现hamc的话,一般使用到该结构体。

struct hmac_ctx_st {
    const EVP_MD *md;    /* 摘要算法的结构体,每种算法都有这么一个结构体,类似算法的句柄 */
    EVP_MD_CTX *md_ctx;  /* 摘要算法的上下文 */
    EVP_MD_CTX *i_ctx;	 /* i代表ipad(内部秘钥),是ipad散列运算的上下文 */
    EVP_MD_CTX *o_ctx;	 /* o代表opad(外部秘钥),是opad散列运算的上下文 */
};

typedef struct hmac_ctx_st HMAC_CTX;

2.2 EVP_MD(文件:ossl_typ.h >>> evp.h)

??摘要算法的结构体,类似摘要算法的句柄。该结构体中定义了通用的摘要计算的抽象方法的集合,可以将其理解为EVP_MD_CTX的子类。

struct evp_md_st {
    int type;
    int pkey_type;
    int md_size; 		/* digest的长度(这个是与算法有关的,比如sha256,摘要值的长度为32字节) */
    unsigned long flags;
    int (*init) (EVP_MD_CTX *ctx);	/* 初始化函数 */
    int (*update) (EVP_MD_CTX *ctx, const void *data, size_t count);	/* 中间过程运算,更新函数 */
    int (*final) (EVP_MD_CTX *ctx, unsigned char *md);	/* 最后一笔运算,用于获取摘要值,不在进行数据的摘要运算 */
    int (*copy) (EVP_MD_CTX *to, const EVP_MD_CTX *from);	/* 复制函数 */
    int (*cleanup) (EVP_MD_CTX *ctx);	/* 复位函数 */	
    int block_size;	/* md的块大小 */
    int ctx_size; 	/* how big does the ctx->md_data need to be */
    /* control function */
    int (*md_ctrl) (EVP_MD_CTX *ctx, int cmd, int p1, void *p2);
} /* EVP_MD */ ;

typedef struct evp_md_st EVP_MD;

2.3 EVP_MD_CTX(文件:ossl_typ.h >>> evp_loacl.h)

?? 摘要算法的上下文。
?? 既然是上下文,肯定包含“摘要”和“数据”。*md_data即为摘要的数据指针,空间一般需要自己申请。
?? 对于本文来说,该结构体中的变量为“*pctx”,它指向了pkey的上下文(hmac在openssl中被划分为pkey类),EVP_PKEY_CTX 的定义如2.4所示 。

struct evp_md_ctx_st {
    const EVP_MD *digest;		/* 摘要 */
    ENGINE *engine;             /* functional reference if 'digest' is ENGINE-provided */
    unsigned long flags;		
    void *md_data;	/* 指向摘要的具体上下文,这个一般有用户自己定义(在openssl的软算法中指向HMAC_CTX所声明的结构体) */
    /* Public key context for sign/verify */
    EVP_PKEY_CTX *pctx; /* 签名(auth)的上下文,openssl将auth归为了pkey类,但是它的运算过程与md运算的过程类似 */
    /* Update function: usually copied from EVP_MD */
    int (*update) (EVP_MD_CTX *ctx, const void *data, size_t count);
} /* EVP_MD_CTX */ ;

typedef struct evp_md_ctx_st EVP_MD_CTX;

2.4 EVP_PKEY_CTX (文件:ossl_typ.h >>> evp.h)

??pkey的上下文结构体如下:

struct evp_pkey_ctx_st {
    /* Method associated with this operation */
    const EVP_PKEY_METHOD *pmeth;
    /* Engine that implements this method or NULL if builtin */
    ENGINE *engine;
    /* Key: may be NULL */
    EVP_PKEY *pkey;
    /* Peer key for key agreement, may be NULL */
    EVP_PKEY *peerkey;
    /* Actual operation */
    int operation;
    /* Algorithm specific data */
    void *data;
    /* Application specific data */
    void *app_data;
    /* Keygen callback */
    EVP_PKEY_gen_cb *pkey_gencb;
    /* implementation specific keygen data */
    int *keygen_info;
    int keygen_info_count;
} /* EVP_PKEY_CTX */ ;

typedef struct evp_pkey_ctx_st EVP_PKEY_CTX;

2.5 EVP_PKEY (文件:ossl_typ.h >>> evp.h)

??pkey算法的结构体,类似pkey算法的句柄。

/*
 * Type needs to be a bit field Sub-type needs to be for variations on the
 * method, as in, can it do arbitrary encryption....
 */
struct evp_pkey_st {
    int type;
    int save_type;
    CRYPTO_REF_COUNT references;
    const EVP_PKEY_ASN1_METHOD *ameth;
    ENGINE *engine;
    ENGINE *pmeth_engine; /* If not NULL public key ENGINE to use */
    union {
        void *ptr;
# ifndef OPENSSL_NO_RSA
        struct rsa_st *rsa;     /* RSA */
# endif
# ifndef OPENSSL_NO_DSA
        struct dsa_st *dsa;     /* DSA */
# endif
# ifndef OPENSSL_NO_DH
        struct dh_st *dh;       /* DH */
# endif
# ifndef OPENSSL_NO_EC
        struct ec_key_st *ec;   /* ECC */
        ECX_KEY *ecx;           /* X25519, X448, Ed25519, Ed448 */
# endif
    } pkey;
    int save_parameters;
    STACK_OF(X509_ATTRIBUTE) *attributes; /* [ 0 ] */
    CRYPTO_RWLOCK *lock;
} /* EVP_PKEY */ ;

typedef struct evp_pkey_st EVP_PKEY;

2.6 EVP_PKEY_METHOD(文件:ossl_typ.h >>> evp.h)

??该结构体中定义了通用的mac计算的抽象方法的集合。

struct evp_pkey_method_st {
    int pkey_id;
    int flags;
    int (*init) (EVP_PKEY_CTX *ctx);
    int (*copy) (EVP_PKEY_CTX *dst, EVP_PKEY_CTX *src);
    void (*cleanup) (EVP_PKEY_CTX *ctx);
    int (*paramgen_init) (EVP_PKEY_CTX *ctx);
    int (*paramgen) (EVP_PKEY_CTX *ctx, EVP_PKEY *pkey);
    int (*keygen_init) (EVP_PKEY_CTX *ctx);
    int (*keygen) (EVP_PKEY_CTX *ctx, EVP_PKEY *pkey);
    int (*sign_init) (EVP_PKEY_CTX *ctx);
    int (*sign) (EVP_PKEY_CTX *ctx, unsigned char *sig, size_t *siglen,
                 const unsigned char *tbs, size_t tbslen);
    int (*verify_init) (EVP_PKEY_CTX *ctx);
    int (*verify) (EVP_PKEY_CTX *ctx,
                   const unsigned char *sig, size_t siglen,
                   const unsigned char *tbs, size_t tbslen);
    int (*verify_recover_init) (EVP_PKEY_CTX *ctx);
    int (*verify_recover) (EVP_PKEY_CTX *ctx,
                           unsigned char *rout, size_t *routlen,
                           const unsigned char *sig, size_t siglen);
    int (*signctx_init) (EVP_PKEY_CTX *ctx, EVP_MD_CTX *mctx);
    int (*signctx) (EVP_PKEY_CTX *ctx, unsigned char *sig, size_t *siglen,
                    EVP_MD_CTX *mctx);
    int (*verifyctx_init) (EVP_PKEY_CTX *ctx, EVP_MD_CTX *mctx);
    int (*verifyctx) (EVP_PKEY_CTX *ctx, const unsigned char *sig, int siglen,
                      EVP_MD_CTX *mctx);
    int (*encrypt_init) (EVP_PKEY_CTX *ctx);
    int (*encrypt) (EVP_PKEY_CTX *ctx, unsigned char *out, size_t *outlen,
                    const unsigned char *in, size_t inlen);
    int (*decrypt_init) (EVP_PKEY_CTX *ctx);
    int (*decrypt) (EVP_PKEY_CTX *ctx, unsigned char *out, size_t *outlen,
                    const unsigned char *in, size_t inlen);
    int (*derive_init) (EVP_PKEY_CTX *ctx);
    int (*derive) (EVP_PKEY_CTX *ctx, unsigned char *key, size_t *keylen);
    int (*ctrl) (EVP_PKEY_CTX *ctx, int type, int p1, void *p2);
    int (*ctrl_str) (EVP_PKEY_CTX *ctx, const char *type, const char *value);
    int (*digestsign) (EVP_MD_CTX *ctx, unsigned char *sig, size_t *siglen,
                       const unsigned char *tbs, size_t tbslen);
    int (*digestverify) (EVP_MD_CTX *ctx, const unsigned char *sig,
                         size_t siglen, const unsigned char *tbs,
                         size_t tbslen);
    int (*check) (EVP_PKEY *pkey);
    int (*public_check) (EVP_PKEY *pkey);
    int (*param_check) (EVP_PKEY *pkey);

    int (*digest_custom) (EVP_PKEY_CTX *ctx, EVP_MD_CTX *mctx);
} /* EVP_PKEY_METHOD */ ;

typedef struct evp_pkey_method_st EVP_PKEY_METHOD;

3. 主要函数

??由于工作的需要,本次只研究了hm_pmeth.c和hamc.c的相关函数,就逐个分析hm_pmeth.c和hmac.c中的函数。
??在1.1.1中,大多数的数据结构已经不再向使用者开放,从封装的角度来看,这是更合理的。如果你在头文件中找不到结构定义,不妨去源码中搜一搜。

3.1 hmac.c中的主要函数

  • HMAC_CTX HMAC_CTX_new(void)
    (1)创建HAMC_CTX上下文结构(即为上下文结构分配一块内存空间)。
  • int HMAC_Init_ex(HMAC_CTX *ctx, const void *key, int len, const EVP_MD *md, ENGINE *impl)
    (1)初始化HAMC_CTX上下文结构,key为秘钥,len为秘钥长度,md为计算hash的函数集合(digest的句柄)
    (2)若key的长度大于“block size”,则需要先对key做一次hash运算,若key的长度小于“block size”,后边以“0”补齐,直到key的长度等于“block size”为止。
    (3)计算ipad,并计算ipad的hash值,存放在i_ctx上下文中;
    (4)计算opad,并计算opad的hash值,存放在o_ctx上下文中;
    (5)将ctx->i_ctx复制到ctx->md_ctx中,此举的目的是根据hamc算法的定义,ipad首先参与hash计算。
  • int HMAC_Init(HMAC_CTX *ctx, const void *key, int len, const EVP_MD *md)
    (1)此函数用于兼容按照早期版本开发的工程,直接调用HMAC_Init_ex实现。
  • int HMAC_Update(HMAC_CTX *ctx, const unsigned char *data, size_t len)
    (1)调用EVP_DigestUpdate实现hash运算(充分看出计算mac和计算hash有很多相似之处)。
  • int HMAC_Final(HMAC_CTX *ctx, unsigned char *md, unsigned int *len)
    (1)调用函数 EVP_DigestFinal_ex() 获取update运算的hash值,放到buf中;
    (2)调用函数 EVP_MD_CTX_copy_ex() 将i_ctx上下文复制到md_ctx中;
    (3)调用函数 EVP_DigestUpdate() 计算hash值;
    (4)再次调用函数 EVP_DigestFinal_ex() 获取最终的hash(digest)值。
    通过以上的这4步运算,按照算法的要求将opadkey拼接到buf的最前方,实现计算hash的最终结果。
  • void HMAC_CTX_free(HMAC_CTX *ctx)
    (1)释放HAMC_CTX上下文结构(这里特别注意需要逐层释放,先释放最内层的,在释放最外层的)。
  • unsigned char *HMAC(const EVP_MD *evp_md, const void *key, int key_len, const unsigned char *d, size_t n, unsigned char *md, unsigned int *md_len)
    (1)该函数实现单笔hash值的计算,为上面函数的组合体。

3.2 hm_pmeth.c中的主要函数

??结构体EVP_PKEY_METHOD中定义的pkey操作的函数很多,但可能多数都用不到,在hm_pmeth.c中主要就实现了如下几个函数,在实际的应用开发中(引擎的开发),我们也是依葫芦画瓢,实现了这些函数。
??关键的结构体:

/* HMAC pkey context structure */
typedef struct {
    const EVP_MD *md;           /* MD for HMAC use */
    ASN1_OCTET_STRING ktmp;     /* Temp storage for key */
    HMAC_CTX *ctx;
} HMAC_PKEY_CTX;
  • static int pkey_hmac_init(EVP_PKEY_CTX *ctx)
    (1)初始化HMAC_PKEY_CTX上下文结构,并赋值给ctx->data;
    (2)这个函数的本质作用是为ctx的data变量分配空间。
  • static int pkey_hmac_copy(EVP_PKEY_CTX *dst, EVP_PKEY_CTX *src)
    (1)赋值上下文。
  • static void pkey_hmac_cleanup(EVP_PKEY_CTX *ctx)
    (1)清空,复位。
  • static int pkey_hmac_keygen(EVP_PKEY_CTX *ctx, EVP_PKEY *pkey)
    (1)此函数重要实现的是,将ctx->data中的秘钥复制到pkey中,该函数实现的是秘钥的搬移,而非重新生成。
  • static int hmac_signctx_init(EVP_PKEY_CTX *ctx, EVP_MD_CTX *mctx)
    (1)此函数主要是为mctx上下文指定进行摘要运算的update函数。
  • static int hmac_signctx(EVP_PKEY_CTX *ctx, unsigned char *sig, size_t *siglen, EVP_MD_CTX *mctx)
    (1)若mctx为空,则仅返回摘要值的长度。
    (2)若mctx非空,则调用HMAC_Final()获取最终的摘要值。
  • static int pkey_hmac_ctrl(EVP_PKEY_CTX *ctx, int type, int p1, void *p2)
    (1)EVP_PKEY_CTRL_SET_MAC_KEY:将key写入ctx->data中,这样key就保存在ctx上下文中。
    (2)EVP_PKEY_CTRL_MD:为hamc运算关联相关的md操作(因为hamc运算本质上digest运算,所以必须指定digest的函数集合)。
    (3)EVP_PKEY_CTRL_DIGESTINIT:从ctx->pkey->pkey.ptr中获取key,并进行hamc的初始化操作。(这里换做engine操作的话,会将key保存到自定义的上下文中,供硬件调用,不需要软件维护,openssl是软件维护了key)
  • static int pkey_hmac_ctrl_str(EVP_PKEY_CTX *ctx, const char *type, const char *value)

4. 软件实现

??分析完以上的函数之后,我们相同的模式实现了engine(引擎)的驱动,并写了sample代码,下面重点通过分析例子代码,梳理一下代码的执行流程。
??首先贴出我已经写好并验证的代码,如下:

/* 明文 */
static const unsigned char P[] = {  
    0x1A, 0x1E, 0x1F, 0x2F, 0x3F, 0x4F, 0xFA, 0xBD, 0xED, 0xCD, 0xFA, 0xFC, 0xCA, 0xDA, 0xDB, 0x12,
    0x34, 0x56, 0x78, 0x90, 0x9A, 0x1D, 0x11, 0x1E, 0x12, 0x6C, 0x36, 0xDD, 0xFF, 0x12, 0x9A, 0x0F,
    0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0x00, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF, 0xFF,
    0xDD, 0x12, 0x33, 0x44, 0x01, 0x12, 0x4A, 0x3F, 0x1A, 0x2B, 0xC8, 0x59, 0x6A, 0x05, 0x85, 0xE0,
};
/* 秘钥 */
static const unsigned char K[] = {
    0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44, 0x44,
    0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44, 0x44,
    0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44, 0x44,
    0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44, 0x44,
    0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44, 0x44,
    0x11, 0x11, 0x22, 0x22, 0x33, 0x33, 0x44, 0x44,
};
/* 通过sha1计算的hamc值 */
static const unsigned char E_hamc_sha1[] = {
    0xFC, 0xE9, 0xFD, 0xB7, 0x95, 0x75, 0x3B, 0xFA, 0x5D, 0xC1, 0xF5, 0x8B, 0x4B, 0x25, 0x17, 0x33, 
    0xE5, 0x29, 0xD4, 0x04,
};
/* 自定义的结构体 */
struct test_sign {
    const char *name;
    unsigned int nid;
    const char *algname;
    const unsigned char *plaintext;
    const unsigned char *key;
    const unsigned char *mac;
    int psize;
    int keylen;
};

static struct test_sign test_signs[] = {
    {
        .name = "HMAC(md5)",
        .nid = EVP_PKEY_HMAC,
        .algname = "MD5",
        .plaintext = P,
        .key = K,
        .mac = E_hamc_md5,
        .psize = sizeof(P),
        .keylen = sizeof(K)
    },
    {0};
}

static int test_hmac(struct test_sign *t)
{
    int ret = SUCCESS, test;
    EVP_MD_CTX *mctx = NULL;
    EVP_PKEY_CTX *pctx = NULL, *genctx = NULL;
    EVP_PKEY *pkey = NULL;
    const EVP_MD *md = NULL;
    unsigned char mac[EVP_MAX_MD_SIZE];
    size_t mac_len = 0;
	
	/* key的生成过程 */
    genctx = EVP_PKEY_CTX_new_id(t->nid, NULL);	/* 通过nid获取 EVP_PKEY_CTX 上下文 */
    EVP_PKEY_keygen_init(genctx);	/* 对新申请的上下文进行初始化,主要用户内存的申请、数据的填充等 */
    EVP_PKEY_CTX_set_mac_key(genctx, t->key, t->keylen);	/* 将key设置到 genctx 上下文中 */
    EVP_PKEY_keygen(genctx, &pkey);	/* 将key复制到pkey中 */
    EVP_PKEY_CTX_free(genctx);	/* 释放 EVP_PKEY_CTX 上下文 */

	/* 通过sha1计算mac值 */
    md = EVP_get_digestbyname(t->algname);	/* 通过算法名称获取md */
    mctx = EVP_MD_CTX_new();	/* 创建一个全新的 EVP_MD_CTX 上下文*/
    EVP_DigestSignInit(mctx, &pctx, md, NULL, pkey);	/* 将md、pkey与mctx进行绑定 */
    EVP_DigestSignUpdate(mctx, t->plaintext, t->psize);	/* 计算摘要值 */
    EVP_DigestSignFinal(mctx, NULL, &mac_len);	/* 获取mac值的长度 */
    EVP_DigestSignFinal(mctx, mac, &mac_len);	/* 获取mac值 */

    /* check */
    TEST_ASSERT(((mac_len == sizeof(t->mac)) && (!memcmp(mac, t->mac, mac_len))),
                t->name, "digest");
    ret |= test;
	
	/* 释放内存 */
    EVP_PKEY_CTX_free(pctx);
    EVP_MD_CTX_free(mctx);
    EVP_PKEY_free(pkey);
    return ret;
}

??计算mac值,主要分为两步走:第1步 生成秘钥,第2步:计算mac值(通过计算hash的方式计算mac值)。

4.1 秘钥生成

  • EVP_PKEY_CTX_new_id():通过nid获取 EVP_PKEY_CTX 类型的上下文 genctx;
  • EVP_PKEY_keygen_init():对新申请的上下文进行初始化,主要用户内存的申请、数据的填充等(若向openssl注册了engine且该engine支持hamc运算,则该函数会调用engine的init函数,否则直接调用openssl的init函数);
  • EVP_PKEY_CTX_set_mac_key():将秘钥设置到 genctx 上下文中。
  • EVP_PKEY_keygen():/* 将key复制到pkey中 */
  • EVP_PKEY_CTX_free():/* 释放genctx上下文,到此为止genctx就寿终正寝了 */
    ??以上一系列操作的目的就是将key放到 EVP_PKEY *pkey 中,目前我所能实现的方式就是这种,不知道是否可以直接将key放到pkey中。

4.2 mac计算

  • EVP_get_digestbyname():通过算法名称获取EVP_MD md(摘要操作的函数集合);
  • EVP_MD_CTX_new() :创建一个摘要上下文
  • EVP_DigestSignInit():该函数主要做了以下几件事情
    (1)若mctx->pctx为空,则新窗口 pctx 上下文;
    (2)为mctx->update指定hash运算的update函数,同时将mctx->pctx->operation = EVP_PKEY_OP_SIGNCTX;
    (3)将md与pctx进行关联;
    (4)进行mctx的初始化操作。
  • EVP_DigestSignUpdate():计算摘要值。
  • EVP_DigestSignFinal():获取mac长度或mac值。

5. 总结

  • 在openssl中,hamc的计算被归为pkey类的计算,但是它和digest的计算有很多的显示之处,主要区别在于digest的计算需要秘钥,而hamc的计算需要秘钥,且秘钥还有两个ipad_key、opad_key,而且这两个可以都是通过我们的秘钥key生成的。
  • hamc的计算有一个秘钥生成的过程,与其说是秘钥生成,不如说是秘钥的复制,其实就是将秘钥放到EVP_PKEY 结构体中的ptr位置处(内存需要自己申请)。
  • EVP_MD_CTX 下文中包含 EVP_PKEY_CTX上下文 ,因为本质上hamc运算也是用digest的那一套函数接口进行计算。
  • openssl的hamc软算法自己维护了ipad_key、opad_key,实际我们硬件实现时,是由硬件维护的,故硬件实现起来,比软件的流程稍微简单一些。
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-10-17 12:06:28  更:2021-10-17 12:08:16 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/23 22:43:23-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码