s7 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/ChangeLog +311 -0
  2. data/LICENCE +24 -0
  3. data/README +56 -0
  4. data/Rakefile +128 -0
  5. data/VERSION +1 -0
  6. data/bin/s7cli +27 -0
  7. data/lib/gettext.rb +28 -0
  8. data/lib/s7.rb +12 -0
  9. data/lib/s7/action.rb +7 -0
  10. data/lib/s7/attribute.rb +226 -0
  11. data/lib/s7/cipher.rb +110 -0
  12. data/lib/s7/configuration.rb +41 -0
  13. data/lib/s7/entry.rb +106 -0
  14. data/lib/s7/entry_collection.rb +44 -0
  15. data/lib/s7/entry_template.rb +10 -0
  16. data/lib/s7/exception.rb +116 -0
  17. data/lib/s7/file.rb +77 -0
  18. data/lib/s7/gpass_file.rb +202 -0
  19. data/lib/s7/key.rb +83 -0
  20. data/lib/s7/message_catalog.rb +5 -0
  21. data/lib/s7/s7_file.rb +47 -0
  22. data/lib/s7/s7cli.rb +213 -0
  23. data/lib/s7/s7cli/attribute_command.rb +69 -0
  24. data/lib/s7/s7cli/command.rb +210 -0
  25. data/lib/s7/s7cli/entry_collection_command.rb +63 -0
  26. data/lib/s7/s7cli/entry_command.rb +728 -0
  27. data/lib/s7/s7cli/option.rb +101 -0
  28. data/lib/s7/secret_generator.rb +30 -0
  29. data/lib/s7/undo_stack.rb +7 -0
  30. data/lib/s7/unicode_data.rb +29 -0
  31. data/lib/s7/utils.rb +37 -0
  32. data/lib/s7/world.rb +150 -0
  33. data/setup.rb +1585 -0
  34. data/test/s7/attribute_test.rb +45 -0
  35. data/test/s7/gpass_file_test.rb +169 -0
  36. data/test/s7/gpass_file_test/passwords.gps.empty +1 -0
  37. data/test/s7/gpass_file_test/passwords.gps.one_entry +2 -0
  38. data/test/s7/gpass_file_test/passwords.gps.three_entries +3 -0
  39. data/test/s7/gpass_file_test/passwords.gps.with_folders +0 -0
  40. data/test/s7/secret_generator_test.rb +29 -0
  41. data/test/s7/unicode_data_test.rb +28 -0
  42. data/test/s7/world_test.rb +35 -0
  43. data/test/test_helper.rb +19 -0
  44. metadata +108 -0
