firefox-data 1.0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 69f8af3ca8b398607350277c4525297d73cf77c0
4
+ data.tar.gz: 68217687204f43a15abc0d1db16404c0b820fecf
5
+ SHA512:
6
+ metadata.gz: b4796e5e185a385c37052bb3d6f07c312c44c7f73029d8fd8e93dee677a44ffdd3df6800c3f2da5b870a5a5cf47d40f1db559ac13d2d070a0867dca72c8cc5ea
7
+ data.tar.gz: 7ca0eff0b21d59cb366d4f228bd6fec66d3111b8c6935f9665f39889ad5ab8ac1f6f52c6ee058ae4521c43c0706e55ed94fbdebba616a8abcff933e5cc6e44d3
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright (c) 2017 Nicolas Martyanoff <khaelin@gmail.com>
2
+
3
+ Permission to use, copy, modify, and distribute this software for any
4
+ purpose with or without fee is hereby granted, provided that the above
5
+ copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Copyright (c) 2017 Nicolas Martyanoff <khaelin@gmail.com>
4
+ #
5
+ # Permission to use, copy, modify, and distribute this software for any
6
+ # purpose with or without fee is hereby granted, provided that the above
7
+ # copyright notice and this permission notice appear in all copies.
8
+ #
9
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
+
17
+ require 'bundler/setup'
18
+
19
+ require 'firefox'
20
+ require 'nss'
21
+ require 'termios'
22
+ require 'thor'
23
+
24
+ class FirefoxData < Thor
25
+ class_option :profile,
26
+ desc: 'the name of the profile',
27
+ banner: 'NAME',
28
+ type: :string,
29
+ default: 'default',
30
+ aliases: ['p']
31
+
32
+ desc "search-logins REGEXP", "Search for logins in the password database"
33
+ option :password,
34
+ desc: 'ask for the master password',
35
+ type: :boolean,
36
+ aliases: ['w']
37
+ def search_logins(re_string)
38
+ re = Regexp.new(re_string, Regexp::IGNORECASE)
39
+
40
+ index = Firefox::ProfileIndex.new()
41
+ index.load()
42
+
43
+ profile = index.profiles[options[:profile]]
44
+
45
+ password = ''
46
+ if options[:password]
47
+ password = ask_password()
48
+ end
49
+
50
+ NSS.init(profile.path)
51
+ NSS.authenticate(password)
52
+
53
+ profile.load_logins(decrypt: true)
54
+ matches = profile.logins.select {|l| l.hostname.match? re}
55
+ renders = matches.map do |login|
56
+ "hostname #{login.hostname}\n" + \
57
+ "username #{login.username}\n" + \
58
+ "password #{login.password}\n"
59
+ end
60
+ puts renders.join("\n")
61
+ end
62
+
63
+ no_commands do
64
+ def without_term_echo(&block)
65
+ attr = Termios.tcgetattr($stdin)
66
+
67
+ nattr = attr.dup
68
+ nattr.c_lflag &= ~(Termios::ECHO | Termios::ICANON)
69
+ Termios.tcsetattr($stdin, Termios::TCSANOW, nattr)
70
+
71
+ begin
72
+ yield
73
+ ensure
74
+ Termios.tcsetattr($stdin, Termios::TCSANOW, attr)
75
+ end
76
+ end
77
+
78
+ def ask_password()
79
+ printf('Password: ')
80
+
81
+ password = ''
82
+
83
+ without_term_echo() do
84
+ password = $stdin.gets().chomp()
85
+ puts ''
86
+ end
87
+
88
+ password
89
+ end
90
+ end
91
+ end
92
+
93
+ FirefoxData.start(ARGV)
@@ -0,0 +1,21 @@
1
+
2
+ require 'rake'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'firefox-data'
6
+ s.version = '1.0.0'
7
+ s.date = '2017-02-11'
8
+ s.summary = 'A library to extract data from firefox profiles.'
9
+ s.description = 'The firefox-data library extracts various types of ' + \
10
+ 'data from firefox profiles.'
11
+ s.homepage = 'https://github.com/galdor/rb-firefox-data'
12
+ s.license = 'ISC'
13
+ s.author = 'Nicolas Martyanoff'
14
+ s.email = 'khaelin@gmail.com'
15
+
16
+ s.required_ruby_version = '>= 2.4.0'
17
+
18
+ s.files = FileList['firefox-data.gemspec', 'LICENSE',
19
+ 'bin/*.rb', 'lib/**/*.rb']
20
+ s.executables = ['firefox-data']
21
+ end
@@ -0,0 +1,25 @@
1
+ # Copyright (c) 2017 Nicolas Martyanoff <khaelin@gmail.com>
2
+ #
3
+ # Permission to use, copy, modify, and distribute this software for any
4
+ # purpose with or without fee is hereby granted, provided that the above
5
+ # copyright notice and this permission notice appear in all copies.
6
+ #
7
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
+
15
+ require 'bundler/setup'
16
+
17
+ require 'pathname'
18
+
19
+ module Firefox
20
+ ROOT_PATH = Pathname.new("#{Dir.home}/.mozilla/firefox")
21
+ end
22
+
23
+ require 'firefox/profile_index'
24
+ require 'firefox/profile'
25
+ require 'firefox/login'
@@ -0,0 +1,109 @@
1
+ # Copyright (c) 2017 Nicolas Martyanoff <khaelin@gmail.com>
2
+ #
3
+ # Permission to use, copy, modify, and distribute this software for any
4
+ # purpose with or without fee is hereby granted, provided that the above
5
+ # copyright notice and this permission notice appear in all copies.
6
+ #
7
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
+
15
+ require 'date'
16
+
17
+ require 'json-schema'
18
+
19
+ require 'nss'
20
+
21
+ module Firefox
22
+ class InvalidLogin < StandardError
23
+ end
24
+
25
+ class Login
26
+ JSON_SCHEMA = {
27
+ 'type' => 'object',
28
+ 'required' => ['id', 'hostname',
29
+ 'encryptedUsername', 'encryptedPassword'],
30
+ 'properties' => {
31
+ 'id' => {'type': 'integer'},
32
+ 'hostname' => {'type': 'string'},
33
+ 'httpRealm' => {'type': ['string', 'null']},
34
+ 'formSubmitURL' => {'type': ['string', 'null']},
35
+ 'usernameField' => {'type': ['string', 'null']},
36
+ 'passwordField' => {'type': ['string', 'null']},
37
+ 'encryptedUsername' => {'type': 'string'},
38
+ 'encryptedPassword' => {'type': 'string'},
39
+ 'guid' => {'type': ['string', 'null']},
40
+ 'encType' => {'type': 'integer'},
41
+ 'timeCreated' => {'type': 'integer'},
42
+ 'timeLastUsed' => {'type': 'integer'},
43
+ 'timePasswordChanged' => {'type': 'integer'},
44
+ 'timesUsed' => {'type': 'integer'},
45
+ },
46
+ }
47
+
48
+ attr_accessor :id, :hostname, :http_realm, :form_submit_url,
49
+ :username_field, :password_field,
50
+ :encrypted_username, :encrypted_password, :enc_type,
51
+ :username, :password,
52
+ :guid,
53
+ :time_created, :time_last_used, :time_password_changed,
54
+ :times_used
55
+
56
+ def initialize()
57
+ end
58
+
59
+ def to_s()
60
+ "#<Firefox::Login #{@hostname}>"
61
+ end
62
+
63
+ def inspect()
64
+ to_s()
65
+ end
66
+
67
+ def self.from_json(data)
68
+ # In firefox (checked in the mercurial repository on 2017-02-11),
69
+ # logins.json is updated in
70
+ # toolkit/components/passwordmgr/storage-json.js
71
+
72
+ begin
73
+ JSON::Validator.validate!(JSON_SCHEMA, data)
74
+ rescue JSON::Schema::ValidationError => err
75
+ raise InvalidLogin, "invalid login data: #{err.message}"
76
+ end
77
+
78
+ login = Login.new()
79
+
80
+ to_date = lambda do |timestamp|
81
+ seconds = timestamp / 1000
82
+ milliseconds = timestamp % 1000
83
+ Time.at(seconds, milliseconds).utc()
84
+ end
85
+
86
+ login.id = data['id']
87
+ login.hostname = data['hostname']
88
+ login.http_realm = data['httpRealm']
89
+ login.form_submit_url = data['formSubmitURL']
90
+ login.username_field = data['usernameField']
91
+ login.password_field = data['passwordField']
92
+ login.encrypted_username = data['encryptedUsername']
93
+ login.encrypted_password = data['encryptedPassword']
94
+ login.enc_type = data['encType']
95
+ login.guid = data['guid']
96
+ login.time_created = to_date.(data['timeCreated'])
97
+ login.time_last_used = to_date.(data['timeLastUsed'])
98
+ login.time_password_changed = to_date.(data['timePasswordChanged'])
99
+ login.times_used = data['timesUsed']
100
+
101
+ login
102
+ end
103
+
104
+ def decrypt()
105
+ @username = NSS.decrypt(@encrypted_username)
106
+ @password = NSS.decrypt(@encrypted_password)
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,54 @@
1
+ # Copyright (c) 2017 Nicolas Martyanoff <khaelin@gmail.com>
2
+ #
3
+ # Permission to use, copy, modify, and distribute this software for any
4
+ # purpose with or without fee is hereby granted, provided that the above
5
+ # copyright notice and this permission notice appear in all copies.
6
+ #
7
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
+
15
+ require 'json'
16
+
17
+ module Firefox
18
+ class InvalidProfile < StandardError
19
+ end
20
+
21
+ class Profile
22
+ attr_reader :name, :path, :logins
23
+
24
+ def initialize(name, path)
25
+ @name = name
26
+ @path = path
27
+ @logins = nil
28
+ end
29
+
30
+ def to_s()
31
+ "#<Firefox::Profile #{@name}>"
32
+ end
33
+
34
+ def inspect()
35
+ to_s()
36
+ end
37
+
38
+ def load_logins(decrypt: false)
39
+ path = @path.join('logins.json')
40
+ data = JSON.parse(File.read(path))
41
+ unless data.key? 'logins'
42
+ raise InvalidProfile, "missing 'logins' entry in #{path}"
43
+ end
44
+
45
+ logins = []
46
+ data['logins'].each do |login_data|
47
+ login = Login.from_json(login_data)
48
+ login.decrypt() if decrypt
49
+ logins << login
50
+ end
51
+ @logins = logins
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,65 @@
1
+ # Copyright (c) 2017 Nicolas Martyanoff <khaelin@gmail.com>
2
+ #
3
+ # Permission to use, copy, modify, and distribute this software for any
4
+ # purpose with or without fee is hereby granted, provided that the above
5
+ # copyright notice and this permission notice appear in all copies.
6
+ #
7
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
+
15
+ module Firefox
16
+ class ProfileIndex
17
+ DEFAULT_PATH = ROOT_PATH.join('profiles.ini')
18
+
19
+ attr_reader :path, :profiles
20
+
21
+ def initialize(path: DEFAULT_PATH)
22
+ @path = path
23
+ @profiles = {}
24
+ end
25
+
26
+ def load()
27
+ sections = []
28
+ section = nil
29
+
30
+ File.open(@path).each do |line|
31
+ if line.match(/^\[([^\]]+)\]/)
32
+ title = $1
33
+ next if title == 'General'
34
+
35
+ section = {}
36
+ sections << section
37
+ elsif !section.nil? && line.match(/^([^=]+)\s*=\s*(.*)/)
38
+ key = $1
39
+ value = $2
40
+
41
+ section[key] = value
42
+ end
43
+ end
44
+
45
+ profiles = {}
46
+ sections.each do |section|
47
+ name = section['Name']
48
+ path = Pathname.new(section['Path'])
49
+ is_relative = section['IsRelative']
50
+
51
+ if is_relative == '1'
52
+ path = ROOT_PATH.join(path)
53
+ end
54
+
55
+ profile = Profile.new(name, path)
56
+ profiles[name] = profile
57
+ end
58
+ @profiles = profiles
59
+ end
60
+
61
+ def profile?(name)
62
+ return @profiles.key? name
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,123 @@
1
+ # Copyright (c) 2017 Nicolas Martyanoff <khaelin@gmail.com>
2
+ #
3
+ # Permission to use, copy, modify, and distribute this software for any
4
+ # purpose with or without fee is hereby granted, provided that the above
5
+ # copyright notice and this permission notice appear in all copies.
6
+ #
7
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
13
+ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
14
+
15
+ require 'ffi'
16
+
17
+ module NSSFFI
18
+ extend FFI::Library
19
+
20
+ class SecItemStr < FFI::Struct
21
+ layout :type, :int,
22
+ :data, :pointer,
23
+ :len, :uint
24
+
25
+ def string()
26
+ self[:data].read_string(self[:len])
27
+ end
28
+ end
29
+
30
+ ffi_lib 'nss3'
31
+
32
+ enum :sec_status, [:wouldblock, -2,
33
+ :failure, -1,
34
+ :success, 0]
35
+
36
+ typedef :int, :pr_bool
37
+ typedef SecItemStr.ptr(), :sec_item
38
+ typedef :int, :sec_item_type
39
+ typedef :pointer, :pl_arena_pool
40
+ typedef :pointer, :pk11_slot_info
41
+
42
+ attach_function :nss_init, 'NSS_Init', [:string], :sec_status
43
+ attach_function :nss_base64_decode_buffer, 'NSSBase64_DecodeBuffer',
44
+ [:pl_arena_pool, :sec_item, :string, :uint], :sec_item
45
+
46
+ attach_function :pk11_get_internal_key_slot, 'PK11_GetInternalKeySlot',
47
+ [], :pk11_slot_info
48
+ attach_function :pk11_free_slot, 'PK11_FreeSlot', [:pk11_slot_info], :void
49
+ attach_function :pk11_check_user_password, 'PK11_CheckUserPassword',
50
+ [:pk11_slot_info, :string], :sec_status
51
+ attach_function :pk11sdr_decrypt, 'PK11SDR_Decrypt',
52
+ [:sec_item, :sec_item, :pointer], :sec_status
53
+
54
+ attach_function :secitem_alloc_item, 'SECITEM_AllocItem',
55
+ [:pl_arena_pool, :sec_item, :uint], :sec_item
56
+ attach_function :secitem_free_item, 'SECITEM_FreeItem',
57
+ [:sec_item, :pr_bool], :void
58
+ end
59
+
60
+ module NSS
61
+ class Error < StandardError
62
+ end
63
+
64
+ def self.init(profile_path)
65
+ res = NSSFFI.nss_init(profile_path.to_s())
66
+ raise NSS::Error, "cannot initialize nss" unless res == :success
67
+ end
68
+
69
+ def self.with_internal_key_slot(&block)
70
+ slot = NSSFFI.pk11_get_internal_key_slot()
71
+ raise NSS::Error, "cannot retrieve internal key slot" if slot.nil?
72
+
73
+ begin
74
+ yield slot
75
+ ensure
76
+ NSSFFI.pk11_free_slot(slot)
77
+ end
78
+ end
79
+
80
+ def self.check_user_password(slot, password)
81
+ res = NSSFFI.pk11_check_user_password(slot, password)
82
+ raise NSS::Error, "authentication failed" unless res == :success
83
+ end
84
+
85
+ def self.authenticate(password)
86
+ with_internal_key_slot do |slot|
87
+ check_user_password(slot, password)
88
+ end
89
+ end
90
+
91
+ def self.base64_decode(str, &block)
92
+ str_item = NSSFFI.nss_base64_decode_buffer(nil, nil, str, str.bytesize())
93
+ raise NSS::Error, "cannot decode base64 string" if str_item.nil?
94
+
95
+ begin
96
+ yield str_item
97
+ ensure
98
+ NSSFFI.secitem_free_item(str_item, 1)
99
+ end
100
+ end
101
+
102
+ def self.decrypt(b64str)
103
+ base64_decode(b64str) do |str_item|
104
+ with_sec_item do |res_item|
105
+ res = NSSFFI.pk11sdr_decrypt(str_item, res_item, nil)
106
+ raise NSS::Error, "cannot decrypt string" unless res == :success
107
+
108
+ res_item.string()
109
+ end
110
+ end
111
+ end
112
+
113
+ def self.with_sec_item(&block)
114
+ item = NSSFFI.secitem_alloc_item(nil, nil, 0)
115
+ raise NSS::Error, "cannot allocate sec item" if item.nil?
116
+
117
+ begin
118
+ yield item
119
+ ensure
120
+ NSSFFI.secitem_free_item(item, 1)
121
+ end
122
+ end
123
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: firefox-data
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Nicolas Martyanoff
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-02-11 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: The firefox-data library extracts various types of data from firefox
14
+ profiles.
15
+ email: khaelin@gmail.com
16
+ executables:
17
+ - firefox-data
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - LICENSE
22
+ - bin/firefox-data
23
+ - firefox-data.gemspec
24
+ - lib/firefox.rb
25
+ - lib/firefox/login.rb
26
+ - lib/firefox/profile.rb
27
+ - lib/firefox/profile_index.rb
28
+ - lib/nss.rb
29
+ homepage: https://github.com/galdor/rb-firefox-data
30
+ licenses:
31
+ - ISC
32
+ metadata: {}
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 2.4.0
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubyforge_project:
49
+ rubygems_version: 2.6.8
50
+ signing_key:
51
+ specification_version: 4
52
+ summary: A library to extract data from firefox profiles.
53
+ test_files: []