Class: KeyTool

Inherits:
Object
  • Object
show all
Defined in:
lib/universa/keytool/keytool.rb

Instance Method Summary collapse

Constructor Details

#initializeKeyTool

Returns a new instance of KeyTool.



42
43
44
45
46
47
48
# File 'lib/universa/keytool/keytool.rb', line 42

def initialize
  @require_password = true
  @autogenerate_password = false
  @tasks = []
  @rounds = 1000000
  init_parser()
end

Instance Method Details

#check_overwrite(output) ⇒ Object



108
109
110
# File 'lib/universa/keytool/keytool.rb', line 108

def check_overwrite output
  error "File #{output} already exists" if File.exists?(output) && !@overwrite
end

#init_parserObject



124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
# File 'lib/universa/keytool/keytool.rb', line 124

def init_parser
  opt_parser = OptionParser.new { |opts|
    opts.banner = ANSI.bold { "\nUniversa Key tool #{Universa::VERSION}" }
    opts.separator ""

    opts.on("--no-password",
            "create resources not protected by password. Not recommended.") { |x|
      @require_password = x
    }

    opts.on("-a", "--autogenerate_password",
            "the new secure password will be generated and shown on the console",
            "while the password is safe, printing it out to the console may not.",
            "Normally, system promts the password on the console that is",
            "more secure") { |x|
      @autogenerate_password = x
    }

    opts.on("-o FILE", "--output FILE", "file name for the output file") { |f|
      @output_file = f
    }

    opts.on("-F", "--force", "force overwrite file") {
      @overwrite = true
    }

    opts.on("-g SIZE", "--generate SIZE", "generate new private key of the specified bis size") { |s|
      task {
        strength = s.to_i
        case strength
          when 2048, 4096
            task {
              # check we have all to generate...
              output = output_file(".private.unikey")
              check_overwrite(output)
              key = PrivateKey.new(strength)
              save_key(output, key)
              puts "\nNew private key is generated: #{output}"
            }
          else
            error "Only supported key sizes are 2048, 4096"
        end
      }
    }

    opts.on("-u FILE", "--update FILE", "update password on the existing key (also add/remove",
            "requires -o output_file or --overwrite to change it in place") { |name|
      task {
        output = output_file(".private.unikey", name)
        check_overwrite output
        key = load_key(name)
        puts("\rKey loaded OK                       ")
        save_key output, key
      }
    }

    opts.on("-r ROUNDS", "--rounds ROUNDS", "how many PBKDF2 rounds to use when saving with password",
            "(1 million by default, the more the better, but takes time)") { |r|
      @rounds = human_to_i(r)
      @rounds < 100000 and error "To few rounds, use at least 100000"
    }

    opts.on("-p FILE", "--public FILE", "extract public key. If output file is not specified,",
            "prints as universa text object to the standard output") { |name|
      task {
        key = load_key(name, true)
        if @output_file
          open(output_file(".public.unikey"), 'wb') { |f| f << key.pack() }
        else
          puts format_text_object(key.pack(), "public key", fileName: name)
          # puts "not yet implemented #{key.long_address.to_s}"
        end
      }
    }

    opts.on("-s FILE", "--show FILE", "show key information") { |name|
      task {
        key = load_key(name, true)
        is_private = key.is_a? PrivateKey
        puts "\r----------------------------------------------------------------------------------------"
        puts "#{is_private ? 'Private' : 'Public'} key, #{key.info.getKeyLength() * 8} bits\n"
        puts "Short address : #{ANSI.bold { key.short_address.to_s }}"
        puts "Long  address : #{ANSI.bold { key.long_address.to_s }}"
      }
    }

    opts.separator ""

    def sample(text)
      "    " + ANSI.bold { ANSI.green { text } }
    end

    opts.on_tail("-h", "--help", "Show this message") do
      puts opts
      puts <<-End

#{ANSI.bold { "Usage samples:" }}

Generate new key 'foo.private.unikey' - will ask password from console (notice extension will be added automatically)

#{sample "unikeys -g 2048 -o foo"}

Show foo addresses:

#{sample "unikeys -s foo.private.unikey"}

Change password of the foo key and save it into new file bar.private.unikey (keeping both for security), will ask
new password from console

#{sample "unikeys -u foo.private.unikey -o bar"}

Change password in place (overwriting old file)

#{sample "unikeys -u foo.private.unikey -f"}

See project home page at #{ANSI.underline { "https://github.com/sergeych/universa" }}

      End
      exit
    end

    opts.on_tail("-v", "--version", "Show versions") do
      puts "Universa core version: #{Service.umi.core_version}"
      puts "UMI version          : #{Service.umi.version}"
      client = Universa::Client.new
      puts "Connected nodes      : #{client.size}"
      exit
    end
  }

  begin
    opt_parser.order!
    if @tasks.empty?
      puts "nothing to do. Please specify one of: -g, -s or -u. See help for more (-h)."
    else
      @tasks.each { |t| t.call }
    end
  rescue MessageException, OptionParser::ParseError => e
    STDERR.puts ANSI.red { ANSI.bold { "\nError: #{e}\n" } }
    exit(1000)
  rescue Interrupt
    exit(1010)
  rescue
    STDERR.puts ANSI.red { "\n#{$!.backtrace.reverse.join("\n")}\n" }
    STDERR.puts ANSI.red { ANSI.bold { "Error: #$! (#{$!.class.name})" } }
    exit(2000)
  end
