Skip to content

Commit c7d17fa

Browse files
committed
add webcrypto as an option
1 parent 8345508 commit c7d17fa

File tree

10 files changed

+927
-177
lines changed

10 files changed

+927
-177
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.idea
2+
.vscode/
23
node_modules
34
.staticrypt.json
45
/www/password_template.html

cli/index.js

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@
55
const fs = require("fs");
66
const path = require("path");
77
const Yargs = require("yargs");
8-
const impl = require("../lib/impl-cryptojs");
98
const codec = require("../lib/codec");
10-
const { generateRandomSalt } = impl;
11-
const { encode } = codec.init(impl);
129

1310
const SCRIPT_URL =
1411
"https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js";
@@ -145,9 +142,23 @@ const yargs = Yargs.usage("Usage: staticrypt <filename> <passphrase> [options]")
145142
type: "string",
146143
describe: "Title for output HTML page.",
147144
default: "Protected Page",
145+
})
146+
.option("implementation", {
147+
alias: "impl",
148+
describe:
149+
'Choose between library-based CryptoJS and native WebCrypto. ' +
150+
'Most browsers only support WebCrypto in secure contexts like localhost or HTTPS',
151+
type: "string",
152+
choices: ["cryptojs", "webcrypto"],
153+
default: "cryptojs",
148154
});
149155
const namedArgs = yargs.argv;
150156

