My post on how to do basic AES and RSA encryption has, for a while now, been one of the most popular posts on my blog, but I continually get questions about why people can’t print out the encrypted messages like a normal string or write them to a file using fprintf()
. The short answer is that encrypted messages are binary data, not ASCII strings with a NUL terminator and thus, they can’t be treated as if they’re ASCII data with a NUL terminator. You might be saying, “but I want to send an encrypted message to my friend as ASCII!”.
Well, time for base64.
We can use base64 to encode our encrypted messages into ASCII strings and then back again to binary data for decryption. OpenSSL has a way of doing this for us:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
char* base64Encode(const unsigned char *message, const size_t length) {
BIO *bio;
BIO *b64;
FILE* stream;
int encodedSize = 4*ceil((double)length/3);
char *buffer = (char*)malloc(encodedSize+1);
if(buffer == NULL) {
fprintf(stderr, "Failed to allocate memory\n");
exit(1);
}
stream = fmemopen(buffer, encodedSize+1, "w");
b64 = BIO_new(BIO_f_base64());
bio = BIO_new_fp(stream, BIO_NOCLOSE);
bio = BIO_push(b64, bio);
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
BIO_write(bio, message, length);
(void)BIO_flush(bio);
BIO_free_all(bio);
fclose(stream);
return buffer;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
int base64Decode(const char *b64message, const size_t length, unsigned char **buffer) {
BIO *bio;
BIO *b64;
int decodedLength = calcDecodeLength(b64message, length);
*buffer = (unsigned char*)malloc(decodedLength+1);
if(*buffer == NULL) {
fprintf(stderr, "Failed to allocate memory\n");
exit(1);
}
FILE* stream = fmemopen((char*)b64message, length, "r");
b64 = BIO_new(BIO_f_base64());
bio = BIO_new_fp(stream, BIO_NOCLOSE);
bio = BIO_push(b64, bio);
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
decodedLength = BIO_read(bio, *buffer, length);
(*buffer)[decodedLength] = '\0';
BIO_free_all(bio);
fclose(stream);
return decodedLength;
}
int calcDecodeLength(const char *b64input, const size_t length) {
int padding = 0;
// Check for trailing '=''s as padding
if(b64input[length-1] == '=' && b64input[length-2] == '=')
padding = 2;
else if (b64input[length-1] == '=')
padding = 1;
return (int)length*0.75 - padding;
}
Let’s say we have some encrypted data in a buffer called “encryptedFile”. We can encode it with base64 as such:
1
2
char *base64Buffer;
base64Buffer = base64Encode(encryptedFile, encryptedFileLength);
And now we can use base64Buffer as a normal C-string with printf()
, strlen()
, etc.
What about writing binary data to a file?
This basically comes down to using fwrite()
instead of the fprintf()
you might use to write ASCII strings to a file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void writeFile(char *filename, unsigned char *file, size_t fileLength) {
FILE *fd = fopen(filename, "wb");
if(fd == NULL) {
fprintf(stderr, "Failed to open file: %s\n", strerror(errno));
exit(1);
}
size_t bytesWritten = fwrite(file, 1, fileLength, fd);
if(bytesWritten != fileLength) {
fprintf(stderr, "Failed to write file\n");
exit(1);
}
fclose(fd);
}
Reading back the file with fread()
involves knowing the size of the file first so we know how many bytes to read. That’s simple enough to do with the fseek()
function once the file is opened, however.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int readFile(char *filename, unsigned char **file) {
FILE *fd = fopen(filename, "rb");
if(fd == NULL) {
fprintf(stderr, "Failed to open file: %s\n", strerror(errno));
exit(1);
}
// Determine size of the file
fseek(fd, 0, SEEK_END);
size_t fileLength = ftell(fd);
fseek(fd, 0, SEEK_SET);
// Allocate space for the file
*file = (unsigned char*)malloc(fileLength);
if(*file == NULL) {
fprintf(stderr, "Failed to allocate memory\n");
exit(1);
}
// Read the file into the buffer
size_t bytesRead = fread(*file, 1, fileLength, fd);
if(bytesRead != fileLength) {
fprintf(stderr, "Error reading file\n");
exit(1);
}
fclose(fd);
return fileLength;
}
Knowing all this, we can now read/write encrypted data to files and encode/decode encrypted data to base64 to use it as C-strings. With this knowledge we can do something fun, like, read in an arbitrary file, encrypt it, save it to a file, and then read it back and decrypt and save it to another file.
See the full example on GitHub.
The only problem with this is that it will not scale to very large files since it reads the entire file into memory before encrypting it. A better to do this would be to read a chunk, encrypt it, write the encrypted chunk to a file and so on until the entire has been encrypted.