223 lines
7.3 KiB
Go
223 lines
7.3 KiB
Go
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))
|
||
}
|