summaryrefslogtreecommitdiff
path: root/lib/tfa/cli.rb
blob: 529d2f86bd0ab2582a7f82e0ab35326161a93586 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
require "thor"

module TFA
  class CLI < Thor
    package_name "TFA"
    class_option :filename
    class_option :directory
    class_option :passphrase

    desc "add NAME SECRET", "add a new secret to the database"
    def add(name, secret)
      storage.save(name, clean(secret))
      "Added #{name}"
    end

    desc "destroy NAME", "remove the secret associated with the name"
    def destroy(name)
      storage.delete(name)
    end

    desc "show NAME", "shows the secret for the given key"
    method_option :format, default: "raw", enum: ["raw", "qrcode"], desc: "The format to export"
    def show(name = nil)
      if name
        secret = storage.secret_for(name)
        case options[:format]
        when "qrcode"
          require 'rqrcode'
          RQRCode::QRCode.new("otpauth://totp/unknown@example.org?secret=#{secret}&issuer=#{name}").as_ansi(
            light: "\033[47m", dark: "\033[40m", fill_character: '  ', quiet_zone_size: 1
          )
        else
          secret
        end
      else
        storage.all.map { |x| x.keys }.flatten.sort
      end
    end

    desc "totp NAME", "generate a Time based One Time Password using the secret associated with the given NAME."
    def totp(name)
      TotpCommand.new(storage).run(name)
    end

    desc "now SECRET", "generate a Time based One Time Password for the given secret"
    def now(secret)
      TotpCommand.new(storage).run('', secret)
    end

    desc "upgrade", "upgrade the database."
    def upgrade
      if !File.exist?(pstore_path)
        say_status :error, "Unable to detect #{pstore_path}", :red
        return
      end
      if File.exist?(secure_path)
        say_status :error, "The new database format was detected.", :red
        return
      end

      if yes? "Upgrade to #{secure_path}?"
        secure_storage
        pstore_storage.each do |row|
          row.each do |name, secret|
            secure_storage.save(name, secret) if yes?("Migrate `#{name}`?")
          end
        end
        File.delete(pstore_path) if yes?("Delete `#{pstore_path}`?")
      end
    end

    desc "encrypt", "encrypts the tfa database"
    def encrypt
      return unless ensure_upgraded!

      secure_storage.encrypt!
    end

    desc "decrypt", "decrypts the tfa database"
    def decrypt
      return unless ensure_upgraded!

      secure_storage.decrypt!
    end

    private

    def storage
      File.exist?(pstore_path) ? pstore_storage : secure_storage
    end

    def pstore_storage
      @pstore_storage ||= Storage.new(pstore_path)
    end

    def secure_storage
      @secure_storage ||= SecureStorage.new(Storage.new(secure_path), ->{ passphrase })
    end

    def filename
      options[:filename] || '.tfa'
    end

    def directory
      options[:directory] || Dir.home
    end

    def pstore_path
      File.join(directory, "#{filename}.pstore")
    end

    def secure_path
      File.join(directory, filename)
    end

    def clean(secret)
      if secret.include?("=")
        /secret=([^&]*)/.match(secret).captures.first
      else
        secret
      end
    end

    def passphrase
      @passphrase ||=
        begin
          result = options[:passphrase] || ask("Enter passphrase:\n", echo: false)
          raise "Invalid Passphrase" if result.nil? || result.strip.empty?
          result
        end
    end

    def ensure_upgraded!
      return true if upgraded?

      error = "Use the `upgrade` command to upgrade your database."
      say_status :error, error, :red
      false
    end

    def upgraded?
      !File.exist?(pstore_path) && File.exist?(secure_path)
    end
  end
end