В комментариях к предыдущей статье справедливо заметили, что помнить множество паролей — напрягает.

И ведь действительно так: придумать сложный, запоминающийся пароль, даже в стиле известного «девятнадцать обезьян...» и потом не перепутать, сколько точно было обезьян — это трудно.

И тут я увидел валяющиеся без дела 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

Закинул на ГитХаб

Результат - шифровать файлы можно, это работает. Ключи - либо на токене, либо вводится пароль вручную. Сделано в порядке "а попробую?", но вроде и пользоваться можно.

И всегда надо помнить про область применимости: аппаратный токен - это уязвимость сама по себе, его можно потерять, поломать, его можно отнять и попросить сообщить пинкод, тем более что сам по себе факт его наличия как бы провоцирует это сделать.