Skip to content

Instantly share code, notes, and snippets.

@liweitianux
Last active August 24, 2023 09:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save liweitianux/84aa72d035e1b1a5ad4d6d629fe74455 to your computer and use it in GitHub Desktop.
Save liweitianux/84aa72d035e1b1a5ad4d6d629fe74455 to your computer and use it in GitHub Desktop.
Chacha20-Poly1305 AEAD with PBKDF2 key derivation
/*-
* SPDX-License-Identifier: MIT
*
* Copyright (c) 2022-2023 Aaron LI
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject
* to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*-
* Simple file encryptor/decryptor.
*/
#include <err.h>
#include <errno.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h> /* getopt() */
#include "xx_crypto.h"
/*
* Read from the given file $fp until EOF, and return the data,
* with data length save in $size.
*
* The returned data must be free()'d after use.
*/
static void *
xx_read_file(FILE *fp, size_t *size)
{
static size_t block_size = 4096;
unsigned char *buf, *tmp;
size_t n, sz, buf_size, tmp_size;
buf = tmp = NULL;
buf_size = sz = 0;
do {
if (sz + block_size > buf_size) {
tmp_size = (buf_size == 0 ? block_size : buf_size * 2);
tmp = calloc(1, tmp_size);
if (tmp == NULL) {
fprintf(stderr, "%s: calloc() failed: %s\n",
__func__, strerror(errno));
goto err;
}
if (buf != NULL) {
memcpy(tmp, buf, buf_size);
free(buf);
}
buf = tmp;
buf_size = tmp_size;
}
n = fread(buf + sz, 1, block_size, fp);
sz += n;
if (n != block_size) {
if (feof(fp)) {
break;
} else {
fprintf(stderr, "%s: fread() failed: %s\n",
__func__, strerror(errno));
goto err;
}
};
} while (!feof(fp));
*size = sz;
return buf;
err:
if (buf)
free(buf);
return NULL;
}
static void
usage(const char *progname)
{
fprintf(stderr,
"Simple file encryptor/decryptor\n"
"\n"
"usage: %s [-d] [-i <infile>] [-o <outfile>]\n"
"\n"
"options:\n"
" -d: do decryption instead of encryption\n"
" -i <infile>: input file (stdin if unspecified)\n"
" -o <outfile>: outfile file (stdout if unspecified)\n"
" -v: show verbose info\n"
"\n",
progname);
exit(1);
}
int
main(int argc, char *argv[])
{
const char *progname = argv[0];
const char *infile = NULL;
const char *outfile = NULL;
FILE *infp = NULL;
FILE *outfp = NULL;
bool mode_encrypt = true;
bool verbose = false;
int ch;
while ((ch = getopt(argc, argv, "di:ho:v")) != -1) {
switch (ch) {
case 'd':
mode_encrypt = false;
break;
case 'i':
infile = optarg;
break;
case 'o':
outfile = optarg;
break;
case 'v':
verbose = true;
break;
case 'h':
default:
usage(progname);
/* NOTREACH */
break;
}
}
argc -= optind;
if (argc != 0) {
warnx("unknown extra arguments");
usage(progname);
/* NOTREACH */
}
infp = stdin;
if (infile != NULL) {
infp = fopen(infile, "r");
if (infp == NULL)
err(1, "fopen(%s)", infile);
}
outfp = stdout;
if (outfile != NULL) {
outfp = fopen(outfile, "w");
if (outfp == NULL)
err(1, "fopen(%s)", outfile);
}
if (verbose) {
fprintf(stderr, "Mode: %s\n",
mode_encrypt ? "encryption" : "decryption");
fprintf(stderr, "Input file: %s\n", infile ? infile : "(stdin)");
fprintf(stderr, "Output file: %s\n", outfile ? outfile : "(stdout)");
}
unsigned char *input, *output;
size_t input_len;
int output_len;
input = xx_read_file(infp, &input_len);
if (input == NULL) {
errx(1, "xx_read_file(%s) failed", infile);
}
if (mode_encrypt) {
if (!xx_encrypt(input, input_len, &output, &output_len)) {
errx(1, "xx_encrypt() failed");
}
} else {
if (!xx_decrypt(input, input_len, &output, &output_len)) {
errx(1, "xx_decrypt() failed");
}
}
if (fwrite(output, 1, output_len, outfp) != (size_t)output_len) {
err(1, "fwrite()");
}
free(input);
free(output);
fflush(outfp);
if (infile != NULL)
fclose(infp);
if (outfile != NULL)
fclose(outfp);
return 0;
}
/*-
* SPDX-License-Identifier: MIT
*
* Copyright (c) 2022-2023 Aaron LI
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject
* to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* XX crypto utilities: Chacha20-Poly1305 AEAD with PBKDF2 key derivation.
*
* Output format:
* <header> || <tag> || <ciphertext>
*
* Header format:
* <mark> || "$" || <hash> || "$" || <iteration> || "$" || <salt> || "$"
*
* Hash: "1" (SHA512)
* Iteration: <integer_string>
* Salt: <hex_string>
* Tag, Ciphertext: binary data
*
* NOTE: The <header> is used as the additional authenticated data (AAD).
*
* Compile:
* $ cc ${CFLAGS} $(pkg-config --cflags libcrypto) \
* -DXX_PASSWORD='"<password-string>"' -c xx_crypto.c
*
* Reference:
* - https://wiki.openssl.org/index.php/EVP_Authenticated_Encryption_and_Decryption
* - https://tls13.xargs.org/
* - https://datatracker.ietf.org/doc/html/rfc8439
* - https://datatracker.ietf.org/doc/html/rfc7905
*/
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include "xx_crypto.h"
#ifndef XX_PASSWORD
#error "XX_PASSWORD undefined"
#endif
#ifdef DEBUG
#define XX_DPRINTF(fmt, ...) \
fprintf(stderr, "[DEBUG] %s: " fmt "\n", __func__, ##__VA_ARGS__)
#else
#define XX_DPRINTF(fmt, ...) /* nothing */
#endif
#define PBKDF2_HASH_SHA512 1
#define PBKDF2_ITERATION 100000
/* header mark of the output */
#define HEADER_MARK "XX/v1"
/* maximum length of the header */
#define HEADER_MAX_LEN 128
/* length of key (256 bits) */
#define CHACHA20_POLY1305_KEY_LEN 32
/* length of IV/nonce (96 bits) */
#define CHACHA20_POLY1305_IV_LEN 12
/* length of the authentication tag (128 bits) */
#define CHACHA20_POLY1305_TAG_LEN 16
typedef struct {
/* Chacha20-Poly1305 cipher */
const EVP_CIPHER *cipher;
/* Passphrase */
const char *password;
/* PBKDF2 parameters */
int hash;
int iteration;
unsigned char salt[PKCS5_SALT_LEN];
/* Derived key, IV/nonce */
unsigned char key[CHACHA20_POLY1305_KEY_LEN];
unsigned char iv[CHACHA20_POLY1305_IV_LEN];
/* Authentication tag */
unsigned char tag[CHACHA20_POLY1305_TAG_LEN];
/* Additional authenticated data (AAD) */
char aad[HEADER_MAX_LEN];
int aad_len;
/* Plaintext and ciphertext */
const unsigned char *in;
int in_len;
unsigned char *out;
int out_len;
} xx_ctx;
static int
init_ctx(xx_ctx *ctx)
{
char *p, *endp;
size_t i;
int n;
ctx->cipher = EVP_chacha20_poly1305();
ctx->password = XX_PASSWORD;
ctx->hash = PBKDF2_HASH_SHA512;
ctx->iteration = PBKDF2_ITERATION;
if (RAND_bytes(ctx->salt, sizeof(ctx->salt)) <= 0) {
XX_DPRINTF("RAND_bytes(salt) failed");
return 0;
}
p = ctx->aad;
endp = ctx->aad + sizeof(ctx->aad);
n = snprintf(p, endp - p, "%s$%d$%d$",
HEADER_MARK, ctx->hash, ctx->iteration);
p += n;
for (i = 0; i < sizeof(ctx->salt); i++) {
n = snprintf(p, endp - p, "%02x", ctx->salt[i]);
p += n;
}
n = snprintf(p, endp - p, "$");
p += n;
ctx->aad_len = p - ctx->aad;
XX_DPRINTF("AAD: |%s| (len=%d)", ctx->aad, ctx->aad_len);
return 1;
}
static int
derive_key_iv(xx_ctx *ctx)
{
const EVP_MD *digest;
unsigned char buf[CHACHA20_POLY1305_KEY_LEN + CHACHA20_POLY1305_IV_LEN];
int len;
if (ctx->hash == PBKDF2_HASH_SHA512) {
digest = EVP_sha512();
} else {
XX_DPRINTF("invalid hash: %d", ctx->hash);
return 0;
}
len = CHACHA20_POLY1305_KEY_LEN + CHACHA20_POLY1305_IV_LEN;
if (!PKCS5_PBKDF2_HMAC(ctx->password, -1 /* pass_len */,
ctx->salt, sizeof(ctx->salt), ctx->iteration,
digest, len, buf)) {
XX_DPRINTF("PKCS5_PBKDF2_HMAC() failed");
return 0;
}
memcpy(ctx->key, buf, sizeof(ctx->key));
memcpy(ctx->iv, buf + sizeof(ctx->key), sizeof(ctx->iv));
XX_DPRINTF("Derived key and IV");
return 1;
}
static int
read_header(xx_ctx *ctx, const unsigned char *in, size_t inlen)
{
const unsigned char *p, *endp;
unsigned char *salt;
char header[HEADER_MAX_LEN] = { 0 };
char salt_str[HEADER_MAX_LEN] = { 0 };
char end[2];
long saltlen;
int hash, iteration, n;
n = 0;
endp = in + inlen;
for (p = in; p < endp && *p; p++) {
if (*p == '$')
n++;
if (n == 4)
break;
}
if (n != 4) {
XX_DPRINTF("Invalid header");
return 0;
}
p++; /* include the '$' */
ctx->aad_len = p - in;
if (ctx->aad_len + 1 > (int)sizeof(ctx->aad)) {
XX_DPRINTF("Header too long");
return 0;
}
memcpy(ctx->aad, in, ctx->aad_len);
ctx->aad[ctx->aad_len] = '\0';
XX_DPRINTF("AAD: |%s| (len=%d)", ctx->aad, ctx->aad_len);
n = sscanf(ctx->aad, "%[^$]$%d$%d$%[^$]%1[$]",
header, &hash, &iteration, salt_str, end);
if (n != 5) {
XX_DPRINTF("sscanf() failed");
return 0;
}
if (strcmp(header, HEADER_MARK) != 0) {
XX_DPRINTF("Invalid mark: %s", header);
return 0;
}
if (hash != PBKDF2_HASH_SHA512) {
XX_DPRINTF("Invalid hash: %d", hash);
return 0;
}
ctx->hash = hash;
if (iteration <= 0) {
XX_DPRINTF("Invalid iteration: %d", iteration);
return 0;
}
ctx->iteration = iteration;
if ((salt = OPENSSL_hexstr2buf(salt_str, &saltlen)) == NULL) {
XX_DPRINTF("OPENSSL_hexstr2buf() failed");
return 0;
}
if ((size_t)saltlen > sizeof(ctx->salt)) {
OPENSSL_free(salt);
XX_DPRINTF("Salt too long");
return 0;
}
memset(ctx->salt, 0, sizeof(ctx->salt));
memcpy(ctx->salt, salt, (size_t)saltlen);
OPENSSL_free(salt);
return 1;
}
static int
do_encrypt(xx_ctx *ctx)
{
EVP_CIPHER_CTX *cctx = NULL;
unsigned char *p;
int len;
if ((cctx = EVP_CIPHER_CTX_new()) == NULL) {
XX_DPRINTF("EVP_CIPHER_CTX_new() failed");
goto err;
}
if (!EVP_EncryptInit_ex(cctx, ctx->cipher, NULL /* engine */,
NULL /* key */, NULL /* IV */)) {
XX_DPRINTF("EVP_EncryptInit_ex(1) failed");
goto err;
}
if (!EVP_CIPHER_CTX_ctrl(cctx, EVP_CTRL_AEAD_SET_IVLEN,
sizeof(ctx->iv), NULL)) {
XX_DPRINTF("EVP_CIPHER_CTX_ctrl(SET_IVLEN) failed");
goto err;
}
if (!EVP_EncryptInit_ex(cctx, ctx->cipher, NULL /* engine */,
ctx->key, ctx->iv)) {
XX_DPRINTF("EVP_EncryptInit_ex(2) failed");
goto err;
}
len = 0;
if (!EVP_EncryptUpdate(cctx, NULL, &len, (unsigned char *)ctx->aad,
ctx->aad_len)) {
XX_DPRINTF("EVP_EncryptUpdate(AAD) failed");
goto err;
}
p = ctx->out;
if (!EVP_EncryptUpdate(cctx, p, &len, ctx->in, ctx->in_len)) {
XX_DPRINTF("EVP_EncryptUpdate(plaintext) failed");
goto err;
}
p += len;
ctx->out_len = len;
if (!EVP_EncryptFinal_ex(cctx, p, &len)) {
XX_DPRINTF("EVP_EncryptFinal_ex() failed");
goto err;
}
ctx->out_len += len;
XX_DPRINTF("ciphertext length: %d", ctx->out_len);
if (!EVP_CIPHER_CTX_ctrl(cctx, EVP_CTRL_AEAD_GET_TAG,
sizeof(ctx->tag), ctx->tag)) {
XX_DPRINTF("EVP_CIPHER_CTX_ctrl(GET_TAG) failed");
goto err;
}
EVP_CIPHER_CTX_free(cctx);
return 1;
err:
if (cctx) {
EVP_CIPHER_CTX_free(cctx);
}
return 0;
}
static int
do_decrypt(xx_ctx *ctx)
{
EVP_CIPHER_CTX *cctx = NULL;
unsigned char *p;
int len;
if ((cctx = EVP_CIPHER_CTX_new()) == NULL) {
XX_DPRINTF("EVP_CIPHER_CTX_new() failed");
goto err;
}
if (!EVP_DecryptInit_ex(cctx, ctx->cipher, NULL /* engine */,
NULL /* key */, NULL /* IV */)) {
XX_DPRINTF("EVP_DecryptInit_ex(1) failed");
goto err;
}
if (!EVP_CIPHER_CTX_ctrl(cctx, EVP_CTRL_AEAD_SET_IVLEN,
sizeof(ctx->iv), NULL)) {
XX_DPRINTF("EVP_CIPHER_CTX_ctrl(SET_IVLEN) failed");
goto err;
}
if (!EVP_DecryptInit_ex(cctx, ctx->cipher, NULL /* engine */,
ctx->key, ctx->iv)) {
XX_DPRINTF("EVP_DecryptInit_ex(2) failed");
goto err;
}
len = 0;
if (!EVP_DecryptUpdate(cctx, NULL, &len, (unsigned char *)ctx->aad,
ctx->aad_len)) {
XX_DPRINTF("EVP_DecryptUpdate(AAD) failed");
goto err;
}
p = ctx->out;
if (!EVP_DecryptUpdate(cctx, p, &len, ctx->in, ctx->in_len)) {
XX_DPRINTF("EVP_DecryptUpdate(ciphertext) failed");
goto err;
}
p += len;
ctx->out_len = len;
if (!EVP_CIPHER_CTX_ctrl(cctx, EVP_CTRL_AEAD_SET_TAG,
sizeof(ctx->tag), ctx->tag)) {
XX_DPRINTF("EVP_CIPHER_CTX_ctrl(SET_TAG) failed");
goto err;
}
if (EVP_DecryptFinal_ex(cctx, p, &len) <= 0) {;
XX_DPRINTF("EVP_DecryptFinal_ex() failed: tag didn't match");
goto err;
}
ctx->out_len += len;
XX_DPRINTF("plaintext length: %d", ctx->out_len);
EVP_CIPHER_CTX_free(cctx);
return 1;
err:
if (cctx) {
EVP_CIPHER_CTX_free(cctx);
}
return 0;
}
/*--------------------------------------------------------------------*/
int
xx_encrypt(const unsigned char *in, int in_len,
unsigned char **out, int *out_len)
{
unsigned char *buf = NULL;
unsigned char *p;
int buf_len;
xx_ctx ctx = { 0 };
if (!init_ctx(&ctx)) {
XX_DPRINTF("init_ctx() failed");
goto err;
}
if (!derive_key_iv(&ctx)) {
XX_DPRINTF("derive_key_iv() failed");
goto err;
}
ctx.in = in;
ctx.in_len = in_len;
ctx.out = calloc(1, ctx.in_len + EVP_MAX_BLOCK_LENGTH);
if (ctx.out == NULL) {
XX_DPRINTF("calloc(ctx.out)");
goto err;
}
if (!do_encrypt(&ctx)) {
XX_DPRINTF("do_encrypt() failed");
goto err;
}
buf_len = ctx.aad_len + sizeof(ctx.tag) + ctx.out_len;
if ((buf = calloc(1, buf_len)) == NULL) {
XX_DPRINTF("calloc() failed");
goto err;
}
p = buf;
/* AAD */
memcpy(p, ctx.aad, ctx.aad_len);
p += ctx.aad_len;
/* Tag */
memcpy(p, ctx.tag, sizeof(ctx.tag));
p += sizeof(ctx.tag);
/* Ciphertext */
memcpy(p, ctx.out, ctx.out_len);
*out = buf;
*out_len = buf_len;
free(ctx.out);
return 1;
err:
if (ctx.out) {
free(ctx.out);
}
if (buf) {
free(buf);
}
return 0;
}
int
xx_decrypt(const unsigned char *in, int in_len,
unsigned char **out, int *out_len)
{
xx_ctx ctx = { 0 };
if (!init_ctx(&ctx)) {
XX_DPRINTF("init_ctx() failed");
goto err;
}
ctx.in = in;
ctx.in_len = in_len;
if (!read_header(&ctx, ctx.in, ctx.in_len)) {
XX_DPRINTF("read_header() failed");
goto err;
}
ctx.in += ctx.aad_len;
ctx.in_len -= ctx.aad_len;
if ((size_t)ctx.in_len < sizeof(ctx.tag)) {
XX_DPRINTF("invalid file (tag)");
goto err;
}
memcpy(ctx.tag, ctx.in, sizeof(ctx.tag));
ctx.in += sizeof(ctx.tag);
ctx.in_len -= sizeof(ctx.tag);
if (!derive_key_iv(&ctx)) {
XX_DPRINTF("derive_key_iv() failed");
goto err;
}
ctx.out = calloc(1, ctx.in_len);
if (ctx.out == NULL) {
XX_DPRINTF("calloc(ctx.out)");
goto err;
}
if (!do_decrypt(&ctx)) {
XX_DPRINTF("do_decrypt() failed");
goto err;
}
*out = ctx.out;
*out_len = ctx.out_len;
return 1;
err:
if (ctx.out) {
free(ctx.out);
}
return 0;
}
/*-
* SPDX-License-Identifier: MIT
*
* Copyright (c) 2022-2023 Aaron LI
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject
* to the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/*
* XX crypto utilities: Chacha20-Poly1305 AEAD with PBKDF2 key derivation.
*/
#ifndef XX_CRYPTO_H_
#define XX_CRYPTO_H_
/*
* Encrypt the data $in of length $in_len.
* The encrypted data is stored in $out of length $out_len,
* which should be free()'ed by the caller.
*
* Return 1 on success, 0 on error.
*/
int xx_encrypt(const unsigned char *in, int in_len,
unsigned char **out, int *out_len);
/*
* Decrypt the data $in of length $in_len.
* The decrypted data is stored in $out of length $out_len,
* which should be free()'ed by the caller.
*
* Return 1 on success, 0 on error.
*/
int xx_decrypt(const unsigned char *in, int in_len,
unsigned char **out, int *out_len);
#endif
@liweitianux
Copy link
Author

liweitianux commented Aug 24, 2023

Compile the encryptor.c with:

$ cc -g -O2 -Wall -Wextra \
      -DXX_PASSWORD='"<your-private-password>"' \
      $(pkg-config --cflags libcrypto) \
      -o encryptor encryptor.c xx_crypto.c \
      $(pkg-config --libs libcrypto)

(NOTE: replace the above <your-private-password> with yours.)

Usage help:

$ ./encryptor -h
Simple file encryptor/decryptor

usage: ./encryptor [-d] [-i <infile>] [-o <outfile>]

options:
    -d: do decryption instead of encryption
    -i <infile>: input file (stdin if unspecified)
    -o <outfile>: outfile file (stdout if unspecified)
    -v: show verbose info

Example:

% printf "Hello, world.\n" | ./encryptor | ./encryptor -d
Hello, world.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment