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