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
data/.gitignore
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.3-p448
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Dmitry Yakimenko
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Makefile
ADDED
data/README.md
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
LastPass Ruby API
|
2
|
+
=================
|
3
|
+
|
4
|
+
[](https://travis-ci.org/detunized/lastpass-ruby)
|
5
|
+
[](https://coveralls.io/r/detunized/lastpass-ruby?branch=master)
|
6
|
+
[](https://codeclimate.com/github/detunized/lastpass-ruby)
|
7
|
+
[](https://gemnasium.com/detunized/lastpass-ruby)
|
8
|
+
|
9
|
+
**This is unofficial LastPass API.**
|
10
|
+
|
11
|
+
This library implements fetching and parsing of LastPass data. The library is
|
12
|
+
still in the proof of concept stage and doesn't support all LastPass features
|
13
|
+
yet. Only account information (logins, passwords, urls, etc.) is available so
|
14
|
+
far.
|
15
|
+
|
16
|
+
There is a low level API which is used to fetch the data from the LastPass
|
17
|
+
server and parse it. Normally this is not the one you would want to use. What
|
18
|
+
you want is the `Vault` class which hides all the complexity and exposes all
|
19
|
+
the accounts already parsed, decrypted and ready to use. See the example
|
20
|
+
program for detail.
|
21
|
+
|
22
|
+
A quick example of accessing your account information:
|
23
|
+
|
24
|
+
```ruby
|
25
|
+
require "lastpass.rb"
|
26
|
+
|
27
|
+
vault = LastPass::Vault.open_remote "username", "password"
|
28
|
+
vault.accounts.each do |i|
|
29
|
+
puts "#{i.name}: #{i.username}, #{i.password} (#{i.url})"
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
The blob received from LastPass could be safely stored locally (it's well
|
34
|
+
encrypted) and reused later on.
|
35
|
+
|
36
|
+
|
37
|
+
LostPass iOS App
|
38
|
+
----------------
|
39
|
+
|
40
|
+
There's an iOS app called [LostPass](http://detunized.net/lostpass/) that is
|
41
|
+
based on a totally incomplete C++ port of this library. If you are a LastPass
|
42
|
+
user it would have made your life much easier if I didn't have to take it down
|
43
|
+
from the App Store. Now it's open source and if you have a developer account
|
44
|
+
or a jailbroken phone you could build it and install it on the phone. The
|
45
|
+
source code is [here](https://github.com/detunized/LostPass).
|
46
|
+
|
47
|
+
|
48
|
+
Contributing
|
49
|
+
------------
|
50
|
+
|
51
|
+
Contribution in any form and shape is very welcome. Have comments,
|
52
|
+
suggestions, patches, pull requests? All of the above are welcome.
|
53
|
+
|
54
|
+
|
55
|
+
License
|
56
|
+
-------
|
57
|
+
|
58
|
+
The library is released under [the MIT
|
59
|
+
license](http://www.opensource.org/licenses/mit-license.php).
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
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 "rspec/core/rake_task"
|
5
|
+
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
# Spec
|
9
|
+
RSpec::Core::RakeTask.new :spec do |task|
|
10
|
+
task.rspec_opts = "--format nested --color"
|
11
|
+
end
|
12
|
+
|
13
|
+
# Example
|
14
|
+
task :example do
|
15
|
+
ruby "-Ilib", "example/example.rb"
|
16
|
+
end
|
data/example/example.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
|
2
|
+
# Licensed under the terms of the MIT license. See LICENCE for details.
|
3
|
+
|
4
|
+
# Run via top level rake file:
|
5
|
+
# $ rake example
|
6
|
+
|
7
|
+
require "lastpass"
|
8
|
+
require "yaml"
|
9
|
+
|
10
|
+
credentials = YAML.load_file File.join File.dirname(__FILE__), "credentials.yaml"
|
11
|
+
|
12
|
+
username = credentials["username"]
|
13
|
+
password = credentials["password"]
|
14
|
+
|
15
|
+
vault = LastPass::Vault.open_remote username, password
|
16
|
+
|
17
|
+
vault.accounts.each_with_index do |i, index|
|
18
|
+
puts "#{index + 1}: #{i.id} #{i.name} #{i.username} #{i.password} #{i.url} #{i.group}}"
|
19
|
+
end
|
data/lastpass.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Copyright (C) 2013 Dmitry Yakimenko (detunized@gmail.com).
|
2
|
+
# Licensed under the terms of the MIT license. See LICENCE for details.
|
3
|
+
|
4
|
+
$:.push File.expand_path("../lib", __FILE__)
|
5
|
+
require "lastpass/version"
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "lastpass"
|
9
|
+
s.version = LastPass::VERSION
|
10
|
+
s.licenses = ["MIT"]
|
11
|
+
s.authors = ["Dmitry Yakimenko"]
|
12
|
+
s.email = "detunized@gmail.com"
|
13
|
+
s.homepage = "https://github.com/detunized/lastpass-ruby"
|
14
|
+
s.summary = "Unofficial LastPass API"
|
15
|
+
s.description = "Unofficial LastPass API"
|
16
|
+
|
17
|
+
s.required_ruby_version = ">= 1.9.3"
|
18
|
+
|
19
|
+
s.add_dependency "httparty", "~> 0.12.0"
|
20
|
+
s.add_dependency "pbkdf2", "~> 0.1.0"
|
21
|
+
|
22
|
+
s.add_development_dependency "rake", "~> 10.0.0"
|
23
|
+
s.add_development_dependency "rspec", "~> 2.14.0"
|
24
|
+
s.add_development_dependency "coveralls", "~> 0.7.0"
|
25
|
+
|
26
|
+
s.files = `git ls-files`.split "\n"
|
27
|
+
s.test_files = `git ls-files spec`.split "\n"
|
28
|
+
s.require_paths = ["lib"]
|
29
|
+
end
|
data/lib/lastpass.rb
ADDED
@@ -0,0 +1,18 @@
|
|
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 "base64"
|
5
|
+
require "httparty"
|
6
|
+
require "openssl"
|
7
|
+
require "pbkdf2"
|
8
|
+
require "stringio"
|
9
|
+
|
10
|
+
require "lastpass/account"
|
11
|
+
require "lastpass/blob"
|
12
|
+
require "lastpass/chunk"
|
13
|
+
require "lastpass/exceptions"
|
14
|
+
require "lastpass/fetcher"
|
15
|
+
require "lastpass/parser"
|
16
|
+
require "lastpass/session"
|
17
|
+
require "lastpass/vault"
|
18
|
+
require "lastpass/version"
|
@@ -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
|
+
module LastPass
|
5
|
+
class Account
|
6
|
+
attr_reader :id,
|
7
|
+
:name,
|
8
|
+
:username,
|
9
|
+
:password,
|
10
|
+
:url,
|
11
|
+
:group
|
12
|
+
|
13
|
+
def initialize id, name, username, password, url, group
|
14
|
+
@id = id
|
15
|
+
@name = name
|
16
|
+
@username = username
|
17
|
+
@password = password
|
18
|
+
@url = url
|
19
|
+
@group = group
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,18 @@
|
|
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 Blob
|
6
|
+
attr_reader :bytes,
|
7
|
+
:key_iteration_count
|
8
|
+
|
9
|
+
def initialize bytes, key_iteration_count
|
10
|
+
@bytes = bytes
|
11
|
+
@key_iteration_count = key_iteration_count
|
12
|
+
end
|
13
|
+
|
14
|
+
def encryption_key username, password
|
15
|
+
Fetcher.make_key username, password, key_iteration_count
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -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 Chunk
|
6
|
+
attr_reader :id,
|
7
|
+
:payload
|
8
|
+
|
9
|
+
def initialize id, payload
|
10
|
+
@id = id
|
11
|
+
@payload = payload
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,33 @@
|
|
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
|
+
# Base class for all errors, should not be raised
|
6
|
+
class Error < StandardError; end
|
7
|
+
|
8
|
+
#
|
9
|
+
# Generic errors
|
10
|
+
#
|
11
|
+
|
12
|
+
# Something went wrong with the network
|
13
|
+
class NetworkError < Error; end
|
14
|
+
|
15
|
+
# Server responded with something we don't understand
|
16
|
+
class InvalidResponse < Error; end
|
17
|
+
|
18
|
+
# Server responded with XML we don't understand
|
19
|
+
class UnknownResponseSchema < Error; end
|
20
|
+
|
21
|
+
#
|
22
|
+
# LastPass returned errors
|
23
|
+
#
|
24
|
+
|
25
|
+
# LastPass error: unknown username
|
26
|
+
class LastPassUnknownUsername < Error; end
|
27
|
+
|
28
|
+
# LastPass error: invalid password
|
29
|
+
class LastPassInvalidPassword < Error; end
|
30
|
+
|
31
|
+
# LastPass error we don't know about
|
32
|
+
class LastPassUnknownError < Error; end
|
33
|
+
end
|
@@ -0,0 +1,125 @@
|
|
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 Fetcher
|
6
|
+
def self.login username, password
|
7
|
+
key_iteration_count = request_iteration_count username
|
8
|
+
request_login username, password, key_iteration_count
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.fetch session, web_client = HTTParty
|
12
|
+
response = web_client.get "https://lastpass.com/getaccts.php?mobile=1&b64=1&hash=0.0",
|
13
|
+
format: :plain,
|
14
|
+
cookies: {"PHPSESSID" => URI.encode(session.id)}
|
15
|
+
|
16
|
+
raise NetworkError unless response.response.is_a? Net::HTTPOK
|
17
|
+
|
18
|
+
Blob.new decode_blob(response.parsed_response), session.key_iteration_count
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.request_iteration_count username, web_client = HTTParty
|
22
|
+
response = web_client.post "https://lastpass.com/iterations.php",
|
23
|
+
query: {email: username}
|
24
|
+
|
25
|
+
raise NetworkError unless response.response.is_a? Net::HTTPOK
|
26
|
+
|
27
|
+
begin
|
28
|
+
count = Integer response.parsed_response
|
29
|
+
rescue ArgumentError
|
30
|
+
raise InvalidResponse, "Key iteration count is invalid"
|
31
|
+
end
|
32
|
+
|
33
|
+
raise InvalidResponse, "Key iteration count is not positive" unless count > 0
|
34
|
+
|
35
|
+
count
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.request_login username, password, key_iteration_count, web_client = HTTParty
|
39
|
+
response = web_client.post "https://lastpass.com/login.php",
|
40
|
+
format: :xml,
|
41
|
+
body: {
|
42
|
+
method: "mobile",
|
43
|
+
web: 1,
|
44
|
+
xml: 1,
|
45
|
+
username: username,
|
46
|
+
hash: make_hash(username, password, key_iteration_count),
|
47
|
+
iterations: key_iteration_count
|
48
|
+
}
|
49
|
+
|
50
|
+
raise NetworkError unless response.response.is_a? Net::HTTPOK
|
51
|
+
|
52
|
+
parsed_response = response.parsed_response
|
53
|
+
raise InvalidResponse unless parsed_response.is_a? Hash
|
54
|
+
|
55
|
+
create_session parsed_response, key_iteration_count or
|
56
|
+
raise login_error parsed_response
|
57
|
+
end
|
58
|
+
|
59
|
+
def self.create_session parsed_response, key_iteration_count
|
60
|
+
ok = parsed_response["ok"]
|
61
|
+
if ok.is_a? Hash
|
62
|
+
session_id = ok["sessionid"]
|
63
|
+
if session_id.is_a? String
|
64
|
+
return Session.new session_id, key_iteration_count
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
nil
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.login_error parsed_response
|
72
|
+
error = (parsed_response["response"] || {})["error"]
|
73
|
+
return UnknownResponseSchema unless error.is_a? Hash
|
74
|
+
|
75
|
+
exceptions = {
|
76
|
+
"unknownemail" => LastPassUnknownUsername,
|
77
|
+
"unknownpassword" => LastPassInvalidPassword,
|
78
|
+
}
|
79
|
+
|
80
|
+
cause = error["cause"]
|
81
|
+
message = error["message"]
|
82
|
+
|
83
|
+
if cause
|
84
|
+
(exceptions[cause] || LastPassUnknownError).new message || cause
|
85
|
+
else
|
86
|
+
InvalidResponse.new message
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.decode_blob blob
|
91
|
+
# TODO: Check for invalid base64
|
92
|
+
Base64.decode64 blob
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.make_key username, password, key_iteration_count
|
96
|
+
if key_iteration_count == 1
|
97
|
+
Digest::SHA256.digest username + password
|
98
|
+
else
|
99
|
+
PBKDF2
|
100
|
+
.new(password: password,
|
101
|
+
salt: username,
|
102
|
+
iterations: key_iteration_count,
|
103
|
+
key_length: 32)
|
104
|
+
.bin_string
|
105
|
+
.force_encoding "BINARY"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.make_hash username, password, key_iteration_count
|
110
|
+
if key_iteration_count == 1
|
111
|
+
Digest::SHA256.hexdigest Digest.hexencode(make_key(username, password, 1)) + password
|
112
|
+
else
|
113
|
+
PBKDF2
|
114
|
+
.new(password: make_key(username, password, key_iteration_count),
|
115
|
+
salt: password,
|
116
|
+
iterations: 1,
|
117
|
+
key_length: 32)
|
118
|
+
.hex_string
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Can't instantiate Fetcher
|
123
|
+
private_class_method :new
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,184 @@
|
|
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 Parser
|
6
|
+
# Splits the blob into chucks grouped by kind.
|
7
|
+
def self.extract_chunks blob
|
8
|
+
chunks = Hash.new { |hash, key| hash[key] = [] }
|
9
|
+
|
10
|
+
StringIO.open blob.bytes do |stream|
|
11
|
+
while !stream.eof?
|
12
|
+
chunk = read_chunk stream
|
13
|
+
chunks[chunk.id] << chunk
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
chunks
|
18
|
+
end
|
19
|
+
|
20
|
+
# Parses an account chunk, decrypts and creates an Account object.
|
21
|
+
# TODO: See if this should be part of Account class.
|
22
|
+
def self.parse_account chunk, encryption_key
|
23
|
+
StringIO.open chunk.payload do |io|
|
24
|
+
id = read_item io
|
25
|
+
name = decode_aes256_auto read_item(io), encryption_key
|
26
|
+
group = decode_aes256_auto read_item(io), encryption_key
|
27
|
+
url = decode_hex read_item io
|
28
|
+
3.times { skip_item io }
|
29
|
+
username = decode_aes256_auto read_item(io), encryption_key
|
30
|
+
password = decode_aes256_auto read_item(io), encryption_key
|
31
|
+
|
32
|
+
Account.new id, name, username, password, url, group
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Reads one chunk from a stream and creates a Chunk object with the data read.
|
37
|
+
def self.read_chunk stream
|
38
|
+
# LastPass blob chunk is made up of 4-byte ID,
|
39
|
+
# big endian 4-byte size and payload of that size.
|
40
|
+
#
|
41
|
+
# Example:
|
42
|
+
# 0000: "IDID"
|
43
|
+
# 0004: 4
|
44
|
+
# 0008: 0xDE 0xAD 0xBE 0xEF
|
45
|
+
# 000C: --- Next chunk ---
|
46
|
+
Chunk.new read_id(stream), read_payload(stream, read_size(stream))
|
47
|
+
end
|
48
|
+
|
49
|
+
# Reads an item from a stream and returns it as a string of bytes.
|
50
|
+
def self.read_item stream
|
51
|
+
# An item in an itemized chunk is made up of the
|
52
|
+
# big endian size and the payload of that size.
|
53
|
+
#
|
54
|
+
# Example:
|
55
|
+
# 0000: 4
|
56
|
+
# 0004: 0xDE 0xAD 0xBE 0xEF
|
57
|
+
# 0008: --- Next item ---
|
58
|
+
read_payload stream, read_size(stream)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Skips an item in a stream.
|
62
|
+
def self.skip_item stream
|
63
|
+
read_item stream
|
64
|
+
end
|
65
|
+
|
66
|
+
# Reads a chunk ID from a stream.
|
67
|
+
def self.read_id stream
|
68
|
+
stream.read 4
|
69
|
+
end
|
70
|
+
|
71
|
+
# Reads a chunk or an item ID.
|
72
|
+
def self.read_size stream
|
73
|
+
read_uint32 stream
|
74
|
+
end
|
75
|
+
|
76
|
+
# Reads a payload of a given size from a stream.
|
77
|
+
def self.read_payload stream, size
|
78
|
+
stream.read size
|
79
|
+
end
|
80
|
+
|
81
|
+
# Reads an unsigned 32 bit integer from a stream.
|
82
|
+
def self.read_uint32 stream
|
83
|
+
stream.read(4).unpack("N").first
|
84
|
+
end
|
85
|
+
|
86
|
+
# Decodes a hex encoded string into raw bytes.
|
87
|
+
def self.decode_hex data
|
88
|
+
raise ArgumentError, "Input length must be multple of 2" unless data.size % 2 == 0
|
89
|
+
raise ArgumentError, "Input contains invalid characters" unless data =~ /^[0-9a-f]*$/i
|
90
|
+
|
91
|
+
data.scan(/../).map { |i| i.to_i 16 }.pack "c*"
|
92
|
+
end
|
93
|
+
|
94
|
+
# Decodes a base64 encoded string into raw bytes.
|
95
|
+
def self.decode_base64 data
|
96
|
+
# TODO: Check for input validity!
|
97
|
+
Base64.decode64 data
|
98
|
+
end
|
99
|
+
|
100
|
+
# Guesses AES encoding/cipher from the length of the data.
|
101
|
+
# Possible combinations are:
|
102
|
+
# - ciphers: AES-256 EBC, AES-256 CBC
|
103
|
+
# - encodings: plain, base64
|
104
|
+
def self.decode_aes256_auto data, encryption_key
|
105
|
+
length = data.length
|
106
|
+
length16 = length % 16
|
107
|
+
length64 = length % 64
|
108
|
+
|
109
|
+
if length == 0
|
110
|
+
""
|
111
|
+
elsif length16 == 0
|
112
|
+
decode_aes256_ecb_plain data, encryption_key
|
113
|
+
elsif length64 == 0 || length64 == 24 || length64 == 44
|
114
|
+
decode_aes256_ecb_base64 data, encryption_key
|
115
|
+
elsif length16 == 1
|
116
|
+
decode_aes256_cbc_plain data, encryption_key
|
117
|
+
elsif length64 == 6 || length64 == 26 || length64 == 50
|
118
|
+
decode_aes256_cbc_base64 data, encryption_key
|
119
|
+
else
|
120
|
+
raise RuntimeError, "'#{data.inspect}' doesn't seem to be AES-256 encrypted"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Decrypts AES-256 ECB bytes.
|
125
|
+
def self.decode_aes256_ecb_plain data, encryption_key
|
126
|
+
if data.empty?
|
127
|
+
""
|
128
|
+
else
|
129
|
+
decode_aes256 :ecb, "", data, encryption_key
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
# Decrypts base64 encoded AES-256 ECB bytes.
|
134
|
+
def self.decode_aes256_ecb_base64 data, encryption_key
|
135
|
+
decode_aes256_ecb_plain decode_base64(data), encryption_key
|
136
|
+
end
|
137
|
+
|
138
|
+
# Decrypts AES-256 CBC bytes.
|
139
|
+
def self.decode_aes256_cbc_plain data, encryption_key
|
140
|
+
if data.empty?
|
141
|
+
""
|
142
|
+
else
|
143
|
+
# LastPass AES-256/CBC encryted string starts with an "!".
|
144
|
+
# Next 16 bytes are the IV for the cipher.
|
145
|
+
# And the rest is the encrypted payload.
|
146
|
+
|
147
|
+
# TODO: Check for input validity!
|
148
|
+
decode_aes256 :cbc,
|
149
|
+
data[1, 16],
|
150
|
+
data[17..-1],
|
151
|
+
encryption_key
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Decrypts base64 encoded AES-256 CBC bytes.
|
156
|
+
def self.decode_aes256_cbc_base64 data, encryption_key
|
157
|
+
if data.empty?
|
158
|
+
""
|
159
|
+
else
|
160
|
+
# LastPass AES-256/CBC/base64 encryted string starts with an "!".
|
161
|
+
# Next 24 bytes are the base64 encoded IV for the cipher.
|
162
|
+
# Then comes the "|".
|
163
|
+
# And the rest is the base64 encoded encrypted payload.
|
164
|
+
|
165
|
+
# TODO: Check for input validity!
|
166
|
+
decode_aes256 :cbc,
|
167
|
+
decode_base64(data[1, 24]),
|
168
|
+
decode_base64(data[26..-1]),
|
169
|
+
encryption_key
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Decrypt AES-256 bytes.
|
174
|
+
# Allowed ciphers are: :ecb, :cbc.
|
175
|
+
# If for :ecb iv is not used and should be set to "".
|
176
|
+
def self.decode_aes256 cipher, iv, data, encryption_key
|
177
|
+
aes = OpenSSL::Cipher::Cipher.new "aes-256-#{cipher}"
|
178
|
+
aes.decrypt
|
179
|
+
aes.key = encryption_key
|
180
|
+
aes.iv = iv
|
181
|
+
aes.update(data) + aes.final
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|