Skip to content

Commit f72f013

Browse files
New verisoned AES ciphertext format.
1 parent 7462310 commit f72f013

File tree

9 files changed

+305
-38
lines changed

9 files changed

+305
-38
lines changed

format-tests/.gitignore

+132
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
2+
## Node.js
3+
4+
# Logs
5+
logs
6+
*.log
7+
npm-debug.log*
8+
yarn-debug.log*
9+
yarn-error.log*
10+
lerna-debug.log*
11+
12+
# Diagnostic reports (https://nodejs.org/api/report.html)
13+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
14+
15+
# Runtime data
16+
pids
17+
*.pid
18+
*.seed
19+
*.pid.lock
20+
21+
# Directory for instrumented libs generated by jscoverage/JSCover
22+
lib-cov
23+
24+
# Coverage directory used by tools like istanbul
25+
coverage
26+
*.lcov
27+
28+
# nyc test coverage
29+
.nyc_output
30+
31+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
32+
.grunt
33+
34+
# Bower dependency directory (https://bower.io/)
35+
bower_components
36+
37+
# node-waf configuration
38+
.lock-wscript
39+
40+
# Compiled binary addons (https://nodejs.org/api/addons.html)
41+
build/Release
42+
43+
# Dependency directories
44+
node_modules/
45+
jspm_packages/
46+
47+
# Snowpack dependency directory (https://snowpack.dev/)
48+
web_modules/
49+
50+
# TypeScript cache
51+
*.tsbuildinfo
52+
53+
# Optional npm cache directory
54+
.npm
55+
56+
# Optional eslint cache
57+
.eslintcache
58+
59+
# Microbundle cache
60+
.rpt2_cache/
61+
.rts2_cache_cjs/
62+
.rts2_cache_es/
63+
.rts2_cache_umd/
64+
65+
# Optional REPL history
66+
.node_repl_history
67+
68+
# Output of 'npm pack'
69+
*.tgz
70+
71+
# Yarn Integrity file
72+
.yarn-integrity
73+
74+
# dotenv environment variables file
75+
.env
76+
.env.test
77+
78+
# parcel-bundler cache (https://parceljs.org/)
79+
.cache
80+
.parcel-cache
81+
82+
# Next.js build output
83+
.next
84+
out
85+
86+
# Nuxt.js build / generate output
87+
.nuxt
88+
dist
89+
90+
# Gatsby files
91+
.cache/
92+
# Comment in the public line in if your project uses Gatsby and not Next.js
93+
# https://nextjs.org/blog/next-9-1#public-directory-support
94+
# public
95+
96+
# vuepress build output
97+
.vuepress/dist
98+
99+
# Serverless directories
100+
.serverless/
101+
102+
# FuseBox cache
103+
.fusebox/
104+
105+
# DynamoDB Local files
106+
.dynamodb/
107+
108+
# TernJS port file
109+
.tern-port
110+
111+
# Stores VSCode versions used for testing VSCode extensions
112+
.vscode-test
113+
114+
# yarn v2
115+
.yarn/cache
116+
.yarn/unplugged
117+
.yarn/build-state.yml
118+
.yarn/install-state.gz
119+
.pnp.*
120+
© 2020 GitHub, Inc.
121+
Terms
122+
Privacy
123+
Security
124+
Status
125+
Help
126+
Contact GitHub
127+
Pricing
128+
API
129+
Training
130+
Blog
131+
About
132+

format-tests/README

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
This directory contains code purely used for testing cryptography formats in
2+
other languages.
3+
4+
For python, the cryptography library is used, installed via:
5+
6+
pip3 install cryptography
7+
8+
For javascript, the crypto-js and buffer libraries are used, installed via:
9+
10+
npm install
11+

format-tests/aes_decrypt.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
#!/usr/bin/env node
2+
3+
// adapted from https://gist.github.com/rjz/15baffeab434b8125ca4d783f4116d81
4+
5+
const buffer = require('buffer');
6+
const crypto = require('crypto');
7+
8+
var keyHex = '23B3E2B8FFB06B330750AABF727B55E3D31E19AA1AAA77D0E70988F57FE2FD82'
9+
var ciphertextHex = '010C1A6CB9E30EEDB700FD101B03109266C9A8D67FD85D405C6FA9620D0954F8FF64E1'
10+
var plaintext = 'Test'
11+
12+
const ALGO = 'aes-256-gcm';
13+
const decrypt = (key, enc, iv, authTag) => {
14+
const decipher = crypto.createDecipheriv(ALGO, key, iv);
15+
decipher.setAuthTag(authTag);
16+
let str = decipher.update(enc, 'base64', 'utf8');
17+
str += decipher.final('utf8');
18+
return str;
19+
};
20+
21+
const key = Buffer.from(keyHex, 'hex');
22+
const ciphertext = Buffer.from(ciphertextHex, 'hex')
23+
24+
const formatVersion = ciphertext.readInt8(0);
25+
const ivLength = ciphertext.readInt8(1);
26+
const ivEnd = 2+ivLength;
27+
28+
const iv = ciphertext.slice(2, ivEnd);
29+
const tagLength = ciphertext.readInt8(ivEnd);
30+
const tagEnd = ivEnd + tagLength + 1;
31+
const authenticationTag = ciphertext.slice(ivEnd+1, tagEnd);
32+
const encryptedData = ciphertext.slice(tagEnd);
33+
34+
var decrypted;
35+
try {
36+
decrypted = decrypt(key, encryptedData, iv, authenticationTag);
37+
}
38+
catch (error) {
39+
console.error(error);
40+
}
41+
console.log((decrypted == plaintext) ? "Pass" : "Fail");
42+
43+

