Надоело постоянно пользоваться телефоном, поэтому написал замену Google Authenticator в виде TUI

в 7:15, , рубрики: 2fa, 2FA-аутентификация, github, go2fa, security, totp, tui

Мне надоело постоянно использовать Google Authenticator и переключаться между ПК и телефоном для подтверждения двухфакторной (мультифакторной) аутентификации. Красивых и функциональных TOTP (Time-based one-time password) хранилок в терминале я не нашел, поэтому сделал эту TUI, которая позволит хранить, управлять, просматривать, копировать 2FA ключ в пару нажатий с поддержкой VIM управления. Ну и просто мне было интересно, какого это создавать свои TUI приложения.

go2fa

go2fa

Репозиторий на GitHub - go2fa

Начем с того, что такое TUI?

TUI - Terminal User Interface. Это когда мы пытаемся сделать что-то похоже на GUI интерфейс, но в терминале. В последнее время это становится все более привлекательным и удобным, появляется много реализаций опенсорсных инстументов, например такие как gopass, lazygit, lazydocker
Вы можете сказать, что я изобретаю велосипед, это действительно так, но для себя отмечу несколько пунктов:

  • Первое, и самое главное, я пишу в vim и использую tmux, а это значит, что 90% времени провожу на клавиатуре и в терминале, поэтому логичнее сделать инструмент там, где будешь пользоваться.

  • Второе, я не нашел красивых и функциональных туишек для работы с TOTP ключами. Все решения представляли из себя просто набор команд, которые требуется вводить и потом еще руками копировать этот ключ. А мне хотелось решение, которое будет постоянно обвовлять ключи и не будет проблем с копированием.

  • Ну и третье, мне было очень интересно создать свою TUI, понять, как это работает и какие открывает возможности для творчества. А потом уже поделиться с другими.

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

На чем можно сделать TUI

На самом деле, много на чем, хоть на shell или php, но я бы не сказал, что для меня это было бы удобно. Решил взять за основу решил взять язык Go, так как частенько его использую для скриптинга и давно хотел попробовать сделать tui на Bubbletea - фреймворк для реализации TUI приложение.

Что нужно реализовать

Нужно было реализовать следующее:

  1. Главный экран, где можно будет выбрать что мы делаем, смотрим ключи или добавляем новые

  2. В просмотре ключей должна быть таблица с фильтрацией, сразу должен быть показ ключей и анимация протухания. Когда ключи свежие, то индикация белая, когда переходят 15 секунд, то желтая и если осталось 5 секунд до протухания, то красная. Должен быть выбор через стрелочки и hjkl (кнопки управления в vim), а также, скрывать ключи, которые не выбраны.

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

  4. Для удаления отдельный экран с подтверждением

  5. Отдельный экран для создания ключей, где можно задать название, описание и сам SecretKey на основе которого генерируются TOTP ключи, естественно с валидацией, если ключ не подходит по формату или что-то не заполнили

  6. Сохранение в локальное хранилище и шифрование этого хранилища

  7. Бекапы при создании новых ключей, чтобы можно было откатиться обратно, если вдруг удалили то, что не должны были удалять

Вся реализация заняла примерно пару дней по несколько часов. Дальше расскажу чуть подробнее те моменты, про которые есть что рассказать.

TOTP

TOTP - Time-based one-time password - одноразовые пароли для двухфакторное или любой другой аутентификации. Тут нужно понимать только базу, что у вас есть какой-то Secret Key и временная метка, для генерации этих паролей и валидации данных использовалась либа - https://github.com/xlzd/gotp, там можете подробнее прочитать про сам TOTP.

В коде для этого всего пару строк:

func GenerateTOTP(utf8string string) (string, int64) {
	return gotp.NewDefaultTOTP(utf8string).NowWithExpiration()
}

в utf8string помещается ваш секретный ключ и все прекрасно работает.

Хранилище

Хранилище представляет из себя json файл, которые располагается в директории $HOME/.local/share/go2fa/stores/vault.json
Где iterator - это текущая степень итерации, а db - основное зашифрованное хранилище.

