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,106 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require "gettext"
4
+
5
+ module S7
6
+ # 機密情報を表現する。
7
+ class Entry
8
+ include GetText
9
+
10
+ # タグ名の配列。
11
+ attr_reader :tags
12
+
13
+ # 属性の配列。
14
+ attr_reader :attributes
15
+
16
+ def initialize
17
+ @attributes = []
18
+ attr = NumericAttribute.new("name" => N_("id"), "editabled" => false,
19
+ "protected" => true)
20
+ @attributes.push(attr)
21
+ attr = TextAttribute.new("name" => N_("name"), "protected" => true)
22
+ @attributes.push(attr)
23
+ attr = NumericAttribute.new("name" => N_("rate"), "protected" => true,
24
+ "value" => 1)
25
+ @attributes.push(attr)
26
+
27
+ @tags = []
28
+ end
29
+
30
+ # ID を取得する。
31
+ def id
32
+ return get_attribute("id").value
33
+ end
34
+
35
+ # ID を設定する。
36
+ def id=(val)
37
+ get_attribute("id").value = val
38
+ end
39
+
40
+ # name で指定した名前の属性を取得する。
41
+ # 存在しない場合、nil を返す。
42
+ def get_attribute(name)
43
+ return @attributes.find { |attr| attr.name == name }
44
+ end
45
+
46
+ # attributes で指定した属性を追加する。
47
+ # 名前が同じ属性が存在する場合、値を書き換える。
48
+ # 名前が同じ属性が存在し、なおかつ型が異なる場合、ApplicationError
49
+ # 例外を発生させる。
50
+ def add_attributes(*attributes)
51
+ [attributes].flatten.each do |attribute|
52
+ attr = @attributes.find { |a|
53
+ a.name == attribute.name
54
+ }
55
+ if attr
56
+ if attr.class == attribute.class
57
+ attr.value = attribute.value
58
+ else
59
+ raise ApplicationError, _("not same attribute type: name=<%s> exist=<%s> argument=<%s>") % [attr.name, attr.class, attribute.class]
60
+ end
61
+ else
62
+ @attributes.push(attribute)
63
+ end
64
+ end
65
+ end
66
+
67
+ # 全ての属性の複製を取得する。
68
+ def duplicate_attributes(exclude_uneditable = true)
69
+ if exclude_uneditable
70
+ attrs = @attributes
71
+ else
72
+ attrs = @attributes.select { |attr|
73
+ attr.editable?
74
+ }
75
+ end
76
+ return attrs.collect { |attr|
77
+ a = attr.dup
78
+ begin
79
+ a.value = attr.value.dup
80
+ rescue TypeError
81
+ end
82
+ a
83
+ }
84
+ end
85
+
86
+ def method_missing(symbol, *args)
87
+ attr_name = symbol.to_s
88
+ if attr_name[-1] == "="
89
+ attr_name = attr_name[0..-2]
90
+ setter = true
91
+ end
92
+ attr = @attributes.find { |a|
93
+ a.name == attr_name
94
+ }
95
+ if attr
96
+ if setter
97
+ attr.value = args.first
98
+ else
99
+ return attr.value
100
+ end
101
+ else
102
+ super
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,44 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require "s7/entry"
4
+
5
+ module S7
6
+ # 機密情報の集合を表現する。
7
+ class EntryCollection
8
+ # 機密情報の配列。
9
+ attr_reader :entries
10
+
11
+ def initialize
12
+ @entries = []
13
+ @max_id = 0
14
+ end
15
+
16
+ # 機密情報を追加する。
17
+ def add_entries(*entries)
18
+ [entries].flatten.each do |entry|
19
+ @max_id += 1
20
+ entry.id = @max_id
21
+ @entries.push(entry)
22
+ end
23
+ end
24
+
25
+ # 機密情報を削除し、削除した機密情報を返す。
26
+ def delete_entries(*entries)
27
+ entries = [entries].flatten
28
+ return @entries.delete_if { |entry|
29
+ entries.include?(entry)
30
+ }
31
+ end
32
+
33
+ # other で指定した EntryCollection オブジェクトの entries をマージ
34
+ # する。
35
+ def merge(other)
36
+ add_entries(other.entries)
37
+ end
38
+
39
+ # 指定した id にマッチする Entry オブジェクトを entries から探す。
40
+ def find(id)
41
+ return @entries.find { |entry| entry.id == id }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,10 @@
1
+ module S7
2
+ # 機密情報の新規作成時に利用するテンプレートを表現する。
3
+ class EntryTemplate
4
+ # 名前。
5
+ attr_accessor :name
6
+
7
+ # 属性の配列。
8
+ attr_accessor :attributes
9
+ end
10
+ end
@@ -0,0 +1,116 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require "gettext"
4
+
5
+ module S7
6
+ # s7で定義する例外クラスの親クラス。
7
+ class ApplicationError < StandardError
8
+ include GetText
9
+ end
10
+
11
+ # 不正なパスフレーズを表現する例外クラス。
12
+ class InvalidPassphrase < ApplicationError
13
+ def to_s
14
+ return _("Invlaid passphrase.")
15
+ end
16
+ end
17
+
18
+ # 不正なパスを表現する例外クラス。
19
+ class InvalidPath < ApplicationError
20
+ def initialize(path)
21
+ @path = path
22
+ end
23
+
24
+ def to_s
25
+ return _("Invalid path: path=<%s>") % @path
26
+ end
27
+ end
28
+
29
+ # ファイルやディレクトリが存在しないことを表現する例外クラス。
30
+ class NotExist < ApplicationError
31
+ def initialize(path)
32
+ @path = path
33
+ end
34
+
35
+ def to_s
36
+ return _("Does not exit: path=<%s>") % @path
37
+ end
38
+ end
39
+
40
+ # 機密情報が存在しないことを表現する例外クラス。
41
+ class NoSuchEntry < ApplicationError
42
+ def initialize(options = {})
43
+ @options = options
44
+ end
45
+
46
+ def to_s
47
+ if !@options.empty?
48
+ s = ""
49
+ @options.each do |key, value|
50
+ s << " #{key}=<#{value}>"
51
+ end
52
+ end
53
+ if s.empty?
54
+ return _("No such entry.")
55
+ else
56
+ return _("No such entry:%s") % s
57
+ end
58
+ end
59
+ end
60
+
61
+ # 機密情報の属性が存在しないことを表現する例外クラス。
62
+ class NoSuchAttribute < ApplicationError
63
+ def initialize(options = {})
64
+ @options = options
65
+ end
66
+
67
+ def to_s
68
+ if !@options.empty?
69
+ s = ""
70
+ @options.each do |key, value|
71
+ s << " #{key}=<#{value}>"
72
+ end
73
+ end
74
+ if s.empty?
75
+ return _("No such attribute.")
76
+ else
77
+ return _("No such attribute:%s") % s
78
+ end
79
+ end
80
+ end
81
+
82
+ # データ長が不正であることを表現する例外クラス。
83
+ class DataLengthError < ApplicationError
84
+ def initialize(actual, needed)
85
+ @actual = actual
86
+ @needed = needed
87
+ end
88
+
89
+ def to_s
90
+ return _("Invalid data length: actual=<%s> needed=<%s>") % [@actual, @needed]
91
+ end
92
+ end
93
+
94
+ # キャンセルを表現する例外クラス。
95
+ class Canceled < ApplicationError
96
+ MESSAGE = _("Canceled.")
97
+
98
+ def to_s
99
+ return MESSAGE
100
+ end
101
+ end
102
+
103
+ # OS のコマンドの実行に失敗したことを表現する例外クラス。
104
+ class CommandFailed < ApplicationError
105
+ # command:: 失敗したコマンド
106
+ # status:: コマンドの終了ステータス
107
+ def initialize(command, status)
108
+ @command = command
109
+ @status = status
110
+ end
111
+
112
+ def to_s
113
+ return _("Failed the command: command=<%s> status=<%d>") % [@command, @status]
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,77 @@
1
+ require "s7/exception"
2
+
3
+ module S7
4
+ class File
5
+ # パスワードコレクションのファイルのバージョン。
6
+ VERSION = "1.1.0"
7
+
8
+ # パスワードコレクションの先頭記述するマジック文字列。
9
+ MAGIC = "GPassFile version #{VERSION}"
10
+
11
+ # Cipherで使用するIV(Initial Vector)
12
+ IV = [5, 23, 1, 123, 12, 3, 54, 94].pack("C8")
13
+
14
+ # パスワードコレクションを格納するファイルを作成する。
15
+ # +path+:: 作成するファイルのパスを文字列で指定する。
16
+ # +passphrase+:: ファイルを暗号化するマスターパスフレーズを
17
+ # 文字列で指定する。
18
+ #
19
+ # すでにファイルが存在した場合、例外を発生させる。
20
+ def self.create(path, passphrase)
21
+ path = ::File.expand_path(path)
22
+ if ::File.exist?(path)
23
+ raise S7::Exception, "already exist file or directory: #{path}"
24
+ end
25
+ ::File.open(path, "w") do |f|
26
+ key = S7::Key.create_instance("SHA-1", passphrase)
27
+ cipher =
28
+ S7::Cipher.create_instance("BF-CBC", :key => key, :iv => S7::File::IV)
29
+ magic = cipher.encrypt(StringIO.new(S7::File::MAGIC))
30
+ f.write(magic)
31
+ end
32
+ end
33
+
34
+ def self.open(path, passphrase)
35
+ path = ::File.expand_path(path)
36
+ if !::File.exist?(path)
37
+ raise S7::Exception, "no such file or directory: #{path}"
38
+ end
39
+ if !block_given?
40
+ raise ArgumentError, "no block given"
41
+ end
42
+ begin
43
+ f = self.new(path, passphrase)
44
+ yield(f)
45
+ ensure
46
+ f.close
47
+ end
48
+ end
49
+
50
+ # パスワードコレクションのパス。
51
+ attr_accessor :path
52
+
53
+ # パスフレーズから生成したCipher用のキー。
54
+ attr_accessor :key
55
+
56
+ # キーとIVから生成したCipher。
57
+ attr_accessor :cipher
58
+
59
+ # 操作対象のファイル。
60
+ attr_accessor :file
61
+
62
+ def initialize(path, passphrase)
63
+ self.path = ::File.expand_path(path)
64
+ self.key = S7::Key.create_instance("SHA-1", passphrase)
65
+ self.cipher = S7::Cipher.create_instance("BF-CBC", :key => key,
66
+ :iv => S7::File::IV)
67
+ self.file = nil
68
+ end
69
+
70
+ # ファイルを閉じる。
71
+ def close
72
+ if self.file
73
+ self.file.close
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,202 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ require "s7/key"
4
+ require "s7/cipher"
5
+ require "s7/entry_collection"
6
+ require "s7/attribute"
7
+ require "gettext"
8
+
9
+ module S7
10
+ # GPass のファイルに関する処理を表現する。
11
+ class GPassFile
12
+ include GetText
13
+
14
+ # GPass のファイルが格納されているデフォルトのパス。
15
+ DEFAULT_PATH = "~/.gpass/passwords.gps"
16
+
17
+ # Blowfish の初期化配列。
18
+ IV = [5, 23, 1, 123, 12, 3, 54, 94].pack("C8")
19
+
20
+ # GPass のファイルであることを示すマジック。
21
+ MAGIC_PATTERN = /\AGPassFile version (\d\.\d\.\d)/
22
+
23
+ # GPass で扱う秘密情報の種類と属性。
24
+ ENTRY_ATTRIBUTE_TEMPLATES = {
25
+ "entry" => [TextAttribute.new("name" => N_("name")),
26
+ TextAttribute.new("name" => N_("description")),
27
+ DateTimeAttribute.new("name" => N_("created_at")),
28
+ DateTimeAttribute.new("name" => N_("updated_at")),
29
+ BooleanAttribute.new("name" => N_("expiration")),
30
+ DateTimeAttribute.new("name" => N_("expire_at"))],
31
+ "folder" => ["entry"],
32
+ "password" => ["entry",
33
+ TextAttribute.new("name" => N_("username")),
34
+ TextAttribute.new("name" => N_("password"),
35
+ "secret" => true)],
36
+ "general" => ["password",
37
+ TextAttribute.new("name" => N_("hostname"))],
38
+ "shell" => ["general"],
39
+ "website" => ["password",
40
+ TextAttribute.new("name" => N_("url"))],
41
+ }
42
+
43
+ ENTRY_ATTRIBUTE_TEMPLATES.each do |name, template|
44
+ loop do
45
+ replaced = false
46
+ template.each_with_index do |attr, i|
47
+ if attr.is_a?(String)
48
+ template[i] = ENTRY_ATTRIBUTE_TEMPLATES[attr]
49
+ replaced = true
50
+ end
51
+ end
52
+ if replaced
53
+ template.flatten!
54
+ else
55
+ break
56
+ end
57
+ end
58
+ end
59
+
60
+ def read(passphrase, path = DEFAULT_PATH)
61
+ res = EntryCollection.new
62
+ File.open(File.expand_path(path), "r") do |f|
63
+ f.flock(File::LOCK_EX)
64
+ s = decrypt(f, passphrase)
65
+ parse(s, res)
66
+ end
67
+ return res
68
+ end
69
+
70
+ private
71
+
72
+ def decrypt(io, passphrase)
73
+ key = Key.create_instance("SHA-1", passphrase)
74
+ cipher = Cipher.create_instance("BF-CBC", :key => key, :iv => IV)
75
+ return cipher.decrypt(io)
76
+ end
77
+
78
+ def parse(s, entry_collection)
79
+ if s.slice!(MAGIC_PATTERN).nil?
80
+ raise InvalidPassphrase
81
+ end
82
+ version = $1
83
+ # TODO: 1.1.0 より前のバージョンもサポートする。
84
+ if version != "1.1.0"
85
+ raise GPassFileError, _("not supported version: %s") % version
86
+ end
87
+ id_entry = {}
88
+ while !s.empty?
89
+ res = parse_gpass_entry(s)
90
+ id = res["id"]
91
+ entry = res["entry"]
92
+ if id_entry.key?(id)
93
+ raise GPassFileError, _("duplicated entry id: %s") % id
94
+ end
95
+ id_entry[id] = entry
96
+ parent = id_entry[res["parent_id"]]
97
+ if parent
98
+ entry.tags.push(*parent.tags)
99
+ end
100
+ if res["type"] == "folder"
101
+ entry.tags.push(entry.name)
102
+ else
103
+ entry_collection.add_entries(entry)
104
+ end
105
+ end
106
+ end
107
+
108
+ def parse_gpass_entry(s)
109
+ id = unpack_number(s)
110
+ parent_id = unpack_number(s)
111
+ type = unpack_string(s, unpack_number(s))
112
+ bytes = unpack_number(s)
113
+ if s.length < bytes
114
+ raise DataLengthError.new(s.length, bytes)
115
+ end
116
+ entry = Entry.new
117
+ unpack_attributes(entry, type, s.slice!(0, bytes))
118
+ return {
119
+ "id" => id,
120
+ "parent_id" => parent_id,
121
+ "type" => type,
122
+ "entry" => entry,
123
+ }
124
+ end
125
+
126
+ def unpack_attributes(entry, type, s)
127
+ template = ENTRY_ATTRIBUTE_TEMPLATES[type]
128
+ if !template
129
+ raise GPassFileError, _("invalid entry type: type=<%s>") % type
130
+ end
131
+ template.each do |a|
132
+ attr = a.dup
133
+ case attr
134
+ when NumericAttribute
135
+ attr.value = unpack_variable_number(s)
136
+ when BooleanAttribute
137
+ attr.value = unpack_boolean(s)
138
+ when DateTimeAttribute
139
+ attr.value = unpack_time(s)
140
+ when TextAttribute
141
+ attr.value = unpack_string(s, unpack_variable_number(s))
142
+ end
143
+ entry.add_attributes(attr)
144
+ end
145
+ end
146
+
147
+ # s で指定したバイト列から 4 バイト取り出し、リトルエンディアンの数
148
+ # 値として unpack した結果を返す。
149
+ def unpack_number(s)
150
+ if s.length < 4
151
+ raise DataLengthError.new(s.length, 4)
152
+ end
153
+ return s.slice!(0, 4).unpack("V").first
154
+ end
155
+
156
+ # s で指定したバイト列から 8 ビット目が 0 のバイトを見つけるまで 1
157
+ # バイトずつ取り出し、4 バイトのリトルエンディアンの数値として
158
+ # unpack した結果を返す。
159
+ def unpack_variable_number(s)
160
+ n = 0
161
+ base = 1
162
+ i = 0
163
+ while (t = s.slice!(0)) && i <= 5
164
+ t = t.ord
165
+ if t & 0x80 == 0
166
+ n += base * t
167
+ break
168
+ else
169
+ n += base * (t & 0x7f)
170
+ base *= 0x80
171
+ end
172
+ i += 1
173
+ end
174
+ return [n].pack("V").unpack("I").first
175
+ end
176
+
177
+ # s で指定したバイト列から n で指定した長さを取り出し、それを文字列
178
+ # として返す。
179
+ def unpack_string(s, n)
180
+ if s.length < n
181
+ raise DataLengthError.new(s.length, n)
182
+ end
183
+ return s.slice!(0, n).force_encoding("utf-8")
184
+ end
185
+
186
+ # s で指定したバイト列から unpack_variable_number を使用して数値を
187
+ # 取り出す。それを time_t の値として扱い、DateTimeオブジェクトを返す。
188
+ def unpack_time(s)
189
+ return Time.at(unpack_variable_number(s)).to_datetime
190
+ end
191
+
192
+ # s で指定したバイト列から unpack_variable_number を使用して数値を
193
+ # 取り出す。それが 0 であれば false を、そうでなければ true を返す。
194
+ def unpack_boolean(s)
195
+ return unpack_variable_number(s) != 0
196
+ end
197
+
198
+ # GPass のファイルの処理中に発生したエラーを表現する例外クラス。
199
+ class GPassFileError < ApplicationError
200
+ end
201
+ end
202
+ end