Если у вас паранойя…

95adaa17861222eebb50d4f648520143

Помните, был такой фильм: хакеры добыли некую секретную информацию, которую можно было прочитать только на определенном компьютере, находящемся в особо охраняемом помещении, потому что на других компьютерах ее нельзя было расшифровать…

На самом деле это довольно несложно делается, попробую показать на примере.
(«промышленные», сертифицированные и прочие решения не рассматриваются потому что потому)

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

Но у шифрования есть минус: вы знаете пароль, и если вас настоятельно попросить — вы его скажете. Значит, это и будет условие номер два: вы не можете рассказать пароль. Захотите -, но не сможете.

Однако, не зная пароля — вы должны получать при этом доступ к своим файлам.
Это означает, что открываться защищенная папка должна сама, но только при «штатной» работе, и не открываться, если что-то пошло не так.
Это условие номер три — работает автоматически.

Можно привязать пароль к каким-то аппаратным свойствам компьютера -, но если выкрасть весь компьютер — будет выкраден и ключ к папке. Значит, ключ должен находиться вне компьютера, но в доступе к нему.
Но что, если злоумышленники украли не только один компьютер, но и весь офис? А вам не дают ничего делать.
Тогда ключ должен быть таким, чтобы третье лицо могло в любой момент уничтожить ключ, возможно даже не зная что именно делает, «не вызывая подозрения санитаров».
Это — четвертое условие: ключ физически не привязан к компьютеру и не должен выглядеть как ключ для постороннего наблюдателя, зато должен быть легко уничтожим.

Вот это сейчас и реализуем:

В качестве средства шифрования используем LUKS. Это непринципиально, просто cryptsetup уже есть.
Чтобы не знать пароль — нужно использовать ключевой файл. LUKS работает с 512-битными ключами — это 64 байта данных, запомнить и рассказать вы не сможете.
Управлять подключением секретного диска можно через PAM на компьютере — всё будет происходить аввтоматически.
Получить ключ можно по сети, да вот хотя бы по http с какого-нибудь веб-сервера. Причем это может быть хоть корпоративный сервер, хоть удаленный сайт, хоть плата esp8266 в стене за гипсокартоном.

И чтобы ключ не выглядел как ключ для посторонних — в качестве ключа можно использовать самый обычный файл, хоть картинку, хоть текст. Взяв от него 512-битный хеш — и получим тот самый уникальный секретный ключ.
А удалить его очень просто: заменить картинку на сайте, пусть даже на похожую. Как именно это сделать — совсем другой вопрос, но он решаемый.

Для этого пишем такой скрипт (/usr/local/bin/paranoid):

#!/bin/bash

# на всякий случай подключаем /sbin
PATH=/sbin:$PATH

# установка общих переменных
setvars(){
  HOME_DIR=$(getent passwd "$USER" | cut -d: -f6)
  MOUNT_POINT="$HOME_DIR/edisk"

  TMPFS_DIR="/tmp/tmpfs_key_$USER"
  IMG_FILE="$TMPFS_DIR/image.bin"
  KEY_FILE="$TMPFS_DIR/secret.key"
  CRYPT_NAME="edisk_$USER"
}

# проверка наличия установленных программ
checks(){
  which awk grep mount umount wget sha512sum xxd cryptsetup mkfs.ext4
  if [ $? -ne 0 ] ; then
    # если чего-то нет - завершаем работу, но "успешно", чтобы не вызывать ошибок
    exit 0
  fi
}

# читаем конфигурационный файд ля юзера
getconfig(){
  CONFIG="/etc/paranoid/users/$USER.conf"

  DATA_DIR=$(sudo awk -F= '/^data_dir/ {gsub(/^ +| +$/, "", $2); print $2}' $CONFIG)
  DISK_IMAGE=$(sudo awk -F= '/^disk_image/ {gsub(/^ +| +$/, "", $2); print $2}' $CONFIG)
  SECRET=$(sudo awk -F= '/^secret/ {gsub(/^ +| +$/, "", $2); print $2}' $CONFIG)

  if [ "x$DISK_IMAGE" = "x" ] || [ "x$SECRET" = "x" ] || [ "x$DATA_DIR" = "x" ] ; then
    echo "No  or  found!"
    exit 0
  fi

  DISK_IMAGE="$DATA_DIR/$DISK_IMAGE"
}

