-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathencryption_test.js
144 lines (111 loc) · 7.22 KB
/
encryption_test.js
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
const crypto = require('crypto');
var ByteBuffer = require('byte-buffer');
const SettingsManager = require("./settingsManager.js");
const LogLib = require("./Log.js");
const SALT_LENGTH = 8; //8-byte salt
const IV_LENGTH = 12; //12-byte IV
const KEY_LENGTH = 16; //The encryption uses a 16-byte key
const AUTHTAG_LENGTH = 16;
const KEY_ITERATIONS = 10000;
const KEY_DIGEST = 'sha256';
const ENCRYPTION_ALGORITHM = 'aes-128-gcm';
exports.SALT_LENGTH = SALT_LENGTH;
exports.IV_LENGTH = IV_LENGTH;
exports.SKEY_LENGTH = KEY_LENGTH;
//TODO: What about crashes when the password is wrong?
//TODO: Add error handling and retrying for "Unsupported state or unable to authenticate data"
function decryptThisAlreadyDangit(message) {
//The incoming message should have the salt, iv, and encrypted message (encrypted message = ciphertext stuck with authTag) all stuck together.
//The following variables slice apart the message into its respective components
var salt = message.slice(0, SALT_LENGTH);
var iv = message.slice(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
var encrypted = message.slice(SALT_LENGTH + IV_LENGTH, message.length);
//What message is sent back if the password is wrong?
return decryptWithSaltIVAndData(salt, iv, encrypted); //This returns a Promise
}
function decryptWithSaltIVAndData(salt, iv, encryptedWithAuthTag) {
var Log = new LogLib.Log("encryption_test.js", "decryptWithSaltIVAndData");
return new Promise(async (resCb, rejCb) => {
var password = await SettingsManager.readSetting("AIRMESSAGE_PASSWORD");
var encrypted = encryptedWithAuthTag.slice(0, encryptedWithAuthTag.length - AUTHTAG_LENGTH);
var authTag = encryptedWithAuthTag.slice(encryptedWithAuthTag.length - AUTHTAG_LENGTH, encryptedWithAuthTag.length);
//For more info on the authTag business, check out the encryption function for more details.
//The authTag is the last 16 byes of the encrypted message
crypto.pbkdf2(password, salt, KEY_ITERATIONS, KEY_LENGTH, KEY_DIGEST, (err, derivedKey) => {
// This gets us the key. It's a symmetric key, so used for both encryption and decryption
// This converts the user-supplied AirMessage password into the key that was used to encrypt the message
var decipher = crypto.createDecipheriv(ENCRYPTION_ALGORITHM, derivedKey, iv); //Create a cipher with the key and iv
decipher.setAuthTag(authTag); //The infamous authTag. See the encrypt function for more info
var decrypted = new ByteBuffer(); //Creates a byte buffer to add the encrypted data to.
var decryptedchunk = decipher.update(encrypted); //This pipes our encrypted data into the cipher and gets the decrypted data.
decrypted.append(decryptedchunk.length); //Allocates space for the decrypted chunk in the byte buffer
decrypted.write(decryptedchunk); //Writes the decrypted data to the byte buffer
try {
decipher.final(); //Finishes the encryption. Some encryption methods put some extra stuff at the end, but
//GCM doesn't, so cipher.final() just returns an empty buffer every time.
//TODO: Add error handling to this where it tries to decrypt again
} catch (err) {
Log.w(err);
}
resCb(decrypted.raw); //Returns the decrypted data
});
});
}
function encrypt(data) {
// This drove me crazy for a solid week, so I'm letting you know what the heck this is doing
// So you hopefully don't have to deal with the same crap I did:
//
// So GCM encryption is weird. In addition to requiring the salt, IV, and encrypted data, it
// also likes to have something called an authTag. This is basically a checksum that's signed
// by the encryption key (I think). It's not strictly necessary to decrypt the data, but many
// programming languages (ex. Java) expect it in order to decrypt anything at all.
//
// So, the decryption should work fine if you only care about the encrypted data and pay no
// attention to the authTag. HOWEVER, if you try to encrypt data without including the authTag
// at the end (it's usually 16 bytes), many other programming languages (cough cough, Java) that
// expect the authTag will get real mad and crash. Node for some reason doesn't include it by
// default at the end of the encrypted data, so you need to call cipher.getAuthTag() and stick
// the buffer to the end manually. If you don't do this, you'll spend a week wondering why other
// languages output encrypted data that's 16 bytes longer.
return new Promise(async(resCb, rejCb) => {
var password = await SettingsManager.readSetting("AIRMESSAGE_PASSWORD");
var salt = crypto.randomBytes(SALT_LENGTH); //Generates a random 8-byte salt used to derive the key from the password
crypto.pbkdf2(password, salt, KEY_ITERATIONS, KEY_LENGTH, KEY_DIGEST, (err, derivedKey) => {
// This gets us the key. It's a symmetric key, so used for both encryption and decryption
// This converts the user-supplied AirMessage password into the key that was used to encrypt the message
var iv = crypto.randomBytes(IV_LENGTH); //Generate 12 bytes of secure random noise for the initialization vector
var cipher = crypto.createCipheriv(ENCRYPTION_ALGORITHM, derivedKey, iv); //Create a cipher with the key and iv
// cipher.setAutoPadding(true);
var encrypted = new ByteBuffer(); //Creates a byte buffer to add the encrypted data to.
var encryptedchunk = cipher.update(data); //This pipes our unencrypted data into the cipher and gets the encrypted data.
encrypted.append(encryptedchunk.length); //Allocates space for the encrypted chunk in the byte buffer
encrypted.write(encryptedchunk); //Writes the encrypted data to the byte buffer
cipher.final(); //Finishes the encryption. Some encryption methods have some extra stuff at the end, but
//GCM doesn't, so cipher.final() just returns an empty buffer every time.
var authTag = cipher.getAuthTag(); //Gets the infamous authTag. Should be 16 bytes every time.
encrypted.append(authTag.length); //Allocates space for the AuthTag. Should always be 16 bytes
encrypted.write(authTag); //Writes the authTag right up next to the encrypted data.
resCb([salt, iv, encrypted.raw]);
});
});
}
// decryptThisAlreadyDangit(message).then((data) => {
// testbuf = new Buffer(data);
// console.log(testbuf.toString('hex').match(/../g).join(' '));
// });
exports.decryptThisAlreadyDangit = decryptThisAlreadyDangit;
exports.decryptWithSaltIVAndData = decryptWithSaltIVAndData;
exports.encrypt = encrypt;
// (async function() {
//
// console.log("encrypting");
// var encrypted = await encrypt(dataToEncrypt);
// console.log("going to print encrypted");
// console.log(encrypted);
// console.log("decrypting");
// var decrypted = await decryptWithSaltIVAndData(encrypted[0], encrypted[1], encrypted[2]);
// //You can also run decryptThisAlreadyDangit(message) if it contains the salt and IV
// console.log(decrypted);
//
//
// })();