@@ -0,0 +1,83 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require "openssl"
4
+
5
+ module S7
6
+ # 暗号化用のキーを表現する。
7
+ class Key
8
+ @@key_classes = {}
9
+
10
+ class << self
11
+ def create_instance(type, passphrase)
12
+ return @@key_classes[type].new(passphrase)
13
+ end
14
+
15
+ private
16
+
17
+ def key_type(name)
18
+ if @@key_classes.nil?
19
+ @@key_classes = {}
20
+ end
21
+ @@key_classes[name] = self
22
+ self.const_set("KEY_TYPE", name)
23
+ end
24
+ end
25
+
26
+ attr_reader :key
27
+ attr_reader :length
28
+
29
+ def initialize(passphrase)
30
+ @key = OpenSSL::Digest.digest(hash_type, passphrase)
31
+ @length = @key.length
32
+ end
33
+
34
+ private
35
+
36
+ def hash_type
37
+ begin
38
+ return self.class.const_get("HASH_TYPE")
39
+ rescue NameError
40
+ return self.class.const_get("KEY_TYPE")
41
+ end
42
+ end
43
+ end
44
+
45
+ class Md5Key < Key
46
+ key_type "MD5"
47
+ end
48
+
49
+ class Sha1Key < Key
50
+ key_type "SHA-1"
51
+ HASH_TYPE = "SHA1"
52
+ end
53
+
54
+ begin
55
+ OpenSSL::Digest.const_get("SHA224")
56
+
57
+ class Sha224Key < Key
58
+ key_type "SHA-224"
59
+ HASH_TYPE = "SHA224"
60
+ end
61
+ rescue NameError
62
+ end
63
+
64
+ begin
65
+ OpenSSL::Digest.const_get("SHA256")
66
+
67
+ class Sha256Key < Key
68
+ key_type "SHA-256"
69
+ HASH_TYPE = "SHA256"
70
+ end
71
+ rescue NameError
72
+ end
73
+
74
+ begin
75
+ OpenSSL::Digest.const_get("SHA512")
76
+
77
+ class Sha512Key < Key
78
+ key_type "SHA-512"
79
+ HASH_TYPE = "SHA512"
80
+ end
81
+ rescue NameError
82
+ end
83
+ end
@@ -0,0 +1,5 @@
1
+ module S7
2
+ # 属性の名前を国際化するときに使用するメッセージカタログを表現する。
3
+ class MessageCatalog
4
+ end
5
+ end
@@ -0,0 +1,47 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require "fileutils"
4
+ require "stringio"
5
+ require "s7/utils"
6
+ require "s7/cipher"
7
+
8
+ module S7
9
+ # s7 オリジナルのファイル形式で機密情報の読み書きを表現する。
10
+ module S7File
11
+ # マジック。
12
+ MAGIC = "s7 file version 0\n"
13
+
14
+ # data で指定したバイト列を暗号化して書き込む。
15
+ def write(path, master_key, cipher_type, data)
16
+ cipher = Cipher.create_instance(cipher_type, :passphrase => master_key)
17
+ FileUtils.touch(path)
18
+ File.open(path, "r+b") do |f|
19
+ f.flock(File::LOCK_EX)
20
+ data = [MAGIC,
21
+ cipher_type + "\n",
22
+ cipher.encrypt(StringIO.new(data))].join
23
+ f.write(data)
24
+ f.truncate(f.pos)
25
+ end
26
+ FileUtils.chmod(0600, path)
27
+ end
28
+ module_function :write
29
+
30
+ # path で指定したファイルを読み込み、master_key で指定したマスター
31
+ # キーを使用して復号する。
32
+ def read(path, master_key)
33
+ File.open(path, "rb") do |f|
34
+ f.flock(File::LOCK_EX)
35
+ magic = f.gets
36
+ if magic != MAGIC
37
+ raise InvalidPassphrase
38
+ end
39
+ cipher_type = f.gets.chomp
40
+ cipher = Cipher.create_instance(cipher_type,
41
+ :passphrase => master_key)
42
+ return cipher.decrypt(f)
43
+ end
44
+ end
45
+ module_function :read
46
+ end
47
+ end
@@ -0,0 +1,213 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require "optparse"
4
+ require "readline"
5
+ require "gettext"
6
+ require "fileutils"
7
+ require "s7/s7cli/option"
8
+ require "s7/s7cli/command"
9
+ command_pattern =
10
+ File.expand_path("s7cli/*_command.rb", File.dirname(__FILE__))
11
+ Dir.glob(command_pattern).each do |path|
12
+ require path.gsub(/\.rb\z/, "")
13
+ end
14
+
15
+ # s7 のコマンドラインインタフェースを表現する。
16
+ module S7
17
+ class S7Cli
18
+ include GetText
19
+
20
+ # 最初に実行するメソッド。
21
+ def self.run(argv)
22
+ obj = S7Cli.new
23
+ obj.parse_command_line_argument(argv)
24
+ obj.run
25
+ end
26
+
27
+ # prompt に指定した文字列を表示後、Readline ライブラリを利用してユー
28
+ # ザからの入力を得る。
29
+ def self.input_string(prompt = "", add_history = true)
30
+ input = Readline.readline(prompt, add_history)
31
+ input.force_encoding("utf-8") if input
32
+ return input
33
+ end
34
+
35
+ # prompt に指定した文字列を表示後、エコーバックせずにユーザからの入
36
+ # 力を得る。パスフレーズの入力を想定。
37
+ def self.input_string_without_echo(prompt = "")
38
+ print(prompt) if prompt.length > 0
39
+ STDOUT.flush
40
+ Utils.execute("stty -echo")
41
+ begin
42
+ input = STDIN.gets
43
+ ensure
44
+ Utils.execute("stty echo")
45
+ puts("")
46
+ end
47
+ if input
48
+ input.chomp!
49
+ input.force_encoding("utf-8")
50
+ end
51
+ return input
52
+ end
53
+
54
+ # prompt に指定した文字列を表示後、ユーザからの入力を得る。
55
+ # それがyes、y、true、t、okのいずれかであれば true を返し、
56
+ # そうでなければ false を返す。大小文字は区別しない。
57
+ # ^D などにより何も入力しない場合、defualt で指定した真偽値を返す。
58
+ def self.input_boolean(prompt = "", default = false)
59
+ if input = Readline.readline(prompt, false)
60
+ input.strip!
61
+ if input.empty?
62
+ return default
63
+ end
64
+ input.force_encoding("utf-8")
65
+ if /\Ay|yes|t|true|ok\z/i =~ input
66
+ return true
67
+ else
68
+ return false
69
+ end
70
+ else
71
+ return default
72
+ end
73
+ end
74
+
75
+ # World クラスのインスタンス。
76
+ attr_accessor :world
77
+
78
+ def initialize
79
+ @world = World.new
80
+ end
81
+
82
+ # コマンドラインオプションを解析する。
83
+ def parse_command_line_argument(argv)
84
+ option_parser = OptionParser.new { |opts|
85
+ opts.banner = _("usage: s7cli [options]")
86
+ opts.separator("")
87
+ opts.separator(_("options:"))
88
+ opts.on("-d", "--base-dir=DIR",
89
+ _("Specify the base directory. Default is '%s'.") % world.base_dir) do |dir|
90
+ world.base_dir = dir
91
+ end
92
+ opts.on("-v", "--version", _("Show version.")) do
93
+ puts(_("s7cli, version %s") % S7::VERSION)
94
+ exit(0)
95
+ end
96
+ opts.on("-h", "--help",
97
+ _("Show help and exit.")) do
98
+ puts(opts)
99
+ exit(0)
100
+ end
101
+ }
102
+ option_parser.parse(argv)
103
+ end
104
+
105
+ # メインの処理を行う。
106
+ def run
107
+ if File.exist?(world.base_dir)
108
+ if !File.directory?(world.base_dir)
109
+ Utils::shred_string(master_key)
110
+ raise InvalidPath.new(world.base_dir)
111
+ end
112
+ world.start_logging
113
+ begin
114
+ master_key =
115
+ S7Cli.input_string_without_echo(_("Enter the master key for s7: "))
116
+ if master_key.nil?
117
+ exit(1)
118
+ end
119
+ world.load(S7File.read(world.secrets_path, master_key))
120
+ world.master_key = master_key
121
+ rescue InvalidPassphrase => e
122
+ puts(e.message)
123
+ retry
124
+ end
125
+ else
126
+ master_key =
127
+ S7Cli.input_string_without_echo(_("Enter the master key for s7: "))
128
+ if master_key.nil?
129
+ exit(1)
130
+ end
131
+ confirmation =
132
+ S7Cli.input_string_without_echo(_("Comfirm the master key for s7: "))
133
+ if master_key != confirmation
134
+ Utils::shred_string(master_key)
135
+ Utils::shred_string(confirmation) if !confirmation.nil?
136
+ raise InvalidPassphrase
137
+ end
138
+ world.master_key = master_key
139
+ FileUtils.mkdir_p(world.base_dir)
140
+ FileUtils.chmod(0700, world.base_dir)
141
+ world.start_logging
142
+ world.logger.info(_("created base directory: %s") % world.base_dir)
143
+ world.save(true)
144
+ end
145
+ world.lock
146
+ begin
147
+ path = File.join(world.base_dir, "s7cli")
148
+ if File.file?(path)
149
+ begin
150
+ hash = YAML.load(S7File.read(path, world.master_key))
151
+ hash["history"].each_line do |line|
152
+ Readline::HISTORY.push(line.chomp)
153
+ end
154
+ rescue
155
+ puts(_("could not read: %s") % path)
156
+ raise
157
+ end
158
+ end
159
+ loop do
160
+ begin
161
+ prompt = "%ss7> " % (world.changed ? "*" : "")
162
+ input = S7Cli.input_string(prompt, true)
163
+ if !input
164
+ puts("")
165
+ break
166
+ end
167
+ name, argv = *Command.split_name_and_argv(input)
168
+ if !name
169
+ next
170
+ end
171
+ command = Command.create_instance(name, world)
172
+ if command
173
+ if !command.run(argv)
174
+ break
175
+ end
176
+ else
177
+ raise ApplicationError, _("Unknown command: %s") % name
178
+ end
179
+ rescue Interrupt
180
+ puts("")
181
+ retry
182
+ rescue ApplicationError
183
+ puts($!)
184
+ rescue => e
185
+ puts(e.message)
186
+ puts("----- back trace -----")
187
+ e.backtrace.each do |line|
188
+ puts(" #{line}")
189
+ end
190
+ end
191
+ end
192
+ ensure
193
+ begin
194
+ world.save
195
+ begin
196
+ path = File.join(world.base_dir, "s7cli")
197
+ # TODO: history の上限を設ける。
198
+ hash = {
199
+ "history" => Readline::HISTORY.to_a.join("\n"),
200
+ }
201
+ S7File.write(path, world.master_key,
202
+ world.configuration.cipher_type, hash.to_yaml)
203
+ rescue
204
+ puts(_("could not write: %s") % path)
205
+ puts($!)
206
+ end
207
+ ensure
208
+ world.unlock
209
+ end
210
+ end
211
+ end
212
+ end
213
+ end
@@ -0,0 +1,69 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require "s7/s7cli/command"
4
+ require "open3"
5
+
6
+ module S7
7
+ class S7Cli
8
+ # 機密情報の属性のうち、指定したものの値をクリップボードにコピーするコ
9
+ # マンドを表現する。
10
+ class AttributeCopyCommand < Command
11
+ command("copy",
12
+ ["attribute:copy",
13
+ "c"],
14
+ _("Copy the attribute value to the clipboard."),
15
+ _("usage: copy [ID] [ATTRIBUTE NAME] [OPTIONS]"))
16
+
17
+ def initialize(*args)
18
+ super
19
+ @config["copy_command"] = "/usr/bin/pbcopy"
20
+ @config["inclement_rate"] = true
21
+ @options.push(IntOption.new("i", "id", "ID", _("Entry ID.")),
22
+ StringOption.new("n", "name", _("Attribute name.")),
23
+ BoolOption.new("inclement_rate",
24
+ _("Inclement the entry's rate after copied.")),
25
+ StringOption.new("copy_command", _("Copy command.")))
26
+ end
27
+
28
+ # コマンドを実行する。
29
+ def run(argv)
30
+ option_parser.parse!(argv)
31
+ if argv.length > 0
32
+ config["id"] = argv.shift.to_i
33
+ end
34
+ if argv.length > 0
35
+ config["name"] = argv.shift
36
+ end
37
+ if config["id"].nil? || config["name"].nil?
38
+ puts(_("Too few arguments."))
39
+ help
40
+ return true
41
+ end
42
+ puts(_("Copy the attribute value to the clipboard."))
43
+ entry = world.entry_collection.find(config["id"])
44
+ if entry.nil?
45
+ raise NoSuchEntry.new("id" => config["id"])
46
+ end
47
+ attr = entry.get_attribute(config["name"])
48
+ if attr.nil?
49
+ raise NoSuchAttribute.new("id" => config["id"],
50
+ "name" => config["name"])
51
+ end
52
+ copy_command = @config["copy_command"]
53
+ res, status = Open3.capture2(copy_command,
54
+ stdin_data: attr.value.to_s,
55
+ binmode: true)
56
+ if status != 0
57
+ raise CommandFailed.new(copy_command, status)
58
+ end
59
+ if config["inclement_rate"]
60
+ # TODO: undo スタックに操作を追加する。
61
+ entry.rate += 1
62
+ puts(_("Changed rate to %d.") % entry.rate)
63
+ world.changed = true
64
+ end
65
+ return true
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,210 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require "gettext"
4
+
5
+ module S7
6
+ class S7Cli
7
+ # コマンドを表現する。
8
+ class Command
9
+ include GetText
10
+
11
+ # オプションの配列。
12
+ attr_reader :options
13
+
14
+ # 設定。
15
+ attr_reader :config
16
+
17
+ # World オブジェクト。
18
+ attr_reader :world
19
+
20
+ @@command_classes = {}
21
+
22
+ class << self
23
+ # s で指定した文字列をコマンド名とオプションに分割する。
24
+ def split_name_and_argv(s)
25
+ s = s.strip
26
+ name = s.slice!(/\A\w+/)
27
+ argv = s.split
28
+ return name, argv
29
+ end
30
+
31
+ def create_instance(name, world)
32
+ klass = @@command_classes[name]
33
+ if klass
34
+ return klass.new(world)
35
+ else
36
+ return nil
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def command(name, aliases, description, usage)
43
+ @@command_classes[name] = self
44
+ self.const_set("NAME", name)
45
+ aliases.each do |s|
46
+ @@command_classes[s] = self
47
+ end
48
+ self.const_set("ALIASES", aliases)
49
+ self.const_set("DESCRIPTION", description)
50
+ self.const_set("USAGE", usage)
51
+ end
52
+ end
53
+
54
+ def initialize(world)
55
+ @world = world
56
+ @config = {}
57
+ @options = []
58
+ end
59
+
60
+ # コマンド名を取得する。
61
+ def name
62
+ return self.class.const_get("NAME")
63
+ end
64
+
65
+ # 別名の配列を取得する。
66
+ def aliases
67
+ return self.class.const_get("ALIASES")
68
+ end
69
+
70
+ # 概要を取得する。
71
+ def description
72
+ return self.class.const_get("DESCRIPTION")
73
+ end
74
+
75
+ # 使用例を取得する。
76
+ def usage
77
+ return self.class.const_get("USAGE")
78
+ end
79
+
80
+ # コマンドラインオプションを解析するための OptionParser オブジェ
81
+ # クトを取得する。
82
+ def option_parser
83
+ return OptionParser.new { |opts|
84
+ opts.banner = [
85
+ "#{name} (#{aliases.join(",")}): #{description}",
86
+ usage,
87
+ ].join("\n")
88
+ if options && options.length > 0
89
+ opts.separator("")
90
+ opts.separator("Valid options:")
91
+ for option in options
92
+ option.define(opts, config)
93
+ end
94
+ end
95
+ }
96
+ end
97
+
98
+ # 使用方法を示す文字列を返す。
99
+ def help
100
+ puts(option_parser)
101
+ end
102
+
103
+ private
104
+
105
+ # キャンセルした旨を出力し、true を返す。
106
+ # Command オブジェクトの run メソッドで return canceled という使
107
+ # い方を想定している。
108
+ def canceled
109
+ puts("\n" + Canceled::MESSAGE)
110
+ return true
111
+ end
112
+ end
113
+
114
+ # プログラムを終了するコマンドを表現する。
115
+ class HelpCommand < Command
116
+ command("help",
117
+ ["usage", "h"],
118
+ _("Describe the commands."),
119
+ _("usage: help [COMMAND]"))
120
+
121
+ def run(argv)
122
+ if argv.length > 0
123
+ command_name = argv.shift
124
+ command = Command.create_instance(command_name, world)
125
+ if command
126
+ command.help
127
+ else
128
+ puts(_("Unknown command: %s") % command_name)
129
+ end
130
+ else
131
+ puts(_("Available commands:"))
132
+ command_classes = @@command_classes.values.uniq.sort_by { |klass|
133
+ klass.const_get("NAME")
134
+ }
135
+ command_classes.each do |klass|
136
+ s = " #{klass.const_get("NAME")}"
137
+ aliases = klass.const_get("ALIASES")
138
+ if aliases.length > 0
139
+ s << " (#{aliases.join(", ")})"
140
+ end
141
+ puts(s)
142
+ end
143
+ end
144
+ return true
145
+ end
146
+ end
147
+
148
+ # 現在の状態を保存するコマンドを表現する。
149
+ class SaveCommand < Command
150
+ command("save", [], _("Save."), _("usage: save"))
151
+
152
+ def initialize(*args)
153
+ super
154
+ @options.push(BoolOption.new("f", "force", _("Force.")))
155
+ end
156
+
157
+ def run(argv)
158
+ option_parser.parse!(argv)
159
+ world.save(config["force"])
160
+ puts(_("Saved."))
161
+ return true
162
+ end
163
+ end
164
+
165
+ # スクリーンロックし、マスターキーを入力するまでは操作できないよう
166
+ # にするコマンドを表現する。
167
+ class LockCommand < Command
168
+ command("lock", [], _("Lock screen after saved."), _("usage: lock"))
169
+
170
+ def run(argv)
171
+ system("clear")
172
+ puts(_("Locked screen."))
173
+ world.save
174
+ prompt = _("Enter the master key for s7: ")
175
+ begin
176
+ input = S7Cli.input_string_without_echo(prompt)
177
+ if input != world.master_key
178
+ raise InvalidPassphrase
179
+ end
180
+ puts(_("Unlocked."))
181
+ return true
182
+ rescue Interrupt
183
+ rescue ApplicationError => e
184
+ puts($!.message + _("Try again."))
185
+ retry
186
+ rescue Exception => e
187
+ puts(e.message)
188
+ puts("----- back trace -----")
189
+ e.backtrace.each do |line|
190
+ puts(" #{line}")
191
+ end
192
+ end
193
+ puts(_("Quit."))
194
+ return false
195
+ end
196
+ end
197
+
198
+ # プログラムを終了するコマンドを表現する。
199
+ class QuitCommand < Command
200
+ command("quit",
201
+ ["exit", "q"],
202
+ _("Quit."),
203
+ _("usage: quit"))
204
+
205
+ def run(argv)
206
+ return false
207
+ end
208
+ end
209
+ end
210
+ end