Files
hotp_calc/cmd/hotpcalc/output.go
T

223 lines
7.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"encoding/hex"
"fmt"
"io"
"strings"
"text/tabwriter"
"time"
"hotpcalc/internal/otp"
"hotpcalc/internal/store"
)
// entry — одно рассчитанное значение.
type entry struct {
Index int `json:"index"`
Counter uint64 `json:"counter"`
Code string `json:"code"`
ValidFrom time.Time `json:"valid_from,omitzero"`
ValidTo time.Time `json:"valid_to,omitzero"`
Current bool `json:"current,omitempty"`
}
// position — найденная позиция токена и рассчитанные от неё значения.
type position struct {
Counter uint64 `json:"counter"` // позиция первого введённого кода
ValidFrom time.Time `json:"valid_from,omitzero"`
ValidTo time.Time `json:"valid_to,omitzero"`
DriftSteps int64 `json:"drift_steps,omitempty"`
DriftSeconds int64 `json:"drift_seconds,omitempty"`
Codes []entry `json:"codes"`
}
// result — полный результат работы программы.
type result struct {
Mode string `json:"mode"`
Algorithm string `json:"algorithm"`
Digits int `json:"digits"`
StepSeconds int `json:"step_seconds,omitempty"`
SeedBytes int `json:"seed_bytes"`
SeedShown string `json:"seed"`
SeedForm string `json:"seed_format"`
Label string `json:"label,omitempty"`
OTP1 string `json:"otp1,omitempty"`
OTP2 string `json:"otp2,omitempty"`
Direct bool `json:"direct,omitempty"`
Ambiguous bool `json:"ambiguous"`
Positions []position `json:"positions"`
}
const tsLayout = "2006-01-02 15:04:05"
func printText(w io.Writer, r result, totpMode bool) {
unit, unitGen := "счётчик", "счётчика"
if totpMode {
unit, unitGen = "шаг", "шага"
}
fmt.Fprintf(w, "Seed: %s (%d байт, %s)\n", r.SeedShown, r.SeedBytes, r.SeedForm)
if r.Label != "" {
fmt.Fprintf(w, "Токен: %s\n", r.Label)
}
fmt.Fprintf(w, "Режим: %s, %s, %d разрядов", upper(r.Mode), r.Algorithm, r.Digits)
if totpMode {
fmt.Fprintf(w, ", шаг %d с", r.StepSeconds)
}
fmt.Fprintln(w)
if r.SeedBytes != 20 {
fmt.Fprintf(w, "Внимание: длина seed %d байт; для eToken PASS ожидается 20 байт.\n", r.SeedBytes)
}
for i, p := range r.Positions {
if len(r.Positions) > 1 {
fmt.Fprintf(w, "\n=== Совпадение %d из %d ===\n", i+1, len(r.Positions))
}
if r.Direct {
fmt.Fprintf(w, "Расчёт: начиная с %s %d (поиск не выполнялся)\n", unitGen, p.Counter)
} else {
fmt.Fprintf(w, "Позиция: код %s → %s %d, код %s → %s %d\n",
r.OTP1, unit, p.Counter, r.OTP2, unit, p.Counter+1)
if totpMode {
fmt.Fprintf(w, "Часы: %s\n", driftText(p.DriftSteps, p.DriftSeconds))
}
}
fmt.Fprintf(w, "\nСледующие значения (%d):\n\n", len(p.Codes))
// Каждая ячейка завершается табуляцией, иначе tabwriter не выравнивает
// последнюю колонку строки.
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', tabwriter.AlignRight)
if totpMode {
fmt.Fprintf(tw, " #\t%s\tКод\t Действителен\n", upperFirst(unit))
} else {
fmt.Fprintf(tw, " #\t%s\tКод\t\n", upperFirst(unit))
}
for _, e := range p.Codes {
if totpMode {
mark := ""
if e.Current {
mark = " ← действует сейчас"
}
fmt.Fprintf(tw, " %d\t%d\t%s\t %s – %s%s\n", e.Index, e.Counter, e.Code,
e.ValidFrom.Local().Format(tsLayout), e.ValidTo.Local().Format("15:04:05"), mark)
} else {
fmt.Fprintf(tw, " %d\t%d\t%s\t\n", e.Index, e.Counter, e.Code)
}
}
tw.Flush()
}
if r.Ambiguous {
fmt.Fprintln(w, "\nВнимание: введённой паре кодов соответствует несколько позиций.")
if len(r.Positions) == 1 {
fmt.Fprintln(w, "Показана первая из них; полный список — с флагом -all.")
}
fmt.Fprintln(w, "Сузьте диапазон перебора или снимите с токена ещё одно значение.")
}
}
func driftText(steps, seconds int64) string {
switch {
case steps == 0:
return "совпадают с этой машиной"
case steps > 0:
return fmt.Sprintf("токен опережает эту машину примерно на %d с (%d %s)",
seconds, steps, plural(steps, "шаг", "шага", "шагов"))
default:
return fmt.Sprintf("токен отстаёт от этой машины примерно на %d с (%d %s)",
-seconds, -steps, plural(-steps, "шаг", "шага", "шагов"))
}
}
// plural подбирает форму русского существительного для числа n.
func plural(n int64, one, few, many string) string {
if n < 0 {
n = -n
}
switch {
case n%100 >= 11 && n%100 <= 14:
return many
case n%10 == 1:
return one
case n%10 >= 2 && n%10 <= 4:
return few
}
return many
}
func upper(s string) string {
return strings.ToUpper(s)
}
func upperFirst(s string) string {
r := []rune(s)
if len(r) == 0 {
return s
}
return strings.ToUpper(string(r[0])) + string(r[1:])
}
// printTokenDetails печатает параметры и состояние одного токена.
func printTokenDetails(w io.Writer, t store.Token, showSeed bool) {
seed := otp.Fingerprint(t.Secret)
if showSeed {
seed = strings.ToUpper(hex.EncodeToString(t.Secret))
}
fmt.Fprintf(w, "\n %s", t.Serial)
if t.Label != "" {
fmt.Fprintf(w, " (%s)", t.Label)
}
fmt.Fprintln(w)
tw := tabwriter.NewWriter(w, 0, 0, 1, ' ', 0)
fmt.Fprintf(tw, " тип\t%s, %s, %d разрядов", upper(t.Type), t.Alg, t.Digits)
if t.IsTOTP() {
fmt.Fprintf(tw, ", шаг %d с", int(t.Config().Step/time.Second))
}
fmt.Fprintln(tw)
fmt.Fprintf(tw, " seed\t%s (%d байт)\n", seed, len(t.Secret))
if t.IsTOTP() {
if t.LastStep > 0 {
fmt.Fprintf(tw, " последний шаг\t%d\n", t.LastStep)
}
} else {
fmt.Fprintf(tw, " счётчик\t%d (значение следующего нажатия)\n", t.Counter)
}
if !t.Created.IsZero() {
fmt.Fprintf(tw, " выпущен\t%s\n", t.Created.Local().Format(tsLayout))
}
if !t.LastUsed.IsZero() {
fmt.Fprintf(tw, " использован\t%s\n", t.LastUsed.Local().Format(tsLayout))
}
tw.Flush()
}
// printTokenList печатает таблицу токенов хранилища.
func printTokenList(w io.Writer, tokens []store.Token, path string) {
if len(tokens) == 0 {
fmt.Fprintf(w, "Хранилище %s пусто.\nСоздайте токен командой: hotpcalc new\n", path)
return
}
fmt.Fprintf(w, "Хранилище: %s\n\n", path)
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
fmt.Fprintln(tw, " СЕРИЙНЫЙ НОМЕР\tТИП\tРАЗР.\tСЧЁТЧИК\tИСПОЛЬЗОВАН\tМЕТКА")
for _, t := range tokens {
used := "—"
if !t.LastUsed.IsZero() {
used = t.LastUsed.Local().Format(tsLayout)
}
label := t.Label
if label == "" {
label = "—"
}
fmt.Fprintf(tw, " %s\t%s\t%d\t%s\t%s\t%s\n",
t.Serial, upper(t.Type), t.Digits, counterString(t), used, label)
}
tw.Flush()
fmt.Fprintf(w, "\nВсего токенов: %d\n", len(tokens))
}