lastpass 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.
- data/.gitignore +3 -0
- data/.ruby-version +1 -0
- data/.travis.yml +3 -0
- data/Gemfile +2 -0
- data/LICENSE +20 -0
- data/Makefile +4 -0
- data/README.md +59 -0
- data/Rakefile +16 -0
- data/example/credentials.yaml.example +4 -0
- data/example/example.rb +19 -0
- data/lastpass.gemspec +29 -0
- data/lib/lastpass.rb +18 -0
- data/lib/lastpass/account.rb +22 -0
- data/lib/lastpass/blob.rb +18 -0
- data/lib/lastpass/chunk.rb +14 -0
- data/lib/lastpass/exceptions.rb +33 -0
- data/lib/lastpass/fetcher.rb +125 -0
- data/lib/lastpass/parser.rb +184 -0
- data/lib/lastpass/session.rb +14 -0
- data/lib/lastpass/vault.rb +34 -0
- data/lib/lastpass/version.rb +6 -0
- data/spec/account_spec.rb +22 -0
- data/spec/blob_spec.rb +23 -0
- data/spec/chunk_spec.rb +14 -0
- data/spec/fetcher_spec.rb +228 -0
- data/spec/parser_spec.rb +292 -0
- data/spec/session_spec.rb +14 -0
- data/spec/spec_helper.rb +41 -0
- data/spec/test_data.rb +996 -0
- data/spec/vault_spec.rb +22 -0
- metadata +164 -0
@@ -0,0 +1,14 @@
|
|
1
|
+
# Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
|
2
|
+
# Licensed under the terms of the MIT license. See LICENCE for details.
|
3
|
+
|
4
|
+
module LastPass
|
5
|
+
class Session
|
6
|
+
attr_reader :id,
|
7
|
+
:key_iteration_count
|
8
|
+
|
9
|
+
def initialize id, key_iteration_count
|
10
|
+
@id = id
|
11
|
+
@key_iteration_count = key_iteration_count
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
|
2
|
+
# Licensed under the terms of the MIT license. See LICENCE for details.
|
3
|
+
|
4
|
+
module LastPass
|
5
|
+
class Vault
|
6
|
+
attr_reader :accounts
|
7
|
+
|
8
|
+
# Fetches a blob from the server and creates a vault
|
9
|
+
def self.open_remote username, password
|
10
|
+
open Vault.fetch_blob(username, password), username, password
|
11
|
+
end
|
12
|
+
|
13
|
+
# Creates a vault from a locally stored blob
|
14
|
+
def self.open_local blob_filename, username, password
|
15
|
+
# TODO: read the blob here
|
16
|
+
end
|
17
|
+
|
18
|
+
# Creates a vault from a blob object
|
19
|
+
def self.open blob, username, password
|
20
|
+
new blob, blob.encryption_key(username, password)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Just fetches the blob, could be used to store it locally
|
24
|
+
def self.fetch_blob username, password
|
25
|
+
Fetcher.fetch Fetcher.login username, password
|
26
|
+
end
|
27
|
+
|
28
|
+
# This more of an internal method, use of the static constructors instead
|
29
|
+
def initialize blob, encryption_key
|
30
|
+
chunks = Parser.extract_chunks blob
|
31
|
+
@accounts = (chunks["ACCT"] || []).map { |i| Parser.parse_account i, encryption_key }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
|
2
|
+
# Licensed under the terms of the MIT license. See LICENCE for details.
|
3
|
+
|
4
|
+
require "spec_helper"
|
5
|
+
|
6
|
+
describe LastPass::Account do
|
7
|
+
let(:id) { "id" }
|
8
|
+
let(:name) { "name" }
|
9
|
+
let(:username) { "username" }
|
10
|
+
let(:password) { "password" }
|
11
|
+
let(:url) { "url" }
|
12
|
+
let(:group) { "group" }
|
13
|
+
|
14
|
+
subject { LastPass::Account.new id, name, username, password, url, group }
|
15
|
+
|
16
|
+
its(:id) { should eq id }
|
17
|
+
its(:name) { should eq name }
|
18
|
+
its(:username) { should eq username }
|
19
|
+
its(:password) { should eq password }
|
20
|
+
its(:url) { should eq url }
|
21
|
+
its(:group) { should eq group }
|
22
|
+
end
|
data/spec/blob_spec.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
|
2
|
+
# Licensed under the terms of the MIT license. See LICENCE for details.
|
3
|
+
|
4
|
+
require "spec_helper"
|
5
|
+
|
6
|
+
describe LastPass::Blob do
|
7
|
+
let(:bytes) { "TFBBVgAAAAMxMjJQUkVNAAAACjE0MTQ5".decode64 }
|
8
|
+
let(:key_iteration_count) { 500 }
|
9
|
+
let(:username) { "postlass@gmail.com" }
|
10
|
+
let(:password) { "pl1234567890" }
|
11
|
+
let(:encryption_key) { "OfOUvVnQzB4v49sNh4+PdwIFb9Fr5+jVfWRTf+E2Ghg=".decode64 }
|
12
|
+
|
13
|
+
subject { LastPass::Blob.new bytes, key_iteration_count }
|
14
|
+
|
15
|
+
its(:bytes) { should eq bytes }
|
16
|
+
its(:key_iteration_count) { should eq key_iteration_count }
|
17
|
+
|
18
|
+
describe "#encryption_key" do
|
19
|
+
it "returns encryption key" do
|
20
|
+
expect(subject.encryption_key username, password).to eq encryption_key
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
data/spec/chunk_spec.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
|
2
|
+
# Licensed under the terms of the MIT license. See LICENCE for details.
|
3
|
+
|
4
|
+
require "spec_helper"
|
5
|
+
|
6
|
+
describe LastPass::Chunk do
|
7
|
+
let(:id) { "IDID" }
|
8
|
+
let(:payload) { "Payload" }
|
9
|
+
|
10
|
+
subject { LastPass::Chunk.new id, payload }
|
11
|
+
|
12
|
+
its(:id) { should eq id }
|
13
|
+
its(:payload) { should eq payload }
|
14
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
# Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
|
2
|
+
# Licensed under the terms of the MIT license. See LICENCE for details.
|
3
|
+
|
4
|
+
require "spec_helper"
|
5
|
+
|
6
|
+
describe LastPass::Fetcher do
|
7
|
+
let(:username) { "username" }
|
8
|
+
let(:password) { "password" }
|
9
|
+
let(:key_iteration_count) { 5000 }
|
10
|
+
let(:session_id) { "53ru,Hb713QnEVM5zWZ16jMvxS0" }
|
11
|
+
let(:session) { LastPass::Session.new session_id, key_iteration_count }
|
12
|
+
let(:blob_response) { "TFBBVgAAAAMxMjJQUkVNAAAACjE0MTQ5" }
|
13
|
+
let(:blob_bytes) { blob_response.decode64 }
|
14
|
+
let(:blob) { LastPass::Blob.new blob_bytes, key_iteration_count }
|
15
|
+
|
16
|
+
describe ".request_iteration_count" do
|
17
|
+
it "makes a POST request" do
|
18
|
+
expect(web_client = double("web_client")).to receive(:post)
|
19
|
+
.with("https://lastpass.com/iterations.php", query: {email: username})
|
20
|
+
.and_return(http_ok(key_iteration_count.to_s))
|
21
|
+
|
22
|
+
LastPass::Fetcher.request_iteration_count username, web_client
|
23
|
+
end
|
24
|
+
|
25
|
+
it "returns key iteration count" do
|
26
|
+
expect(
|
27
|
+
LastPass::Fetcher.request_iteration_count username,
|
28
|
+
double("web_client", post: http_ok(key_iteration_count.to_s))
|
29
|
+
).to eq key_iteration_count
|
30
|
+
end
|
31
|
+
|
32
|
+
it "raises an exception on HTTP error" do
|
33
|
+
expect {
|
34
|
+
LastPass::Fetcher.request_iteration_count username,
|
35
|
+
double("web_client", post: http_error)
|
36
|
+
}.to raise_error LastPass::NetworkError
|
37
|
+
end
|
38
|
+
|
39
|
+
it "raises an exception on invalid key iteration count" do
|
40
|
+
expect {
|
41
|
+
LastPass::Fetcher.request_iteration_count username,
|
42
|
+
double("web_client", post: http_ok("not a number"))
|
43
|
+
}.to raise_error LastPass::InvalidResponse, "Key iteration count is invalid"
|
44
|
+
end
|
45
|
+
|
46
|
+
it "raises an exception on zero key iteration count" do
|
47
|
+
expect {
|
48
|
+
LastPass::Fetcher.request_iteration_count username,
|
49
|
+
double("web_client", post: http_ok("0"))
|
50
|
+
}.to raise_error LastPass::InvalidResponse, "Key iteration count is not positive"
|
51
|
+
end
|
52
|
+
|
53
|
+
it "raises an exception on negative key iteration count" do
|
54
|
+
expect {
|
55
|
+
LastPass::Fetcher.request_iteration_count username,
|
56
|
+
double("web_client", post: http_ok("-1"))
|
57
|
+
}.to raise_error LastPass::InvalidResponse, "Key iteration count is not positive"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe ".request_login" do
|
62
|
+
it "makes a POST request" do
|
63
|
+
expect(web_client = double("web_client")).to receive(:post)
|
64
|
+
.with("https://lastpass.com/login.php", format: :xml, body: anything)
|
65
|
+
.and_return(http_ok("ok" => {"sessionid" => session_id}))
|
66
|
+
|
67
|
+
LastPass::Fetcher.request_login username, password, key_iteration_count, web_client
|
68
|
+
end
|
69
|
+
|
70
|
+
it "returns a session" do
|
71
|
+
expect(request_login_with_xml "<ok sessionid='#{session_id}' />").to eq session
|
72
|
+
end
|
73
|
+
|
74
|
+
it "raises an exception on HTTP error" do
|
75
|
+
expect { request_login_with_error }.to raise_error LastPass::NetworkError
|
76
|
+
end
|
77
|
+
|
78
|
+
it "raises an exception when response is not a hash" do
|
79
|
+
expect { request_login_with_ok "not a hash" }.to raise_error LastPass::InvalidResponse
|
80
|
+
end
|
81
|
+
|
82
|
+
it "raises an exception on unknown response schema" do
|
83
|
+
expect { request_login_with_xml "<unknown />" }.to raise_error LastPass::UnknownResponseSchema
|
84
|
+
end
|
85
|
+
|
86
|
+
it "raises an exception on unknown response schema" do
|
87
|
+
expect { request_login_with_xml "<response />" }.to raise_error LastPass::UnknownResponseSchema
|
88
|
+
end
|
89
|
+
|
90
|
+
it "raises an exception on unknown response schema" do
|
91
|
+
expect { request_login_with_xml "<response><error /></response>" }
|
92
|
+
.to raise_error LastPass::UnknownResponseSchema
|
93
|
+
end
|
94
|
+
|
95
|
+
it "raises an exception on unknown username" do
|
96
|
+
message = "Unknown email address."
|
97
|
+
expect { request_login_with_lastpass_error "unknownemail", message }
|
98
|
+
.to raise_error LastPass::LastPassUnknownUsername, message
|
99
|
+
end
|
100
|
+
|
101
|
+
it "raises an exception on invalid password" do
|
102
|
+
message = "Invalid password!"
|
103
|
+
expect { request_login_with_lastpass_error "unknownpassword", message }
|
104
|
+
.to raise_error LastPass::LastPassInvalidPassword, message
|
105
|
+
end
|
106
|
+
|
107
|
+
it "raises an exception on unknown LastPass error with a message" do
|
108
|
+
message = "Unknow error message"
|
109
|
+
expect { request_login_with_lastpass_error "Unknown cause", message }
|
110
|
+
.to raise_error LastPass::LastPassUnknownError, message
|
111
|
+
end
|
112
|
+
|
113
|
+
it "raises an exception on unknown LastPass error without a message" do
|
114
|
+
cause = "Unknown casue"
|
115
|
+
expect { request_login_with_lastpass_error cause }
|
116
|
+
.to raise_error LastPass::LastPassUnknownError, cause
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
describe ".fetch" do
|
121
|
+
it "makes a GET request" do
|
122
|
+
expect(web_client = double("web_client")).to receive(:get)
|
123
|
+
.with("https://lastpass.com/getaccts.php?mobile=1&b64=1&hash=0.0",
|
124
|
+
format: :plain,
|
125
|
+
cookies: {"PHPSESSID" => session_id})
|
126
|
+
.and_return(http_ok(blob_response))
|
127
|
+
|
128
|
+
LastPass::Fetcher.fetch session, web_client
|
129
|
+
end
|
130
|
+
|
131
|
+
it "returns a blob" do
|
132
|
+
expect(LastPass::Fetcher.fetch session, double("web_client", get: http_ok(blob_response)))
|
133
|
+
.to eq blob
|
134
|
+
end
|
135
|
+
|
136
|
+
it "raises an exception on HTTP error" do
|
137
|
+
expect {
|
138
|
+
LastPass::Fetcher.fetch session, double("web_client", get: http_error)
|
139
|
+
} .to raise_error LastPass::NetworkError
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
describe ".make_key" do
|
144
|
+
it "generates correct keys" do
|
145
|
+
def key iterations
|
146
|
+
LastPass::Fetcher.make_key "postlass@gmail.com", "pl1234567890", iterations
|
147
|
+
end
|
148
|
+
|
149
|
+
expect(key 1).to eq "C/Bh2SGWxI8JDu54DbbpV8J9wa6pKbesIb9MAXkeF3Y=".decode64
|
150
|
+
expect(key 5).to eq "pE9goazSCRqnWwcixWM4NHJjWMvB5T15dMhe6ug1pZg=".decode64
|
151
|
+
expect(key 10).to eq "n9S0SyJdrMegeBHtkxUx8Lzc7wI6aGl+y3/udGmVey8=".decode64
|
152
|
+
expect(key 50).to eq "GwI8/kNy1NjIfe3Z0VAZfF78938UVuCi6xAL3MJBux0=".decode64
|
153
|
+
expect(key 100).to eq "piGdSULeHMWiBS3QJNM46M5PIYwQXA6cNS10pLB3Xf8=".decode64
|
154
|
+
expect(key 500).to eq "OfOUvVnQzB4v49sNh4+PdwIFb9Fr5+jVfWRTf+E2Ghg=".decode64
|
155
|
+
expect(key 1000).to eq "z7CdwlIkbu0XvcB7oQIpnlqwNGemdrGTBmDKnL9taPg=".decode64
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
describe ".make_hash" do
|
160
|
+
it "generates correct hashes" do
|
161
|
+
def hash iterations
|
162
|
+
LastPass::Fetcher.make_hash "postlass@gmail.com", "pl1234567890", iterations
|
163
|
+
end
|
164
|
+
|
165
|
+
expect(hash 1).to eq "a1943cfbb75e37b129bbf78b9baeab4ae6dd08225776397f66b8e0c7a913a055"
|
166
|
+
expect(hash 5).to eq "a95849e029a7791cfc4503eed9ec96ab8675c4a7c4e82b00553ddd179b3d8445"
|
167
|
+
expect(hash 10).to eq "0da0b44f5e6b7306f14e92de6d629446370d05afeb1dc07cfcbe25f169170c16"
|
168
|
+
expect(hash 50).to eq "1d5bc0d636da4ad469cefe56c42c2ff71589facb9c83f08fcf7711a7891cc159"
|
169
|
+
expect(hash 100).to eq "82fc12024acb618878ba231a9948c49c6f46e30b5a09c11d87f6d3338babacb5"
|
170
|
+
expect(hash 500).to eq "3139861ae962801b59fc41ff7eeb11f84ca56d810ab490f0d8c89d9d9ab07aa6"
|
171
|
+
expect(hash 1000).to eq "03161354566c396fcd624a424164160e890e96b4b5fa6d942fc6377ab613513b"
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
#
|
176
|
+
# Helpers
|
177
|
+
#
|
178
|
+
|
179
|
+
private
|
180
|
+
|
181
|
+
def mock_response type, code, body
|
182
|
+
double response: type.new("1.1", code, ""),
|
183
|
+
parsed_response: body
|
184
|
+
end
|
185
|
+
|
186
|
+
def http_ok body
|
187
|
+
mock_response Net::HTTPOK, 200, body
|
188
|
+
end
|
189
|
+
|
190
|
+
def http_error body = ""
|
191
|
+
mock_response Net::HTTPNotFound, 404, body
|
192
|
+
end
|
193
|
+
|
194
|
+
def xml text
|
195
|
+
MultiXml.parse text
|
196
|
+
end
|
197
|
+
|
198
|
+
def lastpass_error cause, message
|
199
|
+
if message
|
200
|
+
%Q{<response><error cause="#{cause}" message="#{message}" /></response>}
|
201
|
+
else
|
202
|
+
%Q{<response><error cause="#{cause}" /></response>}
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def request_login_with_lastpass_error cause, message = nil
|
207
|
+
request_login_with_xml lastpass_error cause, message
|
208
|
+
end
|
209
|
+
|
210
|
+
def request_login_with_xml text
|
211
|
+
request_login_with_ok xml text
|
212
|
+
end
|
213
|
+
|
214
|
+
def request_login_with_ok response
|
215
|
+
request_login_with_response http_ok response
|
216
|
+
end
|
217
|
+
|
218
|
+
def request_login_with_error
|
219
|
+
request_login_with_response http_error
|
220
|
+
end
|
221
|
+
|
222
|
+
def request_login_with_response response
|
223
|
+
LastPass::Fetcher.request_login username,
|
224
|
+
password,
|
225
|
+
key_iteration_count,
|
226
|
+
double("web_client", post: response)
|
227
|
+
end
|
228
|
+
end
|
data/spec/parser_spec.rb
ADDED
@@ -0,0 +1,292 @@
|
|
1
|
+
# Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
|
2
|
+
# Licensed under the terms of the MIT license. See LICENCE for details.
|
3
|
+
|
4
|
+
require "spec_helper"
|
5
|
+
require_relative "test_data"
|
6
|
+
|
7
|
+
describe LastPass::Parser do
|
8
|
+
let(:key_iteration_count) { 5000 }
|
9
|
+
let(:blob) { LastPass::Blob.new TEST_BLOB, key_iteration_count }
|
10
|
+
let(:padding) { "BEEFFACE"}
|
11
|
+
let(:encryption_key) { "OfOUvVnQzB4v49sNh4+PdwIFb9Fr5+jVfWRTf+E2Ghg=".decode64 }
|
12
|
+
|
13
|
+
describe ".extract_chunks" do
|
14
|
+
context "returned chunks" do
|
15
|
+
let(:chunks) { LastPass::Parser.extract_chunks blob }
|
16
|
+
|
17
|
+
it { expect(chunks).to be_instance_of Hash }
|
18
|
+
|
19
|
+
it "all keys are strings" do
|
20
|
+
expect(chunks.keys).to match_array TEST_CHUNK_IDS
|
21
|
+
end
|
22
|
+
|
23
|
+
it "all values are arrays" do
|
24
|
+
expect(chunks.values.map(&:class).uniq).to eq [Array]
|
25
|
+
end
|
26
|
+
|
27
|
+
it "all arrays contain only chunks" do
|
28
|
+
expect(chunks.values.flat_map { |i| i.map &:class }.uniq).to eq [LastPass::Chunk]
|
29
|
+
end
|
30
|
+
|
31
|
+
it "all chunks grouped under correct IDs" do
|
32
|
+
expect(
|
33
|
+
chunks.all? { |id, chunk_group| chunk_group.map(&:id).uniq == [id] }
|
34
|
+
).to be_true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe ".parse_account" do
|
40
|
+
let(:accounts) {
|
41
|
+
LastPass::Parser
|
42
|
+
.extract_chunks(blob)["ACCT"]
|
43
|
+
.map { |i| LastPass::Parser.parse_account i, TEST_ENCRYPTION_KEY }
|
44
|
+
}
|
45
|
+
|
46
|
+
it "parses accounts" do
|
47
|
+
expect(accounts.map &:id).to eq TEST_ACCOUNTS.map &:id
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe ".read_chunk" do
|
52
|
+
it "returns a chunk" do
|
53
|
+
with_hex "4142434400000004DEADBEEF" + padding do |io|
|
54
|
+
expect(LastPass::Parser.read_chunk io).to eq LastPass::Chunk.new("ABCD", "DEADBEEF".decode_hex)
|
55
|
+
expect(io.pos).to eq 12
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe ".read_item" do
|
61
|
+
it "returns an item" do
|
62
|
+
with_hex "00000004DEADBEEF" + padding do |io|
|
63
|
+
expect(LastPass::Parser.read_item io).to eq "DEADBEEF".decode_hex
|
64
|
+
expect(io.pos).to eq 8
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe ".skip_item" do
|
70
|
+
it "skips an empty item" do
|
71
|
+
with_hex "00000000" + padding do |io|
|
72
|
+
LastPass::Parser.skip_item io
|
73
|
+
expect(io.pos).to eq 4
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
it "skips a non-empty item" do
|
78
|
+
with_hex "00000004DEADBEEF" + padding do |io|
|
79
|
+
LastPass::Parser.skip_item io
|
80
|
+
expect(io.pos).to eq 8
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe ".read_id" do
|
86
|
+
it "returns an id" do
|
87
|
+
with_bytes "ABCD" + padding do |io|
|
88
|
+
expect(LastPass::Parser.read_id io).to eq "ABCD"
|
89
|
+
expect(io.pos).to eq 4
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe ".read_size" do
|
95
|
+
it "returns a size" do
|
96
|
+
with_hex "DEADBEEF" + padding do |io|
|
97
|
+
expect(LastPass::Parser.read_size io).to eq 0xDEADBEEF
|
98
|
+
expect(io.pos).to eq 4
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
describe ".read_payload" do
|
104
|
+
it "returns a payload" do
|
105
|
+
with_hex "FEEDDEADBEEF" + padding do |io|
|
106
|
+
expect(LastPass::Parser.read_payload io, 6).to eq "FEEDDEADBEEF".decode_hex
|
107
|
+
expect(io.pos).to eq 6
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe ".read_uint32" do
|
113
|
+
it "returns a number" do
|
114
|
+
with_hex "DEADBEEF" + padding do |io|
|
115
|
+
expect(LastPass::Parser.read_size io).to eq 0xDEADBEEF
|
116
|
+
expect(io.pos).to eq 4
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe ".decode_hex" do
|
122
|
+
it "decodes hex" do
|
123
|
+
expect(LastPass::Parser.decode_hex "")
|
124
|
+
.to eq ""
|
125
|
+
|
126
|
+
expect(LastPass::Parser.decode_hex "00ff")
|
127
|
+
.to eq "\x00\xFF"
|
128
|
+
|
129
|
+
expect(LastPass::Parser.decode_hex "00010203040506070809")
|
130
|
+
.to eq "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09"
|
131
|
+
|
132
|
+
expect(LastPass::Parser.decode_hex "000102030405060708090a0b0c0d0e0f")
|
133
|
+
.to eq "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"
|
134
|
+
|
135
|
+
expect(LastPass::Parser.decode_hex "8af633933e96a3c3550c2734bd814195")
|
136
|
+
.to eq "\x8A\xF6\x33\x93\x3E\x96\xA3\xC3\x55\x0C\x27\x34\xBD\x81\x41\x95"
|
137
|
+
end
|
138
|
+
|
139
|
+
it "raises exception on odd length" do
|
140
|
+
expect { LastPass::Parser.decode_hex "0" }
|
141
|
+
.to raise_error ArgumentError, "Input length must be multple of 2"
|
142
|
+
end
|
143
|
+
|
144
|
+
it "raises exception on invalid characters" do
|
145
|
+
expect { LastPass::Parser.decode_hex "xz" }
|
146
|
+
.to raise_error ArgumentError, "Input contains invalid characters"
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe ".decode_base64" do
|
151
|
+
it "decodes base64" do
|
152
|
+
def check base64, plain
|
153
|
+
expect(LastPass::Parser.decode_base64 base64).to eq plain
|
154
|
+
end
|
155
|
+
|
156
|
+
check "", ""
|
157
|
+
check "YQ==", "a"
|
158
|
+
check "YWI=", "ab"
|
159
|
+
check "YWJj", "abc"
|
160
|
+
check "YWJjZA==", "abcd"
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
describe ".decode_aes256_auto" do
|
165
|
+
def check encoded, decoded
|
166
|
+
expect(LastPass::Parser.decode_aes256_auto encoded, encryption_key)
|
167
|
+
.to eq decoded
|
168
|
+
end
|
169
|
+
|
170
|
+
it "decodes a blank string" do
|
171
|
+
check "", ""
|
172
|
+
end
|
173
|
+
|
174
|
+
it "decodes ECB/plain string" do
|
175
|
+
check "BNhd3Q3ZVODxk9c0C788NUPTIfYnZuxXfkghtMJ8jVM=".decode64,
|
176
|
+
"All your base are belong to us"
|
177
|
+
end
|
178
|
+
|
179
|
+
it "decodes ECB/base64 string" do
|
180
|
+
check "BNhd3Q3ZVODxk9c0C788NUPTIfYnZuxXfkghtMJ8jVM=",
|
181
|
+
"All your base are belong to us"
|
182
|
+
end
|
183
|
+
|
184
|
+
it "decodes CBC/plain string" do
|
185
|
+
check "IcokDWmjOkKtLpZehWKL6666Uj6fNXPpX6lLWlou+1Lrwb+D3ymP6BAwd6C0TB3hSA==".decode64,
|
186
|
+
"All your base are belong to us"
|
187
|
+
end
|
188
|
+
|
189
|
+
it "decodes CBC/base64 string" do
|
190
|
+
check "!YFuiAVZgOD2K+s6y8yaMOw==|TZ1+if9ofqRKTatyUaOnfudletslMJ/RZyUwJuR/+aI=",
|
191
|
+
"All your base are belong to us"
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
describe ".decode_aes256_ecb_plain" do
|
196
|
+
def check encoded, decoded
|
197
|
+
expect(LastPass::Parser.decode_aes256_ecb_plain encoded.decode64, encryption_key)
|
198
|
+
.to eq decoded
|
199
|
+
end
|
200
|
+
|
201
|
+
it "decodes a blank string" do
|
202
|
+
check "", ""
|
203
|
+
end
|
204
|
+
|
205
|
+
it "decodes a short string" do
|
206
|
+
check "8mHxIA8rul6eq72a/Gq2iw==", "0123456789"
|
207
|
+
end
|
208
|
+
|
209
|
+
it "decodes a long string" do
|
210
|
+
check "BNhd3Q3ZVODxk9c0C788NUPTIfYnZuxXfkghtMJ8jVM=", "All your base are belong to us"
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
describe ".decode_aes256_ecb_base64" do
|
215
|
+
def check encoded, decoded
|
216
|
+
expect(LastPass::Parser.decode_aes256_ecb_base64 encoded, encryption_key)
|
217
|
+
.to eq decoded
|
218
|
+
end
|
219
|
+
|
220
|
+
it "decodes a blank string" do
|
221
|
+
check "", ""
|
222
|
+
end
|
223
|
+
|
224
|
+
it "decodes a short string" do
|
225
|
+
check "8mHxIA8rul6eq72a/Gq2iw==", "0123456789"
|
226
|
+
end
|
227
|
+
|
228
|
+
it "decodes a long string" do
|
229
|
+
check "BNhd3Q3ZVODxk9c0C788NUPTIfYnZuxXfkghtMJ8jVM=", "All your base are belong to us"
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
describe ".decode_aes256_cbc_plain" do
|
234
|
+
def check encoded, decoded
|
235
|
+
expect(LastPass::Parser.decode_aes256_cbc_plain encoded.decode64, encryption_key)
|
236
|
+
.to eq decoded
|
237
|
+
end
|
238
|
+
|
239
|
+
it "decodes a blank string" do
|
240
|
+
check "", ""
|
241
|
+
end
|
242
|
+
|
243
|
+
it "decodes a short string" do
|
244
|
+
check "IQ+hiIy0vGG4srsHmXChe3ehWc/rYPnfiyqOG8h78DdX", "0123456789"
|
245
|
+
end
|
246
|
+
|
247
|
+
it "decodes a long string" do
|
248
|
+
check "IcokDWmjOkKtLpZehWKL6666Uj6fNXPpX6lLWlou+1Lrwb+D3ymP6BAwd6C0TB3hSA==",
|
249
|
+
"All your base are belong to us"
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
describe ".decode_aes256_cbc_base64" do
|
254
|
+
def check encoded, decoded
|
255
|
+
expect(LastPass::Parser.decode_aes256_cbc_base64 encoded, encryption_key)
|
256
|
+
.to eq decoded
|
257
|
+
end
|
258
|
+
|
259
|
+
it "decodes a blank string" do
|
260
|
+
check "", ""
|
261
|
+
end
|
262
|
+
|
263
|
+
it "decodes a short string" do
|
264
|
+
check "!6TZb9bbrqpocMaNgFjrhjw==|f7RcJ7UowesqGk+um+P5ug==", "0123456789"
|
265
|
+
end
|
266
|
+
|
267
|
+
it "decodes a long string" do
|
268
|
+
check "!YFuiAVZgOD2K+s6y8yaMOw==|TZ1+if9ofqRKTatyUaOnfudletslMJ/RZyUwJuR/+aI=",
|
269
|
+
"All your base are belong to us"
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
#
|
274
|
+
# Helpers
|
275
|
+
#
|
276
|
+
|
277
|
+
private
|
278
|
+
|
279
|
+
def with_blob &block
|
280
|
+
with_bytes TEST_BLOB, &block
|
281
|
+
end
|
282
|
+
|
283
|
+
def with_hex hex, &block
|
284
|
+
with_bytes hex.decode_hex, &block
|
285
|
+
end
|
286
|
+
|
287
|
+
def with_bytes bytes, &block
|
288
|
+
StringIO.open bytes do |io|
|
289
|
+
yield io
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|