1. 写在前面

  最近由于工作需要,深入系统的学习了OpenSSL中HMAC的实现方式,为了打牢HMAC的根基,且能够帮助后来者,在这里记录了自己的一些调试心得。
  本文分析的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)硬件实现HMAC的话,一般使用到该结构体。

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_RSAstruct rsa_st *rsa;     /* RSA */
# endif
# ifndef OPENSSL_NO_DSAstruct dsa_st *dsa;     /* DSA */
# endif
# ifndef OPENSSL_NO_DHstruct dh_st *dh;       /* DH */
# endif
# ifndef OPENSSL_NO_ECstruct 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,实际我们硬件实现时,是由硬件维护的,故硬件实现起来,比软件的流程稍微简单一些。

【OpenSSL 之五】:HMAC算法分析相关推荐

  1. 信息摘要算法:HMAC算法分析

    1.HMAC概述 HMAC算法首先它是基于信息摘要算法的.目前主要集合了MD和SHA两大系列消息摘要算法.其中MD系列的算法有HmacMD2.HmacMD4.HmacMD5三种算法:SHA系列的算法有 ...

  2. [crypto]-53-openssl命令行的使用(aes/rsa签名校验/rsa加密解密/hmac)

    常用技巧 如何编写一个二进制规律性的文件, 比如你可以编写一个"0123456789abcdef"的文本文件,记得删除换行符然后用ultraedit打开,ctrl+H就可以看到二进 ...

  3. 常用的openssl命令

    一.使用openssl生成MD5哈希: echo -n "testdata" | openssl dgst -md5 -binary | openssl base64 -e -A ...

  4. Linux环境Shell脚本上传下载阿里云OSS文件

    为什么80%的码农都做不了架构师?>>>    Linux环境Shell脚本上传下载阿里云OSS文件 背景 工作中由于我们项目生成的日志文件比较重要,而本地磁盘空间有限存储不了多久, ...

  5. [Shell 脚本] 备份数据库文件至OSS服务(纯shell脚本无sdk)

    背景: 凡事使用服务器搭建的网站就需要定时备份网站数据,常见的方法是打包网站目录,然后备份到FTP服务器上等.也有通过OSS SDK把备份的网站文件上传到OSS服务器上,但是通过SDK来实现,需要一定 ...

  6. python hashlib_python hashlib模块

    hashlib hashlib主要提供字符加密功能,将md5和sha模块整合到了一起,支持md5,sha1, sha224, sha256, sha384, sha512等算法 具体应用 #!/usr ...

  7. ceph rgw:bucket policy实现

    ceph rgw:bucket policy实现 相比于aws,rgw的bucket policy实现的还不是很完善,有很多细节都不支持,并且已支持的特性也在很多细节方面与s3不同,尤其是因为rgw不 ...

  8. HMAC-SHA1算法shell方式

    echo -n 'stringtosign' | openssl dgst -hmac 'key' -sha1 -binary | base64 举例如下: echo -n 'GET&vers ...

  9. [翻译]在 Jelly Bean 中使用应用加密

    翻译自:Using app encryption in Jelly Bean 关键词 : adb install -l 最新的 Android 4.1(Jelly Bean)版本在上周的 Google ...

最新文章

  1. 你必须知道的.net学习总结
  2. 设置单元格不换行,多出的部分隐藏
  3. springcloud完整项目_.net core+Spring Cloud学习之路 一
  4. flink shell的local模式(benv与senv的使用+处理报错的解决方案)
  5. 数据结构:哈希表函数构造和冲突解决方法
  6. poj3050 穷竭搜索 挑战程序设计竞赛
  7. 中国女性灭菌装置行业市场供需与战略研究报告
  8. SQL 查询所有表名、字段名、类型、长度、存储过程、视图
  9. Modbus通用数据读取工具设计及使用
  10. ospf-3型和5型汇总
  11. html超链接点击后变紫色了,超链接不改变字体颜色
  12. 左耳朵耗子的时间管理法则
  13. 修改app名称后,分享到微信的app名称无变化问题
  14. xsmax无法进入dfu模式_iPhone XR/XS/XS Max 如何进入恢复模式或 DFU 模式?
  15. golang操作elasticsearch(oliver/elastic使用文档)
  16. R语言使用sort函数对向量数据进行排序、默认从小到大升序排序、设置decreasing为真进行降序排序
  17. 「医次元」「心际舰队」:论传销洗脑与纳粹之关联
  18. html圣诞快乐英文,圣诞快乐英语,圣诞快乐英语简写?
  19. Delphi 2010的好消息
  20. 遥感期刊影响因子(中科院分区)

热门文章

  1. Android diva 分析(全)
  2. c语言排序算法插入法,C语言中冒泡法、选择法、插入法三种常见排序算法分析.doc...
  3. PHP实现页面静态化
  4. 计算机专业大几用到移动硬盘,2T有轻薄,4T大容量,双盘在面前,你会怎么选?—希捷2T移动硬盘评测...
  5. 全员“拉踩”苹果,入局一年多的苹果芯片已成“行业标杆”?
  6. 从壹开始[ 做贡献 ]之三 || 北京.Net俱乐部活动——DNT精英论坛开幕
  7. pta 1144 The Missing Number
  8. 记一次tomcat、gateway配置SSL,使用https访问
  9. 支付的那些事——领域模型篇
  10. 小米平板2win下屏蔽按键