157+
const impl = (namedArgs.impl === "cryptojs") ?
158+
require("../lib/impl-cryptojs") : require("../lib/impl-webcrypto");
159+
const {generateRandomSalt} = impl;
160+
const {encode} = codec.init(impl);
161+
151162
// if the 's' flag is passed without parameter, generate a salt, display & exit
152163
if (isOptionSetByUser("s", yargs) && !namedArgs.salt) {
153164
console.log(generateRandomSalt());
@@ -213,9 +224,6 @@ try {
213224
process.exit(1);
214225
}
215226

216-
// encrypt input
217-
const encryptedMessage = encode(contents, passphrase, salt);
218-
219227
// create crypto-js tag (embedded or not)
220228
let cryptoTag = SCRIPT_TAG;
221229
if (namedArgs.embed) {
@@ -232,27 +240,31 @@ if (namedArgs.embed) {
232240
}
233241
}
234242

235-
const data = {
236-
codec_iif: transcludeModule("../lib/codec"),
237-
crypto_tag: cryptoTag,
238-
decrypt_button: namedArgs.decryptButton,
239-
embed: namedArgs.embed,
240-
encrypted: encryptedMessage,
241-
impl_iif: transcludeModule("../lib/impl-cryptojs"),
242-
instructions: namedArgs.instructions,
243-
is_remember_enabled: namedArgs.noremember ? "false" : "true",
244-
output_file_path:
245-
namedArgs.output !== null
246-
? namedArgs.output
247-
: input.replace(/\.html$/, "") + "_encrypted.html",
248-
passphrase_placeholder: namedArgs.passphrasePlaceholder,
249-
remember_duration_in_days: namedArgs.remember,
250-
remember_me: namedArgs.rememberLabel,
251-
salt: salt,
252-
title: namedArgs.title,
253-
};
243+
// encrypt input
244+
encode(contents, passphrase, salt).then((encryptedMessage) => {
245+
const implModulePath = (namedArgs.impl === "cryptojs") ? "../lib/impl-cryptojs" : "../lib/impl-webcrypto";
246+
const data = {
247+
codec_iif: transcludeModule("../lib/codec"),
248+
crypto_tag: cryptoTag,
249+
decrypt_button: namedArgs.decryptButton,
250+
embed: namedArgs.embed,
251+
encrypted: encryptedMessage,
252+
impl_iif: transcludeModule(implModulePath),
253+
instructions: namedArgs.instructions,
254+
is_remember_enabled: namedArgs.noremember ? "false" : "true",
255+
output_file_path:
256+
namedArgs.output !== null
257+
? namedArgs.output
258+
: input.replace(/\.html$/, "") + "_encrypted.html",
259+
passphrase_placeholder: namedArgs.passphrasePlaceholder,
260+
remember_duration_in_days: namedArgs.remember,
261+
remember_me: namedArgs.rememberLabel,
262+
salt: salt,
263+
title: namedArgs.title,
264+
};
254265

255-
genFile(data);
266+
genFile(data);
267+
});
256268

257269
/**
258270
* Fill the template with provided data and writes it to output file.

example/example_encrypted.html

Lines changed: 61 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,15 @@
168168
var impl = ((function(){
169169
const exports = {};
170170

171+
var IV_BITS = 16 * 8;
172+
var HEX_BITS = 4;
173+
171174
/**
172175
* Salt and encrypt a msg with a password.
173176
* Inspired by https://github.com/adonespitogo
174177
*/
175178
function encrypt(msg, hashedPassphrase) {
176-
var iv = CryptoJS.lib.WordArray.random(128 / 8);
179+
var iv = CryptoJS.lib.WordArray.random(IV_BITS / 8);
177180

178181
var encrypted = CryptoJS.AES.encrypt(msg, hashedPassphrase, {
179182
iv: iv,
@@ -183,7 +186,8 @@
183186

184187
// iv will be hex 16 in length (32 characters)
185188
// we prepend it to the ciphertext for use in decryption
186-
return iv.toString() + encrypted.toString();
189+
var encoded = iv.toString() + encrypted.toString();
190+
return Promise.resolve(encoded);
187191
}
188192
exports.encrypt = encrypt;
189193

@@ -196,14 +200,17 @@
196200
* @returns {string}
197201
*/
198202
function decrypt(encryptedMsg, hashedPassphrase) {
199-
var iv = CryptoJS.enc.Hex.parse(encryptedMsg.substr(0, 32));
200-
var encrypted = encryptedMsg.substring(32);
203+
var ivLength = IV_BITS / HEX_BITS;
204+
var iv = CryptoJS.enc.Hex.parse(encryptedMsg.substr(0, ivLength));
205+
var encrypted = encryptedMsg.substring(ivLength);
201206

202-
return CryptoJS.AES.decrypt(encrypted, hashedPassphrase, {
207+
const decrypted = CryptoJS.AES.decrypt(encrypted, hashedPassphrase, {
203208
iv: iv,
204209
padding: CryptoJS.pad.Pkcs7,
205210
mode: CryptoJS.mode.CBC,
206211
}).toString(CryptoJS.enc.Utf8);
212+
213+
return Promise.resolve(decrypted);
207214
}
208215
exports.decrypt = decrypt;
209216

@@ -220,7 +227,7 @@
220227
iterations: 1000,
221228
});
222229

223-
return hashedPassphrase.toString();
230+
return Promise.resolve(hashedPassphrase.toString());
224231
}
225232
exports.hashPassphrase = hashPassphrase;
226233

@@ -230,10 +237,11 @@
230237
exports.generateRandomSalt = generateRandomSalt;
231238

232239
function signMessage(hashedPassphrase, message) {
233-
return CryptoJS.HmacSHA256(
240+
const signature = CryptoJS.HmacSHA256(
234241
message,
235242
CryptoJS.SHA256(hashedPassphrase).toString()
236243
).toString();
244+
return Promise.resolve(signature);
237245
}
238246
exports.signMessage = signMessage;
239247

@@ -254,15 +262,18 @@
254262
* @param {string} msg
255263
* @param {string} passphase
256264
* @param {sting} salt
257-
* @returns {string} The encoded text
265+
* @returns {Promise<string>} The encoded text
258266
*/
259267
function encode(msg, passphrase, salt) {
260-
const hashedPassphrase = impl.hashPassphrase(passphrase, salt);
261-
const encrypted = impl.encrypt(msg, hashedPassphrase);
262-
// we use the hashed passphrase in the HMAC because this is effectively what will be used a passphrase (so we can store
263-
// it in localStorage safely, we don't use the clear text passphrase)
264-
const hmac = impl.signMessage(hashedPassphrase, encrypted);
265-
return hmac + encrypted;
268+
return impl.hashPassphrase(passphrase, salt).then(function(hashedPassphrase) {
269+
return impl.encrypt(msg, hashedPassphrase).then(function(encrypted) {
270+
return impl.signMessage(hashedPassphrase, encrypted).then(function(hmac) {
271+
// we use the hashed passphrase in the HMAC because this is effectively what will be used a passphrase (so we can store
272+
// it in localStorage safely, we don't use the clear text passphrase)
273+
return hmac + encrypted;
274+
});
275+
});
276+
});
266277
}
267278
exports.encode = encode;
268279

@@ -272,20 +283,22 @@
272283
*
273284
* @param {*} encoded
274285
* @param {*} hashedPassphrase
275-
* @returns {Object} {success: true, decoded: string} | {succss: false, message: string}
286+
* @returns {Promise<Object>} {success: true, decoded: string} | {succss: false, message: string}
276287
*/
277288
function decode(signedMsg, hashedPassphrase) {
278289
const encryptedHMAC = signedMsg.substring(0, 64);
279290
const encryptedMsg = signedMsg.substring(64);
280-
const decryptedHMAC = impl.signMessage(hashedPassphrase, encryptedMsg);
281-
282-
if (decryptedHMAC !== encryptedHMAC) {
283-
return { success: false, message: "Signature mismatch" };
284-
}
285-
return {
286-
success: true,
287-
decoded: impl.decrypt(encryptedMsg, hashedPassphrase),
288-
};
291+
return impl.signMessage(hashedPassphrase, encryptedMsg).then(function(decryptedHMAC) {
292+
if (decryptedHMAC !== encryptedHMAC) {
293+
return { success: false, message: "Signature mismatch" };
294+
}
295+
return impl.decrypt(encryptedMsg, hashedPassphrase).then(function(decoded) {
296+
return {
297+
success: true,
298+
decoded: decoded,
299+
};
300+
});
301+
});
289302
}
290303
exports.decode = decode;
291304

@@ -316,7 +329,7 @@
316329
var decode = codec.init(impl).decode;
317330

318331
// variables to be filled when generating the file
319-
var encryptedMsg = '5b8ddda06f7972f55040c4e53ac58581408b427bb14d0856b3e8fa275836489fa2195f5c68889a2c9b81f3bad4d84683U2FsdGVkX1+ZRQKlbRyks/JsZcQ+tKL24t+BOrwQilIyt5/uiwboh274a5/NfJs+iJG7meL6uQlKNUKgVPNW7pL00Wvsj2PTZbve2YuB7Qq1QqZZ/jAM9IXPWAMKs9VoGBEvDw3BUhgiCVOq0ibTYFBz9LO5xcuI1Gwdnw7DNYE2/aadml4/eo4D3yQI7U61vbBo1KYjEGr9rBgoUHnkSw==',
332+
var encryptedMsg = '8af5413a09610945eaf71ded7559ecaaa93c8414c8e39e3e4951cf8ade51c9a399c664369959d92b91ab900fc4f7f7d6U2FsdGVkX18uzOPNOJ7JLg2NQCum1dvLl7CO+XZCJU6JdN7QwWmx4aSJDqnWfu6lmttosCYmZFfbTg8eT/8NcVoP2O9dCRohqIBzXJpfnFiJ0SRSSxWF/vkImNJUOamBQyd+6ND41LEC7YmkzQAJpu4z7TIrTqXIv37a0wICxwmpFC4AO8SoIYvubRXXVDZipUJQetvK3ZBOCBF6OSZbag==',
320333
salt = 'b93bbaf35459951c47721d1f3eaeb5b9',
321334
isRememberEnabled = true,
322335
rememberDurationInDays = 0; // 0 means forever
@@ -332,15 +345,16 @@
332345
* @returns
333346
*/
334347
function decryptAndReplaceHtml(hashedPassphrase) {
335-
var result = decode(encryptedMsg, hashedPassphrase);
336-
if (!result.success) {
337-
return false;
338-
}
339-
var plainHTML = result.decoded;
348+
return decode(encryptedMsg, hashedPassphrase).then(function(result) {
349+
if (!result.success) {
350+
return false;
351+
}
352+
var plainHTML = result.decoded;
340353

341-
document.write(plainHTML);
342-
document.close();
343-
return true;
354+
document.write(plainHTML);
355+
document.close();
356+
return true;
357+
});
344358
}
345359

346360
/**
@@ -378,13 +392,13 @@
378392

379393
if (hashedPassphrase) {
380394
// try to decrypt
381-
var isDecryptionSuccessful = decryptAndReplaceHtml(hashedPassphrase);
382-
383-
// if the decryption is unsuccessful the password might be wrong - silently clear the saved data and let
384-
// the user fill the password form again
385-
if (!isDecryptionSuccessful) {
386-
return clearLocalStorage();
387-
}
395+
decryptAndReplaceHtml(hashedPassphrase).then(function(isDecryptionSuccessful) {
396+
// if the decryption is unsuccessful the password might be wrong - silently clear the saved data and let
397+
// the user fill the password form again
398+
if (!isDecryptionSuccessful) {
399+
return clearLocalStorage();
400+
}
401+
});
388402
}
389403
}
390404
}
@@ -397,10 +411,12 @@
397411
shouldRememberPassphrase = document.getElementById('staticrypt-remember').checked;
398412

399413
// decrypt and replace the whole page
400-
var hashedPassphrase = impl.hashPassphrase(passphrase, salt);
401-
var isDecryptionSuccessful = decryptAndReplaceHtml(hashedPassphrase);
402-
403-
if (isDecryptionSuccessful) {
414+
impl.hashPassphrase(passphrase, salt).then(function(hashedPassphrase) {
415+
return decryptAndReplaceHtml(hashedPassphrase);
416+
}).then(function(isDecryptionSuccessful) {
417+
if (!isDecryptionSuccessful) {
418+
return alert('Bad passphrase!');
419+
}
404420
// remember the hashedPassphrase and set its expiration if necessary
405421
if (isRememberEnabled && shouldRememberPassphrase) {
406422
window.localStorage.setItem(rememberPassphraseKey, hashedPassphrase);
@@ -413,9 +429,7 @@
413429
);
414430
}
415431
}
416-
} else {
417-
alert('Bad passphrase!');
418-
}
432+
});
419433
});
420434
</script>
421435
</body>

0 commit comments

Comments
 (0)