Я использую уменьшенная версия из SJCL для шифрования файлов резервных копий, запрошенных пользователями. Это в Скрипт Google Apps надстройка для Google Таблиц.
Меня больше всего беспокоит правильное использование, обработка исключений и лучшие практики добавления части шифрования в скрипт. В настоящее время он работает должным образом в процессе резервного копирования и восстановления.
Резервное копирование (и шифрование)
Пользователь вводит кодовую фразу
Он начинается с того, что пользователю предлагается ввести и повторно ввести парольную фразу в приглашении. 1) Одна проблема заключается в том, что текстовое поле приглашения не маскирует парольную фразу. Чтобы исправить это, я бы показал диалог с HTML и формой (ввод пароля). Но тогда мне нужно будет работать с клиент-серверным вызовом, и, хотя это не проблема, подсказка намного проще, менее сложна, и нет необходимости помещать HTML — это может быть сомнительно для пользователя?
const passphrase1 = ui.prompt(
'Backup',
'Enter passphrase:',
ui.ButtonSet.OK_CANCEL);
if (passphrase1.getSelectedButton() === ui.Button.CANCEL) return 0;
const passphrase2 = ui.prompt(
'Backup',
'Please re-enter this passphrase:',
ui.ButtonSet.OK_CANCEL);
if (passphrase2.getSelectedButton() === ui.Button.CANCEL) return 0;
Тестовая кодовая фраза
Здесь сценарий проверяет, совпадают ли парольные фразы, а также проверяет длину ключевой фразы и наличие хотя бы верхних, нижних цифр и специальных символов. Это очень простой тест — так что pA$$w0rd00
пройти тест — и я действительно не хотел тестировать что-либо, как это делает GnuPG. В таком случае, нет ничего (никакого теста, как GnuPG) лучше этого простого теста?
const passphrase = passphrase1.getResponseText();
if (passphrase !== passphrase2.getResponseText() || passphrase.length < 10 || testPassphrasePolicy(passphrase)) {
ui.alert(
'Backup',
'Invalid passphrase.',
ui.ButtonSet.OK);
return 1;
}
function testPassphrasePolicy (passphrase) {
if (!/[a-z]+/.test(passphrase)) return 1;
if (!/[A-Z]+/.test(passphrase)) return 1;
if (!/[0-9]+/.test(passphrase)) return 1;
if (!/[~!@#$%^*-_=+[{]}/;:,.?]+/.test(passphrase)) return 1;
return 0;
}
Зашифровать данные
Часть шифрования довольно проста. В backup
это объект вроде backup = { foe: 'abc', bar: 123 }
. Шаги следующие:
- Stringify
backup
- Кодировать в base64
- Вычислить SHA256
- Объедините base64 с
:
и SHA256 - Зашифруйте с помощью AES-128 в режиме GCM и используя SHA256 в качестве данных аутентификации. Если есть ошибка, просто вернитесь и покажите общее сообщение об ошибке.
- Расшифровка теста. Если есть ошибка, просто вернитесь и покажите общее сообщение об ошибке.
- Вернуть каплю зашифрованной резервной копии
Я подозреваю, что шаги 3 и 4 являются чрезмерными, но до добавления шифрования именно так скрипт выполнял простой тест целостности (с SHA1).
function encryptBackup_ (backup, passphrase) {
const string = JSON.stringify(backup);
const webSafeCode = Utilities.base64EncodeWebSafe(string, Utilities.Charset.UTF_8);
const sha = computeDigest('SHA_256', webSafeCode, 'UTF_8');
const data = webSafeCode + ':' + sha;
let encrypted = '';
try {
encrypted = sjcl.encrypt(passphrase, data, { mode: "gcm", adata: sha });
} catch (err) {
ConsoleLog.error(err);
return 0;
}
try {
const decrypted = sjcl.decrypt(passphrase, encrypted);
const parts = decrypted.split(':');
const test_sha = computeDigest('SHA_256', parts[0], 'UTF_8');
if (test_sha !== parts[1]) throw new Error('digestBackup_(): Bad decryption.');
} catch (err) {
ConsoleLog.error(err);
return 0;
}
const date = Utilities.formatDate(DATE_NOW, 'GMT', 'yyyy-MM-dd-HH-mm-ss');
const name="data" + date + '.backup';
const blob = Utilities.newBlob(encrypted, 'application/octet-stream', name);
return blob;
}
Восстановление (и расшифровка)
После того, как пользователь выберет диск file
, сценарий запрашивает парольную фразу и пытается расшифровать файл. Эта часть проверяет, возможно ли расшифровать файл, выбранный пользователем.
Кодовая фраза кэшируется в экземпляре кеша, ограниченном текущим пользователем и скриптом так что сценарий может получить его позже, чтобы фактически расшифровать файл и восстановить данные. Срок действия кеша истекает через 120 секунд (сеанс), и как только он извлекается позже, он немедленно удаляется.
const ui = SpreadsheetApp.getUi();
const passphrase = ui.prompt(
'Restore',
'Enter passphrase:',
ui.ButtonSet.OK_CANCEL);
if (passphrase.getSelectedButton() === ui.Button.CANCEL) return 0;
let decrypted = null;
try {
decrypted = sjcl.decrypt(passphrase.getResponseText(), data);
} catch (err) {
ConsoleLog.error(err);
return 4;
}
const address = computeDigest(
'SHA_1',
file.getId() + SpreadsheetApp2.getActiveSpreadsheet().getId(),
'UTF_8');
CacheService2.put('user', address, 'string', passphrase.getResponseText(), 120);
const parts = decrypted.split(':');
const test_sha = computeDigest('SHA_256', parts[0], 'UTF_8');
if (test_sha !== parts[1]) return 4;
const string = base64DecodeWebSafe(parts[0], 'UTF_8');
return JSON.parse(string);
1 ответ
Пользователь вводит кодовую фразу
Никто больше не использует модальные диалоговые окна ОС. Я бы предложил модальное окно для конкретной веб-страницы, такое как показано Вот. Вам нужно нажать «новый пользователь», чтобы появилось окно.
Что касается наименования, passphrase1
и passphrase1
кажется, теряют информацию, почему бы и нет passphrase
и passphraseReentered
(или же reenteredPassprase
)? Теперь мне кажется, что это два разных пароля.
Тестовая кодовая фраза
Вы четко не задокументировали, какая политика проверяется, хотя это может быть в проектной документации или пока отсутствует. Но такие вещи, как политика, не должны выводиться из исходного кода. Было бы довольно сложно проверить исходный код, если он является ведущим, поэтому вы не сможете обнаружить ошибки.
if (passphrase !== passphrase2.getResponseText() || passphrase.length < 10 || testPassphrasePolicy(passphrase)) {
Вы делаете здесь слишком много, и это видно. Прежде всего, проверка правильности повторного ввода пароля отделена от проверки политики паролей. Если это другая проверка, с ней следует обращаться отдельно.
В любом случае это быстро станет очевидным, когда пользователи начнут жаловаться, что они впервые ввели пароль. дважды прежде чем вы проверите, что пароль соответствует политике (d’Oh!).
Я бы сказал, что требование длины должно быть частью политики паролей, что устраняет еще одно уравнение из if
.
if (!/[a-z]+/.test(passphrase)) return 1;
Нет нет нет. Прежде всего, хотя бы вернуть false
или что-то подобное, не 0
или же 1
. Но не следует ли указывать пользователю, какой тест не прошел?
После всех этих проверок вы выполняете return 0
что на самом деле звучит для меня негативно. Однако я не видел, чтобы это не помогло с определенными персонажами. Это могло бы быть хорошо, но я лично убедился бы, что пароль не содержит каких-либо странных символов, управляющих символов и т. Д.
Зашифровать данные
Я подозреваю, что шаги 3 и 4 являются чрезмерными, но до добавления шифрования именно так скрипт выполнял простой тест целостности (с SHA1).
Он переусердствует, и, поскольку вы все равно несовместимы с оригиналом, я бы удалил вычисление SHA-256 (обратите внимание на тире в SHA-256).
const webSafeCode = Utilities.base64EncodeWebSafe(string, Utilities.Charset.UTF_8);
Почему вы расширяете свой открытый текст, кодируя его, прежде чем шифрование станет для меня недоступным, GCM отлично справляется с двоичным кодом, спасибо.
const sha = computeDigest('SHA_256', webSafeCode, 'UTF_8');
Итак, что это возвращает, чтобы вы могли поместить его в конец текстовой строки? Если он возвращает шестнадцатеричные числа или основание 64, это должно быть ясно видно при просмотре вызова.
let encrypted = '';
Если вы не сможете назначить, вы нажмете return 0
в противном случае присвоение выполняется в следующем операторе. Не назначайте пустые строки, ноль или ноль, когда это не требуется.
encrypted = sjcl.encrypt(passphrase, data, { mode: "gcm", adata: sha });
JCL усиливает ваши пароли в 1000 раз
Да, это может быть были в порядке, когда PBKDF2, версия усиления пароля была впервые создана. В наши дни вы захотите набрать минимум миллион, а, возможно, позже увеличить рабочий коэффициент. К сожалению, использовать (просто) пароли для любого шифрования сложно.
Я не буду комментировать часть расшифровки в функции шифрования. Я думаю, что это слишком осторожно, потому что в случае ошибки я не ожидал, что код сработает return blob
но поскольку это резервные данные …
if (test_sha !== parts[1]) throw new Error('digestBackup_(): Bad decryption.');
Но я прокомментирую плохо скопированный код, мы не в digestBackup
и ошибки уже должны содержать трассировку стека, поэтому нет необходимости регистрировать ее дополнительно.
const date = Utilities.formatDate(DATE_NOW, 'GMT', 'yyyy-MM-dd-HH-mm-ss');
const name="data" + date + '.backup';
Дата в секундах и имя файла, в котором эта дата используется для различения резервных копий. Четный если на самом деле этого никогда не произойдет, тестировщики модулей / приложений будут вас презирать. Вам нужен лучший способ отделить капли друг от друга. Хуже того, они могут не уловить это, и вы можете перезаписать предыдущую резервную копию — текущие компьютеры работают быстро, вы можете многое сделать за секунду и, возможно, вы захотите создать резервную копию до и после выполнения процесса.
Обратите внимание, что функция шифрования, начиная с (ненужного) вычисления SHA и заканчивая созданием большого двоичного объекта, полностью игнорирует тип данных. Если вы скажете это, вы можете протестировать шифрование / дешифрование отдельно. РЕДАКТИРОВАТЬ: С другой стороны, вы можете использовать имя файла в качестве дополнительных аутентифицированных данных (adata
), поэтому злоумышленник не может уйти с переименованием резервных копий. Тем не менее, вы можете добавить эти данные в качестве параметра к вызову функции.
Восстановление (и расшифровка)
Прежде всего, где decryptBackup_
функция? Всегда делайте свои приложения симметричными, даже если в результате получается всего лишь однолинейный метод. Представьте себе будущего разработчика (то есть вас), ищущего его. Теперь вы внезапно смешиваете код пользовательского интерфейса и код, выполняющий реальную работу.
return 4;
Предупреждение, обнаружен высокий уровень октарина. Не используйте магические числа!
const address = computeDigest(
'SHA_1',
file.getId() + SpreadsheetApp2.getActiveSpreadsheet().getId(),
'UTF_8');
CacheService2.put('user', address, 'string', passphrase.getResponseText(), 120);
Простите, что это делает? Здесь? Вообще?
if (test_sha !== parts[1]) return 4;
Четыре! Тупая ошибка мяча для гольфа!
Мне нравится, что вы подумали о кэшировании и управлении парольной фразой. Управление ключами / секретами очень важно в криптографии.
Привет и спасибо за уделенное время! Я очень ценю это, так как мой скрипт нуждается в обзоре, прежде чем я подумаю о его слиянии с основной веткой. Я узнал что-то новое, и хотя одна или две проблемы — см. Ниже — не будут проблемой в данном конкретном случае, я учту ваше объяснение в других случаях. Мои комментарии по каждой из указанных вами проблем слишком длинные для этого поля. Как я могу правильно ответить на ваш ответ — что вы предлагаете?
— EqualBear0000
Больше ящиков, обрабатывайте по одному комментарию за раз. Мы всегда можем перейти в чат, если раздел комментариев станет слишком длинным, мы будем уведомлены автоматически.
— Маартен Бодевес
Некоторые соображения: 1. Код из вопроса работает на стороне сервера. Кстати, надстройка предназначена для Google Таблиц, поэтому все работает на стороне сервера, за исключением кнопки «Создать резервную копию сейчас» и подсказок с парольной фразой. 2. После шифрования файл отправляется по электронной почте пользователю — это требуется владельцу электронной таблицы и пользователю надстройки, поскольку владелец электронной таблицы может поделиться с другим пользователем, у которого есть надстройка.
— EqualBear0000
Теперь все следующие части имеют одинаковый ответ «Замечено, исправлю!»: «Теперь мне кажется, что это два разных пароля.«….»проверка правильности повторного ввода пароля осуществляется отдельно от проверки политики паролей«….»по крайней мере, вернуть false или что-то подобное, а не 0 или 1.«….»Прежде всего, где находится decryptBackup функция? Всегда делайте ваши приложения симметричными _ «….»Не назначайте пустые строки, ноль или ноль, когда это не требуется.«….»Тестеры вашего устройства / приложения будут вас презирать«На самом деле у меня нет тестеров.
— EqualBear0000
«Никто больше не использует модальные диалоговые окна ОС.«Чтобы быть более конкретным, он использует Диалоговое окно «Скрипт приложений»; должен ли я заменить его на настраиваемый диалог показать коробку?
— EqualBear0000