format-tests/aes_decrypt.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env python3
2+
3+
import binascii
4+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
5+
6+
key = binascii.unhexlify('23B3E2B8FFB06B330750AABF727B55E3D31E19AA1AAA77D0E70988F57FE2FD82')
7+
ciphertext = binascii.unhexlify('010C1A6CB9E30EEDB700FD101B03109266C9A8D67FD85D405C6FA9620D0954F8FF64E1')
8+
plaintext = b'Test'
9+
10+
formatVersion = int(ciphertext[0])
11+
ivLength = int(ciphertext[1])
12+
ivEnd = 2 + ivLength
13+
14+
iv = ciphertext[2:ivEnd]
15+
tagLength = int(ciphertext[ivEnd])
16+
tagEnd = ivEnd + tagLength + 1
17+
authenticationTag = ciphertext[ivEnd+1:tagEnd]
18+
encryptedData = ciphertext[tagEnd:]
19+
20+
aesGcmKey = AESGCM(key)
21+
message = aesGcmKey.decrypt(iv, encryptedData + authenticationTag, None)
22+
print('Pass' if (plaintext == message) else 'Fail')

format-tests/aes_encrypt.py

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#!/usr/bin/env python3
2+
3+
import binascii
4+
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
5+
6+
key = binascii.unhexlify('23B3E2B8FFB06B330750AABF727B55E3D31E19AA1AAA77D0E70988F57FE2FD82')
7+
iv = binascii.unhexlify('1A6CB9E30EEDB700FD101B03')
8+
plaintext = b'Test'
9+
expected= binascii.unhexlify('010C1A6CB9E30EEDB700FD101B03109266C9A8D67FD85D405C6FA9620D0954F8FF64E1')
10+
11+
aesGcmKey = AESGCM(key)
12+
ciphertext = aesGcmKey.encrypt(iv, plaintext, None)
13+
14+
encryptedData = ciphertext[:-16]
15+
authenticationTag = ciphertext[-16:]
16+
17+
output = bytes([1]) + bytes([len(iv)]) + iv + bytes([len(authenticationTag)]) + authenticationTag + encryptedData
18+
19+
print('Pass' if (output == expected) else 'Fail')
20+

format-tests/package-lock.json

+32
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

format-tests/package.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "format-tests",
3+
"version": "1.0.0",
4+
"description": "This directory contains code purely used for testing cryptography formats in other languages.",
5+
"main": "aes_decrypt.js",
6+
"dependencies": {
7+
"buffer": "^5.6.0",
8+
"crypto-js": "^4.0.0"
9+
},
10+
"devDependencies": {},
11+
"scripts": {
12+
"test": "echo \"Error: no test specified\" && exit 1"
13+
},
14+
"keywords": [],
15+
"author": "",
16+
"license": "ISC"
17+
}

src/main/java/com/softwareverde/cryptography/aes/AesKey.java

+23-20
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.softwareverde.logging.Logger;
44
import com.softwareverde.util.ByteUtil;
55
import com.softwareverde.util.bytearray.ByteArrayBuilder;
6+
import com.softwareverde.util.bytearray.ByteArrayReader;
67

