authentic 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 8a5638eac32ad5d3c4da52d99a8c25ca5c2bdca5
4
- data.tar.gz: ca509f3b357a2d5a8cdffa593c6721f45adb72cf
3
+ metadata.gz: c3ab2e1fa925121b4404ff3f34e23a0e7e4e8437
4
+ data.tar.gz: 63809f5dc1296be67f369b143a8d1d95f3489f6f
5
5
  SHA512:
6
- metadata.gz: 17d8c8a27bb04f8c36cfdd4adbbffcfe4d797ceb3ac4581fcbcca60a7157629024f6b72ecdd890aff20e25f7458a2b291da2ab818c5ae7d44d3d10c21aa365d0
7
- data.tar.gz: 29649b4fdb4fe73a9193bd8a14d0b17956a886a81db9275854c480212402c7550d1c608455da30134191e16b32c879ec6de2ef4db9807608d6b7490bbe49aa51
6
+ metadata.gz: ee92948d013b885d18f911acd694b6ba803386e2640695530aa17365e53c7f3fe39fb75956beefa53f0cb52cd294024262bb301f2d2d7f64b7c510a67caee6ef
7
+ data.tar.gz: 6dc8572b0913523d15f26a9c226bceb993dcadeec9e930470c0f385a17ca0f0de01efa36ecd6ce4496489f6cc4995b53e2a506efb6e6ccae4a3c7689c45f5507
@@ -5,6 +5,9 @@ require "keychain"
5
5
  require "rotp"
6
6
  require "colorize"
7
7
  require "ostruct"
8
+ require "json"
9
+ require "openssl"
10
+ require "base64"
8
11
 
9
12
  module Authentic
10
13
  class CLI < Thor
@@ -37,7 +40,7 @@ module Authentic
37
40
  desc "delete NAME", "Delete a TOTP key"
38
41
  option :force, default: false, type: :boolean, aliases: '-f'
39
42
  def delete(name)
40
- item = Keychain.generic_passwords.where(service: "authentic gem", account: name).first
43
+ item = Keychain.generic_passwords.where(label: "authentic gem", account: name).first
41
44
  unless item
42
45
  return say "\u2717".colorize(:red) + " Couldn't find service #{name.colorize(:red)}..."
43
46
  end
@@ -57,9 +60,70 @@ module Authentic
57
60
  28 => "🌑",
58
61
  }
59
62
 
63
+ desc "export", "Export TOTP secret keys"
64
+ option 'qr', default: false, type: :boolean, aliases: '-q'
65
+ def export
66
+ keys = Keychain
67
+ .generic_passwords
68
+ .where(label: "authentic gem")
69
+ .all.map do |key|
70
+ secret = key.password.gsub(/=*$/, '')
71
+ totp = ROTP::TOTP.new(secret)
72
+ OpenStruct.new(
73
+ secret: secret,
74
+ name: key.attributes[:account],
75
+ service: key.attributes[:service]
76
+ )
77
+ end.sort_by { |k| [k.service, k.name] }
78
+
79
+ if options['qr']
80
+ keys.each do |key|
81
+ puts "#{key.service} - #{key.name}\n"
82
+ puts `qrencode 'otpauth://totp/#{key.name}?issuer=#{key.service}&secret=#{key.secret}' -s 5 -o - | ~/.iterm2/imgcat`
83
+ end
84
+ else
85
+ data = keys.map(&:to_h).to_json
86
+
87
+ password = ask "Please enter a password for exported data:", echo: false
88
+ return if password.empty?
89
+
90
+ salt = OpenSSL::Random.random_bytes(32)
91
+ cipher = OpenSSL::Cipher::AES256.new :CBC
92
+ cipher.encrypt
93
+ cipher.key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(password, salt, 20000, 32)
94
+ cipher.iv = salt
95
+
96
+ cipher_text = cipher.update(data)
97
+ cipher_text << cipher.final
98
+
99
+ puts [salt, cipher_text].map { |part| Base64.strict_encode64(part) }.join(':')
100
+ end
101
+ end
102
+
103
+ desc "import DATA", "Import TOTP secret keys"
104
+ def import(data)
105
+ password = ask "Please enter a password for exported data:", echo: false
106
+ return if password.empty?
107
+
108
+ salt, cipher_text = data.split(':').map { |part| Base64.strict_decode64(part) }
109
+
110
+ cipher = OpenSSL::Cipher::AES256.new :CBC
111
+ cipher.decrypt
112
+ cipher.iv = salt
113
+ cipher.key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(password, salt, 20000, 32)
114
+
115
+ text = cipher.update(cipher_text)
116
+ text << cipher.final
117
+
118
+ keys = JSON.parse(text)
119
+
120
+ keys.each do |key|
121
+ add(key['name'], key['secret'], key['service'])
122
+ end
123
+ end
124
+
60
125
  desc "generate", "Generate TOTP codes"
61
126
  option 'skip-copy', default: false, type: :boolean, aliases: '-s'
62
- option 'qr-codes', default: false, type: :boolean, aliases: '-qr'
63
127
  def generate
64
128
  now = Time.now
65
129
  keys = Keychain
@@ -77,14 +141,6 @@ module Authentic
77
141
  )
78
142
  end.sort_by { |k| [k.service, k.name] }
79
143
 
80
- if options['qr-codes']
81
- keys.each do |key|
82
- puts "#{key.service} - #{key.name}\n"
83
- puts `qrencode 'otpauth://totp/#{key.name}?issuer=#{key.service}&secret=#{key.secret}' -s 5 -o - | ~/.iterm2/imgcat`
84
- end
85
- return
86
- end
87
-
88
144
  table = keys.each_with_index.map do |key, idx|
89
145
  number = (idx + 1).to_s.rjust(keys.size.to_s.size, ' ')
90
146
  service_prefix = "#{key.service} - " if key.service && key.service != key.name
@@ -98,7 +154,7 @@ module Authentic
98
154
 
99
155
  print_table(table.to_a)
100
156
 
101
- unless options['skip-copy']
157
+ unless options['skip-copy'] || keys.size == 0
102
158
  if keys.size > 1
103
159
  prompt = "\nWhich key should I copy?"
104
160
  prompt += " [1-#{keys.size}, leave empty to exit]"
@@ -1,3 +1,3 @@
1
1
  module Authentic
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: authentic
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ufuk Kayserilioglu
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-09-01 00:00:00.000000000 Z
11
+ date: 2017-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: colorize