{
  "iterator": 29,
  "db": "bJvzEpGPZFG2hS0LI3pYOoDHP/5kLO23E9IKEpsUrt6mSL/HudDftPwlUcnCeKBQCaJ0zmpPQpmcw+3IFlPv6+VMJmK7cSgyoHh7JBFwHW2oqBUfemU1wk+/mfBu4HEtD+HoV/XR6XRl+P/tzMBenUkVMzmw1WPCE4U2NOtTkEojxIb6lsi+72GpDGTY3W8namNwWq9i8IoSkIBKVcVhew3sPkW8nTHwz3fEDDnBWDxhEQsViUdrLvxmgX34sBTDlu6v1DZ4R9+Hwd2eGoTZ/mBOGGqI3NGxW/yShIPpy0VrW2uSUasLdc51bAsKxc5fK7EQm1KFchpk1H4N36Dh06Kqmzf/irLJQCPPVr1v5QtpHtCzJm4mEb5ZVZmr+gEz9oIJw6pOztk4UDVw3YDa/7SBSfoxqBW/Mm1DuXJxH/prbCLrKqxr+CcqIasI8H6KcQm2AIRBuKwkR8MaBRpV85rN+XOtWapd/W8LzFbHqy1mDTm7DznC4ZoXIjBDdYNbwwi/zcR7ID4le1TMOszXZtA4l0XThxuk5MqdwBB8sCy4Xe1BxZ6Kf0q0MNlx89gnlRjEE5Ym+Ven5Rqiea+YoNM7Uq0t9o2SoddgvPz+0NvzaPnU4342ukRm0r5wmHvmqoBxdeYC1VkQXI72dPjMWrBloybnLsSDwaoLxhyCA="
}

Такой формат был выбран, чтобы использовать дополнительную мета-информацию. Сейчас это iterator, но в будущем можно расширять как-угодно.
Самое db - это base64 байтовое значение, которое зашифровано с помощью RSA.

Слишком заморачиваться с шифрование я не хотел, да и работа должна оставаться быстрой. С учетом того, что это можно закинуть в репозиторий, главное не закидывать туда приватный ключ. Считаю, что вполне нормальная реализация. Хотя понятие "нормально" будет отличаться от каждого человека, вы можете сами сделать так, как нравится. Да и не особо я знаток шифрования, если есть полезная информация, поделитесь, изучу.

Помимо самого хранилища, при создании новых ключей или при удалении, в $HOME/.local/share/go2fa/backups сохраняются бекапы и вы можете восстановить любую предыдущую итерацию, конечно же, если есть приватный ключ.

А сами ключи хранятся в $HOME/.local/share/go2fa/keys, если будете что-то дропать с системы, не забудьте про этот момент.

Немного про Bubbletea

В целом, делать tuiшки на основе данного фреймворка прикольно, сначала не очень понятно, особенно когда дело доходит до смены экранов и состояний, но после просмотра examples все более менее становится на свои места.

У них действительно много примеров, если есть вопросы, то можно залезть в issue и попробовать найти их там. А так, все дело в попытках. Но, как рекомендацию, я бы посоветовал сначала ознакомиться с архитектурой Elm, так как фреймворк основан на ней и потом все проще понимать.

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

На что хотел бы обратить внимание - это смена экранов, так как может вызывать трудности.
У меня в проекте есть папка go2fa/internal/screens, здесь находятся все реализованные экраны. Переключение между ними происходи следующим образом:

В main.go начинаем с:

