В комментариях к предыдущей статье справедливо заметили, что помнить множество паролей — напрягает.
И ведь действительно так: придумать сложный, запоминающийся пароль, даже в стиле известного «девятнадцать обезьян...» и потом не перепутать, сколько точно было обезьян — это трудно.
И тут я увидел валяющиеся без дела USB‑токены...
Ну, так получилось: один старый, но когда‑то навороченный Aladdin, а другой современный, но простой Rutoken Lite, оставшийся после апгрейда.
Что, если использовать их?
Надо сказать, что на самом деле я с ними раньше вообще не работал, ну кроме «вставить в компьютер и следовать инструкциям на сайте», поэтому разобраться наконец, что это вообще такое, было интересно.
Вообще с этими токенами просто беда какая‑то: в основном они у нас используются либо для банк‑клиентов, либо для общения с госструктурами, поэтому все описания их работы можно условно разделить на две категории:
Первая — это описание для «Мариванны», в стиле «красненький подходит для алкоголя, а синенький не подходит для алкоголя!», «установите нашу программу ХХХХ, она лучше! (лицензия на год столько то денег), напишите полстраницы заклинаний, если не помогло — позвоните по тел 8-800...»
Вторая — от людей глубоко в теме, искусно владеющих канцеляритом типа «получить сертификат в УЦ ЕГАИС с использованием СКЗИ ФСТЭК НЖТИ-1100», где довольно подробно расписано как обновить сертификаты в той самой программе ХХХХ, а если она выдает ошибку 003 425 255 то установить компонент из пакета MS VStudio или пакет DirectX 10.0 (ну, как‑то так, примерно такое советовали сделать).
Но как же оно работает‑то? И как это можно использовать для себя?
В общем, оказалось довольно просто.
Логика работы ключей
В целом все эти USB‑токены разных видов можно поделить на два типа:
простые, фактически «флешка с пинкодом»
сложные, со встроенной криптографией
Например, тот старый Aladdin — сложный, Rutoken Lite — простой, Rutoken ЭЦП 2/3 — сложный.
На простые, по факту, можно просто что‑то записать, и что‑то оттуда прочитать, при этом это что‑то можно закрыть пинкодом. Если пинкод не введен — данные не получить, если ввести несколько раз неправильно — токен блокируется.
У сложных возможностей больше, можно записать публичные и приватные ключи для ассимметричного шифрования (причем оно понимает, какой ключ — какой), можно сгенерировать эти ключи прямо в токене, можно что‑то зашифровать или расшифровать, используя ключи..
В общем что‑то вроде встроенного openssl.
Зачем это нужно: из «сложного» токена нельзя никак извлечь созданный или записанный в него приватный ключ.
Можно использовать при шифровании на токене, но невозможно извлечь и скопировать, никому.
Поэтому токен всегда один, и всегда конкретно этот — если требуется ограничить доступ к чему‑то, с контролем, что «пользователь может быть только один» — используется именно такой ключ.
Из «простого», зная пинкод, можно прочитать ключ, а значит при желании владелец ключа может его копировать, дублировать и т. д.
Отличие от флешки только в том, что нужен одновременно и токен, и пинкод, сложнее незаметно украсть, но в остальном — все в руках владельца.
Подключаем USB-токен
Для работы с ними в Linux можно использовать специальную утилиту pkcs11-tool.
Команды более‑менее стандартны, но есть важный нюанс: нужен драйвер, умеющий работать с конкретным ключом.
В pkcs11-tool это называется module, по факту ‑.so — библиотека.
В частности, для Aladdin такой драйвер для arm64 мне найти не удалось, а вот для Rutoken он есть, если поискать на сайте. Поэтому дальнейшие эксперименты пока только с Rutoken Lite.
Как уже сказал, это — простой токен, «флешка с пином», шифровать он не умеет, разные ключи не понимает, понимает только один вид данных — просто data. А вот что они означают и как с ними дальше работать — за это уже должна отвечать внешняя программа. Это и буду использовать.
Для начала нужно установить пакеты:
apt install pcscd pcsc_tools opensc
Вставляем USB Rutoken Lite
lsusb
...
Bus 001 Device 016: ID 0a89:0025 Aktiv Rutoken lite
...
Как минимум, через USB его видно.
Попробуем команду pcsc_scan - она должна вывести небольшую "простыню" с упоминанием Rutoken, после чего программу можно закрыть по Ctrl-C.
Если всё так - значит, устройство работает и успешно опознано.
Попробуем с ним что-то сделать:
pkcs11-tool -L
Available slots:
Slot 0 (0x0): Aktiv Rutoken lite 00 00 (token not recognized)
Как и сказано выше - токен найден, но pkcs11-tool не умеет с ним работать из коробки. Нужен драйвер, module.
Тут пришлось идти на сайт, искать драйвера.
Т.к. у меня Armbian, т.е. Debian под arm64 - обычные x86 пакеты не подошли, но по счастью, там есть раздел Другие (MIPS, Байкал, ARM...)
Осталось скачать архив, найти в нем подходящий .deb-файл, распаковать его, извлечь .so-библиотеку и положить ее в /usr/lib/
dpkg -x librtpkcs11ecp_2.18.0.0-1_arm64.deb tmpdir
cd tmpdir
(не хотелось устанавливать неизвестно что сразу в систему - поэтому просто распаковка, а потом перенести что куда надо вручную).
Внутри tmpdir — структура каталогов, в которой нужно найти библиотеку, и скопировать ее на место (в версии для x86 скорее всего никаких существенных отличий не будет)
cp librtpkcs11ecp.so /usr/lib/
Теперь пробуем еще раз:
pkcs11-tool --module librtpkcs11ecp.so -L
Available slots:
Slot 0 (0x0): Aktiv Rutoken lite 00 00
token label : Rutoken lite
token manufacturer : Aktiv Co.
token model : Rutoken lite
token flags : login required, rng, token initialized, PIN initialized
hardware version : 67.4
firmware version : 32.2
serial num : xxxxxxxx
pin min/max : 6/249
Slot 1 (0x1):
(empty)
Slot 2 (0x2):
(empty)
....
Вот, теперь заработало!
Слоты - это понятие, относящееся к стандарту PKCS#11: их в принципе несколько, в каждом может находиться "token", причем в одном физическом ключе может быть несколько слотов с токенами, но в данном случае токен один и занимает Slot 0.
Интересно поле token flags, там могут быть указания на необходимость замены пинов, либо еще какая иформация. В данном случае токен инициализирован, пины уже настроены, ничего делать не требуется.
Отдельно заметим флаг rng, это означает, что токен может быть генератором случайной последовательности.
Работа с токеном
Во-первых, доступы.
Права доступа определяются пинкодом. Есть обычный, пользовательский пинкод, и административный, SO (Security Officer) PIN.
Если ввести неправильно несколько раз пользовательский пинкод - он будет заблокирован, но его можно сменить используя административный.
Если заблокируется административный - можно переинициализировать токен с потерей всех данных в нем.
PIN по умолчанию - 12345678, SO PIN - 87654321. Их можно поменять:
pkcs11-tool --module librtpkcs11ecp.so --login --login-type user --change-pin
pkcs11-tool --module librtpkcs11ecp.so --login --login-type so --change-pin
Соответственно, первая команда - пинкод для обычного юзера, вторая меняет административный (SO) пинкод.
Чтобы не вводить пинкод вручную - можно указать его в командной строке
pkcs11-tool --module librtpkcs11ecp.so --login --login-type user --pin 12345678
или в переменной окружения
PIN="12345678" pkcs11-tool --module librtpkcs11ecp.so --login --login-type user --pin env:PIN
Чужую командную строку можно подсмотреть через ps, а вот так заданную переменную окружения - через /proc/, для чего нужны соответствующие права.
То, что "умеет" делать токен - называется механизмами. Проверим:
pkcs11-tool --module librtpkcs11ecp.so --list-mechanism
Using slot 0 with a present token (0x0)
Supported mechanisms:
Ожидаемо. Механизмов у Rutoken Lite нет, он почти ничего не умеет.
Умеет только записывать и читать файлы:
pkcs11-tool --module librtpkcs11ecp.so --type data --label 'Secret' --write-object secret.txt
pkcs11-tool --module librtpkcs11ecp.so --type data --label 'Secret' --read-object
Указывать тип данных нужно обязательно, правда, все равно он никаких других кроме data не понимает. Если добавить --private и --pin/--login - записанный файл будет защищен пинкодом.
Можно посмотреть, что записано на токене:
pkcs11-tool --module librtpkcs11ecp.so -O
pkcs11-tool --module librtpkcs11ecp.so -O --pin 12345678
Защищенные записи без пинкода не видны, поэтому первая команда не покажет --private записи, вторая покажет все.
Еще этот окен умеет генерировать случайные данные в виде байтов:
pkcs11-tool --module librtpkcs11ecp.so --generate-random 512
Создаст 512 случайных байт.
Всё остальное, внутреннее шифрование и прочее, требует более продвинутых токенов, и с этим не работает.
Практическое применение
Что можно полезного с ним сделать?
Да, он не поддерживает внутреннего шифрования, приватных/публичных ключей, но зато он умеет просто хранить некие данные.
В принципе, на него можно записать и приватный, и публичный ключи для асимметричного шифрования - просто сам по себе он их не различает, этим должна заниматься внешняя программа.
Но тут вопрос в другом: асимметричное шифрование - это всё-таки больше корпоративные дела: электронные подписи, шифрование для конкретного получателя.
Для персонального использования для себя лично - оно в общем-то особо и не надо, когда все возможные отправители и получатели всегда один человек.
Зато он прекрасно подходит для хранения ключей симметричного шифрования, причем длинных и неподбирабельных. Каждому можно дать какое-то короткое имя, а потом использовать их, просто вызывая по этому имени из токена.
Таким образом, схема работы вырисовывается следующая:
1 - программа (скрипт) управления ключами в токене
2 - программа шифрования с использованием токена
3 - программа дешифрования с использованием токена
Реализация
Во-первых, нужно убедиться, что токен подключен, если нет - предложить подключить или выйти.
Во‑вторых, если подключен — запросить пинкод. Хотя pkcs11-tool умеет сам запрашивать пинкод — будет неудобно делать это постоянно, на каждый чих программы.
Затем прочитать имеющиеся ключи, пронумеровать их, и предложить удалить, или создать новый.
При создании ключа задействовал функцию токена — генерацию случайных байт. Конечно, есть и другие способы — но что она зря будет пропадать?
Поскольку пароль потом предполагается вводить «как текст» — эта последовательность байт пребразуется в строку base64, так и сохраняется. Для преобразования используется openssl.
На всякий случай, с расчетом на эксперименты с асимметричными ключами, немного усложнил названия симметричных ключей — они все называются «key XXXXX». Можно было бы обойтись и без этого, стандарт PKCS#11 предполагает разные типы данных — но конкретно Rutoken Lite их не понимает.
Для шифрования слегка модифицировал скрипты encrypt-decrypt из предыдущей статьи, добавив возможность вызова ключа из токена.
А чтобы не плодить кучу скриптов - скомпоновал в один. Разные функции вызываются в зависимости от разного имени исполняемого файла, точно так же как с busybox:
crypto_pack - создает нужные симлинки
crypto_keys - управление ключами
crypto_encode - шифрование
crypto_decode - расшифровка
Многобукв...
#!/bin/bash
# v 0.1
MAGICK="enc1"
pkcslib="librtpkcs11ecp.so"
cipher="aes-256-ctr"
progname=${0##*/}
prepare_token(){
token=$(pkcs11-tool --module "$pkcslib" -T 2>/dev/null | awk -F': ' '/token model/{print $2}')
while [ "x$token" = "x" ]; do
echo "No token found!"
read -p "Insert it and press any key for try again, or q for exit: " ans
echo
if [ "x$ans" = "xq" ];then
exit 0
fi
token=$(pkcs11-tool --module "$pkcslib" -T 2>/dev/null | awk -F': ' '/token model/{print $2}')
done
echo "$token found"
echo
read -s -p 'Enter PIN: ' pincode
echo
if [ "x$pincode" = "x" ];then
exit 0
fi
}
crypto_keys(){
prepare_token
while true; do
mapfile -t OBJECTS < <(PINCODE="$pincode" pkcs11-tool --module "$pkcslib" --pin env:PINCODE -O --type data 2>/dev/null | awk -F': ' '/label: / {print $2}' | sed -e 's/^\s*'"'"'//' -e 's/'"'"'\s*$//' | grep '^key ')
if [ ${#OBJECTS[@]} -eq 0 ]; then
echo "No keys inside."
else
echo "Found keys:"
for i in "${!OBJECTS[@]}"; do
name="${OBJECTS[$i]}"
name="${name#* }"
printf "%d: %s\n" $((i+1)) "$name"
done
fi
echo
read -p "Select key for deletion, n (new key) or q (quit): " choice
echo
case "$choice" in
q|Q)
echo "Bye..."
exit 0
;;
n|N)
read -p "Enter new key name: " new_label
if [ "x$new_label" != "x" ]; then
tmpfile=$( mktemp )
pkcs11-tool --module "$pkcslib" --generate-random 100 2>/dev/null | openssl base64 -A > "$tmpfile"
PINCODE="$pincode" pkcs11-tool --module "$pkcslib" --pin env:PINCODE \
--write-object "$tmpfile" --private \
--type data --label "key $new_label" 2>/dev/null >/dev/null
rm -f "$tmpfile"
fi
;;
[0-9]*)
index=$((choice-1))
if [ $index -ge 0 ] && [ $index -lt ${#OBJECTS[@]} ]; then
name="${OBJECTS[$index]}"
name="${name#* }"
read -p "Remove key '$name'? (y/N): " confirm
echo
if [ "x$confirm" = "xy" ]; then
echo "Deletion '$label'..."
PINCODE="$pincode" pkcs11-tool --module "$pkcslib" --pin env:PINCODE \
--delete-object --type data --label "key $name" 2>/dev/null
else
echo "Cancelled."
fi
else
echo "Incorrect number."
fi
;;
*)
echo "Unknown command."
;;
esac
echo
done
}
pwd_encrypt(){
output="$infile.enc"
echo -n "$MAGICK" > "$output"
openssl dgst -sha256 -binary "$infile" >> "$output"
echo -n "$pass1" | openssl enc -"$cipher" -salt -in "$infile" -pbkdf2 -pass stdin >> "$output"
}
crypto_encode(){
read -p "Password, key, or quit? (p/k/q): " ans
echo
if [ "x$ans" = "xp" ]; then
read -p "Enter password: " -s pass1
echo
if [ "x$pass1" = "x" ] ; then
exit 0
fi
pwd_encrypt
elif [ "x$ans" = "xk" ]; then
prepare_token
while true; do
mapfile -t OBJECTS < <(PINCODE="$pincode" pkcs11-tool --module "$pkcslib" --pin env:PINCODE -O --type data 2>/dev/null | awk -F': ' '/label: / {print $2}' | sed -e 's/^\s*'"'"'//' -e 's/'"'"'\s*$//' | grep '^key ')
if [ ${#OBJECTS[@]} -eq 0 ]; then
echo "No keys inside."
else
echo "Found keys:"
for i in "${!OBJECTS[@]}"; do
name="${OBJECTS[$i]}"
name="${name#* }"
printf "%d: %s\n" $((i+1)) "$name"
done
fi
echo
read -p "Select key or q (quit): " choice
case "$choice" in
q|Q)
echo "Bye..."
exit 0
;;
[0-9]*)
index=$((choice-1))
if [ $index -ge 0 ] && [ $index -lt ${#OBJECTS[@]} ]; then
name="${OBJECTS[$index]}"
name="${name#* }"
pass1=$(PINCODE="$pincode" pkcs11-tool --module "$pkcslib" --pin env:PINCODE --read-object --type data --label "key $name" 2>/dev/null)
pwd_encrypt
exit 0
else
echo "Incorrect number."
fi
;;
*)
echo "Unknown command."
;;
esac
echo
done
fi
}
pwd_decrypt(){
tmp_out=$(mktemp -p .)
tmp_in=$(mktemp -u -p .)
mkfifo "$tmp_in"
tail -c +37 "$infile" > "$tmp_in" &
echo -n "$pass1" | openssl enc -"$cipher" -d -in "$tmp_in" -out "$tmp_out" -pbkdf2 -pass stdin
rm "$tmp_in"
openssl dgst -sha256 -binary "$tmp_out" | cmp -n 32 -s --ignore-initial=0:4 - "$infile"
if [ $? -ne 0 ]; then
echo "Incorrect password"
rm "$tmp_out"
exit 2
fi
mv "$tmp_out" "$orig"
echo "Ok, $orig decoded"
}
crypto_decode(){
echo -n "$MAGICK" | cmp -n 4 -s - "$infile"
if [ $? -ne 0 ]; then
echo "File $infile has unknown format"
exit 1
fi
orig=$(echo $infile | grep -oP '[^/]+(?=\.enc$)')
if [ "x$orig" = "x" ] ; then
echo "File $infile has unknown format"
exit 1
fi
if [ -f "$orig" ] ; then
msg="File $orig already exists, enter new name or overwrite? "
echo "$msg"
read -e -i "$orig" -p "> " orig
echo
fi
if [ "x$orig" = "x" ] ; then
exit 0
fi
read -p "Password, key, or quit? (p/k/q): " ans
echo
if [ "x$ans" = "xp" ]; then
read -s -p "Enter password: " pass1
echo
if [ "x$pass1" = "x" ] ; then
exit 0
fi
pwd_decrypt
exit 0
elif [ "x$ans" = "xk" ]; then
prepare_token
while true; do
mapfile -t OBJECTS < <(PINCODE="$pincode" pkcs11-tool --module "$pkcslib" --pin env:PINCODE -O --type data 2>/dev/null | awk -F': ' '/label: / {print $2}' | sed -e 's/^\s*'"'"'//' -e 's/'"'"'\s*$//' | grep '^key ')
if [ ${#OBJECTS[@]} -eq 0 ]; then
echo "No keys inside."
else
echo "Found keys:"
for i in "${!OBJECTS[@]}"; do
name="${OBJECTS[$i]}"
name="${name#* }"
printf "%d: %s\n" $((i+1)) "$name"
done
fi
echo
read -p "Select key or q (quit): " choice
case "$choice" in
q|Q)
echo "Bye..."
exit 0
;;
[0-9]*)
index=$((choice-1))
if [ $index -ge 0 ] && [ $index -lt ${#OBJECTS[@]} ]; then
name="${OBJECTS[$index]}"
name="${name#* }"
pass1=$(PINCODE="$pincode" pkcs11-tool --module "$pkcslib" --pin env:PINCODE --read-object --type data --label "key $name" 2>/dev/null)
pwd_decrypt
exit 0
else
echo "Incorrect number."
fi
;;
*)
echo "Unknown command."
;;
esac
echo
done
fi
}
if [ "$progname" = "crypto_keys" ]; then
crypto_keys
elif [ "$progname" = "crypto_encode" ]; then
if [ -f "$1" ] ; then
infile=$1
crypto_encode
else
echo "No input file"
fi
elif [ "$progname" = "crypto_decode" ]; then
if [ -f "$1" ] ; then
infile=$1
crypto_decode
else
echo "No input file"
fi
else
if [ ! -f "crypto_keys" ]; then ln -s crypto_pack crypto_keys ; fi
if [ ! -f "crypto_encode" ]; then ln -s crypto_pack crypto_encode ; fi
if [ ! -f "crypto_decode" ]; then ln -s crypto_pack crypto_decode ; fi
fi
Результат - шифровать файлы можно, это работает. Ключи - либо на токене, либо вводится пароль вручную. Сделано в порядке "а попробую?", но вроде и пользоваться можно.
И всегда надо помнить про область применимости: аппаратный токен - это уязвимость сама по себе, его можно потерять, поломать, его можно отнять и попросить сообщить пинкод, тем более что сам по себе факт его наличия как бы провоцирует это сделать.