s7 0.0.1

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.
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