# проверка на "открытость"
is_mapped() {
  ls /dev/mapper | grep -q "$CRYPT_NAME"
}

# проверка на смонтированность
is_mounted() {
  mount | grep -q "$MOUNT_POINT"
}

# самая главная часть - получение ключа
fetch_key() {
  mkdir -p "$TMPFS_DIR"
  if [ $? -ne 0 ] ; then
    exit 0
  fi
  sudo chmod 700 "$TMPFS_DIR"
  # монтируем временную ФС
  sudo mount -t tmpfs -o size=1M tmpfs "$TMPFS_DIR"

  # получаем ключ по указанному URL
  # ничего не мешает получать по 2-3 ключа из разных источников и
  # конкатенировать их в один файл
  wget -q -O "$IMG_FILE" "$SECRET"

  if [ $? -ne 0 ] || [ ! -f "$IMG_FILE" ] ; then
    echo "No key file loaded!"
    sudo umount "$TMPFS_DIR"
    rmdir "$TMPFS_DIR"
    exit 0
  fi

  # формируем 512-битный ключ
  sha512sum "$IMG_FILE" | awk '{print $1}' | xxd -r -p > $KEY_FILE

  # затираем и удаляем источник - в памяти ничего не остается
  fsize=$(stat -c "%s" "$IMG_FILE")
  dd if=/dev/urandom of="$IMG_FILE" bs=$fsize count=1 status=none
  rm -f "$IMG_FILE"

  chmod 600 "$KEY_FILE"
}

# очистка ключа - затираем и удаляем
cleanup_key() {
  if [[ -f "$KEY_FILE" ]]; then
    dd if=/dev/urandom of="$KEY_FILE" bs=64 count=1 status=none
    rm -f "$KEY_FILE"
  fi

  sudo umount "$TMPFS_DIR"
  rmdir "$TMPFS_DIR"
}

# процедура форматирования/создания диска
format_file(){
  setvars;
  getconfig;

  sudo mkdir -p $DATA_DIR

  # если диска нет - создаем
  if [ ! -f "$DISK_IMAGE" ] ; then
    echo -n "Enter disk size (MB): "
    read size; size=$(( $size * 1 ));
    if [ $size -gt 0 ] ; then
      sudo dd if=/dev/urandom of="$DISK_IMAGE" bs=1M count=$size status=progress
    else
      echo "Size must be greater than 0!"
    fi
  fi

  # если есть и не используется - меняем ключ
  if [ -f "$DISK_IMAGE" ] ; then
    if is_mapped; then
      echo "Encripted disk already opened"
    else
      fetch_key
      echo "Open encryped disk..."
      sudo cryptsetup luksFormat "$DISK_IMAGE" "$KEY_FILE"
      sudo cryptsetup luksOpen "$DISK_IMAGE" "$CRYPT_NAME" --key-file "$KEY_FILE"
      sudo mkfs.ext4 "/dev/mapper/$CRYPT_NAME"
      sudo cryptsetup luksClose "$CRYPT_NAME"
      cleanup_key
    fi
  fi
}

# процедура монтирования
mount_disk() {
  setvars;
  getconfig;

  if [ -f "$DISK_IMAGE" ] ; then
    if is_mapped; then
      echo "Encripted disk already opened"
    else
      fetch_key
      echo "Open encryped disk..."
      sudo cryptsetup luksOpen "$DISK_IMAGE" "$CRYPT_NAME" --key-file "$KEY_FILE"
      cleanup_key
    fi

    if is_mapped; then
      if is_mounted; then
        echo "Already mounted $MOUNT_POINT."
      else
        echo "Mount..."
        mkdir -p "$MOUNT_POINT"
        if [ $? -eq 0 ] ; then
          sudo mount /dev/mapper/"$CRYPT_NAME" "$MOUNT_POINT"
          sudo chown $USER "$MOUNT_POINT"
          sudo chmod 700 "$MOUNT_POINT"
        fi
      fi
    else
     echo "Incorrect key"
    fi
  fi
}

