lastpass 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|