end

#load_key(name, allow_public = false) ⇒ Object



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
# File 'lib/universa/keytool/keytool.rb', line 82

def load_key(name, allow_public = false)
  packed = open(name, 'rb') { |f| f.read } rescue error("can't read file: #{name}")
  begin
    PrivateKey.from_packed(packed)
  rescue Exception => e
    if e.message.include?('PasswordProtectedException')
      puts "\nThe key is password-protected"
      while (true)
        puts "\renter password for #{name}:"
        password = STDIN.noecho(&:gets).chomp
        STDOUT << ANSI.faint { "trying to decrypt..." }
        key = PrivateKey.from_packed(packed, password: password) rescue nil
        key and break key
      end
    elsif allow_public
      begin
        PublicKey.from_packed(packed)
      rescue
        error "can't load the private/public key (file corrupt?)"
      end
    else
      error "can't load the private key (file corrupt?)"
    end
  end
end

#output_file(extension = nil, overwrite_existing_name = nil) ⇒ Object



73
74
75
76
77
78
79
80
# File 'lib/universa/keytool/keytool.rb', line 73

def output_file(extension = nil, overwrite_existing_name = nil)
  name = @output_file
  if !name
    (overwrite_existing_name && @overwrite) or error "specify output file with -o / --output"
    name = overwrite_existing_name
  end
  extension && !name.end_with?(extension) ? "#{name}#{extension}" : name
end

#save_key(name, key) ⇒ Object



112
113
114
115
116
117
118
119
120
121
122
# File 'lib/universa/keytool/keytool.rb', line 112

def save_key name, key
  open(name, 'wb') { |f|
    f << if @require_password
           password = session_password
           puts "\nEncrypting key with #@rounds PBKDF rounds..."
           key.pack_with_password(password, @rounds)
         else
           key.pack()
         end
  }
end

#session_passwordObject



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/universa/keytool/keytool.rb', line 54

def session_password
  @require_password or return nil
  @session_password ||= begin
                          if @autogenerate_password
                            psw = 29.random_alnums
                            puts "Autogenerated password: #{ANSI.bold { psw }}"
                            psw
                          else
                            puts "\nPlease enter password for key to be generated"
                            psw1 = STDIN.noecho(&:gets).chomp
                            puts "Please re-enter the password"
                            psw2 = STDIN.noecho(&:gets).chomp
                            psw1 == psw2 or error "passwords do not match"
                            psw1.length < 8 and error "password is too short"
                            psw1
                          end
                        end
end

#task(&block) ⇒ Object



50
51
52
# File 'lib/universa/keytool/keytool.rb', line 50

def task &block
  @tasks << block
end