[#] pass - консольный менеджер паролей
btimofeev(tavern,13) — All
2020-08-31 19:36:10


** Для повышения количества контента в сети делаю репост статьи из моего блога, оригинал здесь https://emunix.org/post/pass-cli/ **



Pass - это небольшой bash-скрипт хранящий пароли в обычных текстовых файлах зашифрованных с помощью GPG. Файлы можно организовывать в директории, при этом имя файла является названием сайта или ресурса для которого файл хранит пароль. Файлы имеют очень простой формат: первая строка содержит только пароль, все остальные строки содержат любые другие данные. Например, файл github.com.gpg может иметь такое содержимое:

md3rTks3!=
login: Maria
url: https://github.com/login 
email: maria@example.com

Указывать поля вида login: и url: именно в таком формате не обязательно, но подобные метаданные позволяют менеджеру паролей автоматически подставлять ваш логин на веб-сайтах.

Очень простой формат файла и организации хранения паролей дает нам большие возможности: мы можем просматривать и редактировать пароли без установленной программы pass (достаточно иметь установленные GnuPG и любой текстовый редактор), мы можем писать свои скрипты для работы с паролями, мы можем хранить все изменения паролей в системе контроля версий git и синхронизировать пароли на разных компьютерах через неё же. Также pass имеет большую поддержку со стороны сообщества: существуют реализации программы для Android и iOS, плагины для веб-браузеров Chrome и Firefox, графические клиенты для Windows, Mac и Linux, расширения для Alfred, dmenu, rofi и Emacs, скрипты для импорта паролей из других приложений.

В этой статье мы рассмотрим именно консольную версию, доступную на сайте https://www.passwordstore.org/


Установка
=========

Установить программу вы можете с помощью пакетного менеджера своего дистрибутива, например:

- для Ubuntu \ Debian: sudo apt-get install pass
- для ArchLinux: pacman -S pass
- для macOS: brew install pass


Предварительная настройка
========================

Чтобы использовать программу вам нужен gpg-ключ, которым будут шифроваться файлы с паролями. Если у вас ещё нет своего ключа, то прочтите вот эту статью и создайте его.

Далее пишем в терминале pass init your@e-mail.ru, где вместо your@e-mail.ru вам необходимо написать адрес электронной почты, который вы указывали при создании gpg-ключа (здесь e-mail используется в качестве gpg-id).

Эта команда создаст директорию ~/.password-store в которой будут храниться ваши пароли и запомнит каким ключом их шифровать (вы также можете использовать разные gpg ключи для разных поддиректорий, для этого есть флаг -s: pass init -s поддиректория second@key-id.com)

Если вы хотите, чтобы директория с паролями хранилась в git-репозитории, то выполните команду pass git init. После этого при каждом создании\изменении паролей pass будет автоматически делать коммит в репозиторий.


Использование
=============

Чтобы добавить пароль выполите команду pass insert email/your@email.ru и введите пароль который хотите сохранить.

Эта команда создаст поддиректорию email в хранилище паролей, добавит в неё зашифрованый текстовый файл your@email.ru.gpg, в котором сохранит введённый вами пароль.

Если вам нужно по-быстрому сгенерировать пароль, то можете написать pass generate youtube.com 15. Эта команда сгенерирует новый пароль длинной 15 символов, запишет его в файл youtube.com.gpg и покажет на экране. Если вы добавите опцию -n, то пароль будет состоять только из букв и цифр (без специальных символов), если добавите опцию -c, то пароль сразу будет скопирован в буфер обмена.

Ранее я писал, что в файле может содержаться не только пароль, но и другая текстовая информация (например логин или просто какие-то заметки). Для того что бы записать несколько строк в файл вы можете использовать опции –multiline или -m: pass insert -m web/github.com. Напоминаю: пароль вводится в первой строке, а все остальные данные уже после него на отдельных строках (см. пример в начале статьи). Что бы закончить ввод и сохранить данные нажмите Ctrl+d.

Изменить файл с паролем можно командой pass edit web/github.com. При этом откроется редактор указанный в переменной окружения EDITOR. Этой командой также можно добавлять в хранилище новые пароли, если вам удобнее делать это через текстовый редактор.

Для удаления файла с паролем используется команда pass rm web/github.com, а для переименования pass mv старое_название новое_название.

Для того чтобы показать пароль на экране просто введите pass web/github.com. При этом, чтобы не вводить длинные названия сайтов, вы можете пользоваться автодополнением bash нажимая клавишу Tab. Используйте флаг -c чтобы сразу скопировать пароль в буфер обмена: pass -с web/github.com.

Посмотреть какие вообще есть записи в хранилище можно просто введя pass (а для просмотра записей в отдельной директории pass имя_директории).

Вы можете искать файлы по части названия pass find строка_поиска и, более того, вы можете искать внутри всех файлов с паролями pass grep строка_поиска (правда это действие довольно медленное).


Версионирование и синхронизация
==============================

Выше я писал, что если вы выполните команду pass git init, то программа создаст внутри хранилища паролей git-репозиторий и будет автоматически коммитить все изменения. А это означает, что из коробки у нас появляется синхроницация паролей на разных компьютерах.

Вы можете использовать любые команды git, написав перед ними слово pass. Это нужно, чтобы перед их вызовом вам не приходилось каждый раз переходить в директорию ~/.password-store.

Добавляем адрес репозитория на вашем сервере pass git remote add origin your-remote-server.com:pass-store.

Забираем с него файлы с паролями pass git pull.

Сохраняем добавленные или изменённые пароли на сервер pass git push.


Заключение
==========

Как видите pass очень простая, но мощная утилита, следующая философии unix. Часто её ругают за то, что названия файлов с паролями хранятся в открытом виде (а они обычно представляют собой названия сайтов на которых вы зарегистрированы). Но лично мне нравится настолько простой формат хранения паролей.

[#] Re: pass - консольный менеджер паролей
tuple(ping,54) — btimofeev
2024-09-30 17:21:22


> Часто её ругают за то, что названия файлов с паролями хранятся в открытом виде (а они обычно представляют собой названия сайтов на которых вы зарегистрированы). Но лично мне нравится настолько простой формат хранения паролей.

Существует расширение, которое позволяет хранить дерево сайтов в "гробнице" - https://github.com/roddhjav/pass-tomb#readme

---

А ещё есть расширение, которое позволяет использовать pass для двухфакторного входа - OTP (правда, смысл двухфакторки теряется) - https://github.com/tadfisher/pass-otp#readme
У яндекса тоже есть OTP, но у них свой - YAOTP - с дополнительным ключом - пин-кодом. Для него использую https://github.com/tujh2/yaGotp

Для себя я писал отдельный скрипт, который работает как клиент только для чтения хранилища pass. Одной командой он копирует пароль, а другой командой парсит и копирует поле "login: blabla" или "email: gg@gg.ru", если не найдено первое.

[#] Re: pass - консольный менеджер паролей
btimofeev(ping,6) — tuple
2024-10-01 15:39:06


tuple> Для себя я писал отдельный скрипт, который работает как клиент только для чтения хранилища pass. Одной командой он копирует пароль, а другой командой парсит и копирует поле "login: blabla" или "email: gg@gg.ru", если не найдено первое.

Я использую скрипт https://github.com/carnager/rofi-pass , который позволяет искать и копировать логины\пароли через rofi (https://github.com/davatorium/rofi )
P.S. Edited: 2024-10-01 15:39:40

[#] Re: pass - консольный менеджер паролей
tuple(ping,54) — btimofeev
2024-10-19 09:42:54


btimofeev> Я использую скрипт https://github.com/carnager/rofi-pass , который позволяет искать и копировать логины\пароли через rofi (https://github.com/davatorium/rofi )

С помощью rofi работает у меня сторонний скрипт bemoji для выбора эмодзи из UTF-8: https://github.com/marty-oehme/bemoji .

Кстати, вот сам скрипт "pypassmenu" на текущий момент. Пусть под лицензией UNLICENSE. Далёк от идеала, но может кто вдохновится или будет интересно.
#!/usr/bin/env python

import os
import re
import sys
import logging
from time import sleep
from pathlib import Path
from subprocess import Popen, PIPE
from argparse import ArgumentParser


class PassFile:
    def __init__(self, name: str, content: str):
        self.name = name
        self._lines: list[str] = content.split('\n')
        self._fields: dict[str, str] = {}
        for line in self._lines:
            m = re.match(r'(.+): (.+)', line)
            if m:
                self._fields[m.group(1)] = m.group(2)
        logging.info(f'Created PassFile instance of {name}')
        logging.debug(f'PassFile fields: {self._fields}')

    @property
    def password(self) -> str:
        return self._lines[0]

    def _get_field(self, field: str) -> str:
        return self._fields[field]

    @property
    def email(self) -> str:
        try:
            logging.debug('Trying to find "email" field')
            return self._get_field('email')
        except KeyError:
            logging.debug('Falling back to "e-mail" field')
            return self._get_field('e-mail')

    @property
    def login(self) -> str:
        try:
            logging.debug('Trying to find "login" field')
            return self._get_field('login')
        except KeyError:
            logging.debug('Falling back to e(-)mail field')
            return self.email


def fetch_passfiles() -> dict[str,Path]:
    store_path = Path(os.getenv('PASSWORD_STORE_DIR') or
                      os.getenv('HOME') + '/.password-store')
    gpg_files = list(store_path.glob('**/*.gpg'))
    gpg_files_dict = {}
    for filepath in gpg_files:
        relative_filepath = str(filepath.relative_to(store_path))
        gpg_files_dict[re.sub(r'\.gpg$', '', relative_filepath)] = filepath
    return gpg_files_dict

def choose_passfile_with_dmenu(gpg_files_dict: dict[str,Path]) -> tuple[str, Path]:
    p = Popen(['dmenu'], stdout=PIPE, stdin=PIPE, text=True)
    index = '\n'.join([key for key in gpg_files_dict])
    stdout = p.communicate(input=index)[0]
    choosen_filename = stdout.strip()
    return choosen_filename, gpg_files_dict[choosen_filename]

def fetch_passfile_by_path(filename: str, filepath: Path) -> PassFile:
    p = Popen(['gpg', '-d', '--quiet', str(filepath)], stdout=PIPE, text=True)
    return PassFile(filename, p.stdout.read().strip())

def copy_to_clipboard(text: str):
    p = Popen(['xclip', '-selection', 'clipboard'], stdin=PIPE, text=True)
    p.communicate(input=text)
    logging.info('Clipboard was written')
    logging.debug(f'Clipboard content: {text}')

def clear_clipboard():
    logging.debug('Clipboard cleared!')
    copy_to_clipboard('')

def notify(header: str, body: str):
    Popen(['notify-send', header, body])
    logging.info('Notification sent')
    logging.debug(f'Notification content:\n{header=}\n{body=}')

def main():
    parser = ArgumentParser(prog='pypassmenu',
                            description='Improved passmenu written in python')
    supported_fields = ( 'password', 'login', 'email', )
    parser.add_argument('field', type=str, choices=supported_fields,
                        default=supported_fields[0], help='Obtained field')
    args = parser.parse_args()
    
    # logging.basicConfig(level=logging.DEBUG)

    filename, filepath = choose_passfile_with_dmenu(fetch_passfiles())
    pass_ = fetch_passfile_by_path(filename, filepath)
    important_value = 'something'
    try:
        match args.field:
            case 'password':
                important_value = pass_.password
            case 'login':
                important_value = pass_.login
            case 'email':
                important_value = pass_.email
    except KeyError:
        notify('Не удалось найти поле',
               f'pypassmenu: выбранное поле не найдено в {pass_.name}')
        sys.exit(1)
    copy_to_clipboard(important_value)
    sleep(20)
    clear_clipboard()

if __name__ == '__main__':
    main()