# процедура отключения диска
unmount_disk() {
  setvars;

  if [ ! $UMOUNT_FORCE ] ; then
    # проверка на наличие рабочих сессий - для автоотключения
    USER_SESSIONS=$(who | grep -w "$USER" | wc -l)
    USER_PROCESSES=$(ps -u "$USER" --no-headers | wc -l)
  fi

  if [ "$USER_SESSIONS" -gt 1 ] || [ "$USER_PROCESSES" -gt 0 ]; then
    echo "User $USER logged in ($USER_SESSIONS sessions, $USER_PROCESSES processes)"
  else
    echo "Last session $USER ended"

    if is_mounted; then
      echo "Unmount..."
      sudo umount "$MOUNT_POINT"
      rmdir "$MOUNT_POINT"
    else
      echo "Already unmounted."
    fi

    if is_mapped; then
      echo "Close LUKS-volume..."
      sudo cryptsetup luksClose "$CRYPT_NAME"
    else
      echo "LUKS-volume closed."
    fi
   fi
  fi
}

usage(){
  cat <.conf:
=============================
data_dir = /var/spool/paranoid
disk_image = data.dump
secret = http://url/secret_file.ext
=============================

Interactive mode:

$0 format 
  Create new or format existing cryptodisk with SECRET

$0 format 
  Create new or format existing cryptodisk with SECRET for user 

$0 mount
  Mount cryptodisk (if exists)

$0 unmount
  Unmount cryptodisk (if mounted)


PAM mode:

/etc/pam.d/common-session:
session optional  pam_exec.so $0

If used fscrypt - do it twice, before and after pam_fscrypt.so
EOF
}

#=============================
checks;
uid=$(id -u)

case "$1" in
  mount)
    mount_disk
    ;;
  unmount)
    UMOUNT_FORCE=1
    unmount_disk
    ;;
  format)
    if [ "x$2" != "x" ] ; then
      if [ $uid -eq 0 ] ; then
        USER="$2"
        echo "Run for user $USER"
      else
        echo "User change ignored"
        exit 0
      fi
    fi
    format_file;
    ;;
  *)
    case "$PAM_TYPE" in
      open_session)
        USER=$PAM_USER
        mount_disk
        ;;
      close_session)
        USER=$PAM_USER
        unmount_disk
        ;;
      *)
        usage
        ;;
    esac
    ;;
esac

Теперь нужно создать конфиг для нашего юзера — вон как там написано:

/etc/paranoid/users/vasya.conf:

data_dir = /var/spool/paranoid
disk_image = data.dump
secret = http://image172.jpg

Если работаем под vasya:

paranoid format

Если под рутом:

paranoid format vasya

Указываем размер диска в мегабайтах, и если всё на своих местах — файл будет создан, зашифрован и отформатирован.
Подключаем — отключаем:

paranoid mount
paranoid unmount

Для автоматизации внесем записи в /etc/pam.d/common-session:

session optional pam_exec.so /usr/local/bin/paranoid

Если использовалось fscrypt для шифрования домашнего каталога — то дважды:

session optional pam_exec.so /usr/local/bin/paranoid
session optional pam_fscrypt.so
session optional pam_exec.so /usr/local/bin/paranoid

Смысл в том, что скрипты вызываются и при входе, и при выходе -, но при входе он сработает только после fscrypt, а при выходе его надо вызывать до fscrypt.
Повторный запуск скрипту не мешает.

Теперь при логине в систему в домашнем каталоге юзера появляется примонтированный секретный диск, который исчезает при выходе из системы.
Либо можно принудительно его закрыть командой.
Если по любой причине ключевой файл недоступен или изменился — диск останется зашифрованным.
Если при этом запустить paranoid format — то еще и перешифрованным под новый ключ, с потерей всего что там было ранее записано.

Более того, файл диска не обязан быть именно файлом на диске — это может быть вообще подключаемая флешка или ISCSI-диск (data_dir = »/dev», disk_image = «sdb1» или подобное).
Если он в наличии — будет молча примонтирован, если нет — то нет.

© Habrahabr.ru