78
import javax.crypto.Cipher;
89
import javax.crypto.KeyGenerator;
@@ -17,9 +18,10 @@
1718
public class AesKey {
1819
public static final int DEFAULT_KEY_SIZE = 256;
1920

21+
protected static final byte FORMAT_VERSION = 1;
22+
2023
private static final String KEY_ALGORITHM = "AES";
2124
private static final String ENCRYPTION_CIPHER = "AES/GCM/NoPadding"; // Using GCM instead of CBC as it provides authentication
22-
private static final int LEGACY_AUTHENTICATION_TAG_LENGTH = 12; // default is tag length is not specified
2325
private static final int AUTHENTICATION_TAG_LENGTH = 16; // max allowed value
2426
private static final int INITIALIZATION_VECTOR_LENGTH = 12; // IV size of 12-bytes is specifically recommended for AES-GCM (more efficient than other lengths)
2527

@@ -81,17 +83,19 @@ public byte[] encrypt(byte[] plainText) {
8183
final byte[] initializationVectorBytes = _createInitializationVector();
8284
final AlgorithmParameterSpec initializationVector = new GCMParameterSpec(AUTHENTICATION_TAG_LENGTH * Byte.SIZE, initializationVectorBytes);
8385
aesCipher.init(Cipher.ENCRYPT_MODE, _key, initializationVector);
84-
final byte[] cipherText = aesCipher.doFinal(plainText);
86+
final byte[] javaCipherText = aesCipher.doFinal(plainText);
87+
final byte[] cipherText = Arrays.copyOfRange(javaCipherText, 0, javaCipherText.length - AUTHENTICATION_TAG_LENGTH);
88+
final byte[] authenticationTag = Arrays.copyOfRange(javaCipherText, javaCipherText.length - AUTHENTICATION_TAG_LENGTH, javaCipherText.length);
8589

8690
// prefix cipher text with initialization vector
8791
final ByteArrayBuilder byteArrayBuilder = new ByteArrayBuilder();
88-
byteArrayBuilder.appendByte((byte) (0x80 | initializationVectorBytes.length));
89-
byteArrayBuilder.appendByte((byte) AUTHENTICATION_TAG_LENGTH);
92+
byteArrayBuilder.appendByte(FORMAT_VERSION);
93+
byteArrayBuilder.appendByte((byte) initializationVectorBytes.length);
9094
byteArrayBuilder.appendBytes(initializationVectorBytes);
95+
byteArrayBuilder.appendByte((byte) AUTHENTICATION_TAG_LENGTH);
96+
byteArrayBuilder.appendBytes(authenticationTag);
9197
byteArrayBuilder.appendBytes(cipherText);
9298

93-
Arrays.fill(initializationVectorBytes, (byte) 0);
94-
9599
return byteArrayBuilder.build();
96100
}
97101
catch (final Exception e) {
@@ -103,24 +107,23 @@ public byte[] encrypt(byte[] plainText) {
103107
public byte[] decrypt(byte[] cipherText) {
104108
try {
105109
// remove initialization vector from cipher text
106-
byte initializationVectorLength = cipherText[0];
107-
int authenticationTagLength = LEGACY_AUTHENTICATION_TAG_LENGTH;
108-
int ivStartIndex = 1;
109-
if ((initializationVectorLength & 0x80) != 0) {
110-
initializationVectorLength = (byte) (initializationVectorLength ^ 0x80);
111-
authenticationTagLength = cipherText[1];
112-
ivStartIndex = 2;
113-
}
114-
int cipherTextOffset = ByteUtil.byteToInteger(initializationVectorLength) + ivStartIndex;
115-
final byte[] initializationVectorBytes = Arrays.copyOfRange(cipherText, ivStartIndex, cipherTextOffset);
110+
final ByteArrayReader byteArrayReader = new ByteArrayReader(cipherText);
111+
byte formatVersion = byteArrayReader.readByte();
112+
byte initializationVectorLength = byteArrayReader.readByte();
113+
final byte[] initializationVectorBytes = byteArrayReader.readBytes((int) initializationVectorLength);
114+
byte authenticationTagLength = byteArrayReader.readByte();
115+
byte[] authenticationTag = byteArrayReader.readBytes((int) authenticationTagLength);
116+
final byte[] encryptedData = byteArrayReader.readBytes(byteArrayReader.remainingByteCount());
117+
118+
final byte[] javaCipherText = new byte[encryptedData.length + authenticationTagLength];
119+
ByteUtil.setBytes(javaCipherText, encryptedData, 0);
120+
ByteUtil.setBytes(javaCipherText, authenticationTag, encryptedData.length);
121+
116122
final AlgorithmParameterSpec initializationVector = new GCMParameterSpec(authenticationTagLength * Byte.SIZE, initializationVectorBytes);
117-
final byte[] encryptedData = Arrays.copyOfRange(cipherText, cipherTextOffset, cipherText.length);
118123

119124
final Cipher aesCipher = Cipher.getInstance(ENCRYPTION_CIPHER);
120125
aesCipher.init(Cipher.DECRYPT_MODE, _key, initializationVector);
121-
final byte[] plainText = aesCipher.doFinal(encryptedData);
122-
123-
Arrays.fill(initializationVectorBytes, (byte) 0);
126+
final byte[] plainText = aesCipher.doFinal(javaCipherText);
124127

125128
return plainText;
126129
}

0 commit comments

Comments
 (0)