func main() {
	screen_y := screens.ListMethodsScreen()

...


if _, err := tea.NewProgram(screen_y).Run(); err != nil {
	fmt.Println("Error running program:", err)
	os.Exit(1)
}

Где ListMethodsScreen - это метод, который возвращает модель экрана.

func ListMethodsScreen() ListMethodsModel {
	items := []list.Item{
		item{ title: "Show keys", alias: "show_keys" },
		item{ title: "Add key", alias: "add_key" },
	}

	const defaultWidth = 20

	l := list.New(items, itemDelegate{}, defaultWidth, listHeight)
	l.Title = "GO2FA"
	l.Styles.Title = titleStyle
	l.Styles.PaginationStyle = paginationStyle
	l.SetShowStatusBar(false)
	l.SetFilteringEnabled(false)

	output := termenv.NewOutput(os.Stdout)
	return ListMethodsModel{list: l, output: output}
}

type ListMethodsModel struct {
	list     list.Model
	quitting bool
	output   *termenv.Output
}

Здесь как раз задается структура модели list, в моем случае это просмотр ключей и добавление новых, прописывается размеры, стили и заголовок, выключается статус бар и фильтрация.

Кстати, задавать стили одно удовольствие

var (
	paginationStyle   = list.DefaultStyles().PaginationStyle.PaddingLeft(4)
)

l.Styles.PaginationStyle = paginationStyle

После этого мы возвращаем саму модель

return ListMethodsModel{list: l, output: output}

Теперь, когда вы запустили программу, мы попадаем сразу в первый экран. В самом файле описываем метод Update, где отслеживаем нажатие клавиш или изменение размера терминала.

func (m ListMethodsModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.WindowSizeMsg:
		m.list.SetWidth(msg.Width)
		return m, nil

	case tea.KeyMsg:
		switch msg.String() {
		case "q", "esc":
			m.quitting = true
			m.output.ClearScreen()

			return m, tea.Quit
		}

		switch msg.Type {
			case tea.KeyCtrlC:
				m.quitting = true
				m.output.ClearScreen()

				return m, tea.Quit

			case tea.KeyEnter:
				item, ok := m.list.SelectedItem().(item)
				if ok {
					if item.alias == "add_key" {
						screen_y := ScreenInputSecret()
						return RootScreen().SwitchScreen(&screen_y)
					}

					if item.alias == "show_keys" {
						screen_y := ListKeysScreen()
						return RootScreen().SwitchScreen(&screen_y)
					}
				}
		}
	}

	var cmd tea.Cmd
	m.list, cmd = m.list.Update(msg)
	return m, cmd
}

Если было событие Enter и был выбран один из элементов модели, то мы выбираем следующий экран

if item.alias == "add_key" {
	screen_y := ScreenInputSecret()
	return RootScreen().SwitchScreen(&screen_y)
}

И таким образом, через SwithScreen происходит вся магия переключения экранов.

// this is the switcher which will switch between screens
func (m rootScreenModel) SwitchScreen(model tea.Model) (tea.Model, tea.Cmd) {
    m.model = model
    return m.model, m.model.Init()    // must return .Init() to initialize the screen (and here the magic happens)
}

Чтобы сильно не растягивать статью, я привожу только базовый пример реализации, подробнее можно посмотреть в репозитории.

Публикация TUI в brew

Это был мой первый опыт публикации go приложения в brew репозитории. Homebrew я выбрал, потому что оказалось туда запушить очень просто, для этого нужно репозиторий homebrew-public и использовать goreleaser.

После в директории с вашим приложением, запускаете:

goreleaser init

у вас создастся файл .goreleaser.yaml
там будут все необходимые настройки, главное удостоверьтесь, что репозиторий подходящий.
Дальше выполняем и у вас происходит релиз. Автоматика, удобно.

goreleaser check
goreleaser release

Прикладываю статью, которой пользовался: https://dev.to/aurelievache/learning-go-by-examples-part-9-use-homebrew-goreleaser-for-distributing-a-golang-app-44ae

Итог

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

Ctrl + t - открыть новое окно терминала
go2fa - открыть tui для ключей
enter - выбираем просмотр ключей 
jk - выбираю ключ 
enter - копирую ключ и дальше просто вставляю в нужном сервисе

На автомате это происходит действительно за пару секунд. Раньше это занимало гораздо больше времени, особенно когда ты хранишь это в телефоне.

Есть небольшой недостаток того, что с Google Authentificator нельзя экспортировать ключи в каком-либо формате, чтобы потом их адекватно перенести в go2fa, поэтому приходится заново подключать 2FA в сервисах. Это занимает определенное время в самом начале, зато потом не требуется никаких действий. Ну и лучше все-таки задублировать в какую-то хранилку на телефоне или на ПК, где вы еще пользуетесь, так как бывает требуется входить в сервис и с телефона, да и безопаснее.

Если вы хотите попробовать, то достаточно поставить через brew:

brew install curkan/public/go2fa
# и запустить
go2fa

Только убедитесь, что стоит Xclip или Xsel. Иначе не будет работать копирование.

Для себя понял, что в текущих реалиях не сложно создавать tui приложения, которые вам помогают в будущем удобнее чем-то пользоваться, много разных решений и на разных языках. Я был вдохновлен gopass, lazygit, lazydocker - все это TUIшки, которые делают наши программисткую жизнь удобнее, если ты пользователь терминала.

Спасибо за прочтение, буду рад ответить на замечания и предложения. Ну и если хотите поддержать, то просто подпишитесь на телегу, я там часто рассказываю про свои проекты: https://t.me/tsurkan_hut

С Уважением, Никита Цуркан

Автор: tsurkan_n

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js