1pass 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/1pass +37 -0
- data/lib/1pass.rb +20 -0
- data/lib/1pass/version.rb +3 -0
- data/lib/content.rb +19 -0
- data/lib/decrypt.rb +53 -0
- data/lib/encryption_key.rb +32 -0
- data/lib/key.rb +44 -0
- data/lib/keychain.rb +39 -0
- metadata +93 -0
data/bin/1pass
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require '1pass'
|
4
|
+
require 'optparse'
|
5
|
+
require 'ostruct'
|
6
|
+
require 'highline/import'
|
7
|
+
|
8
|
+
options = OpenStruct.new
|
9
|
+
|
10
|
+
opts = OptionParser.new do |opts|
|
11
|
+
opts.banner = "Usage: 1pass [options]"
|
12
|
+
|
13
|
+
opts.on("-l", "--list", "List all keychain entries") do |l|
|
14
|
+
options.list = true
|
15
|
+
end
|
16
|
+
|
17
|
+
opts.on("-k", "--key [key-name]", "Get details for given key name") do |k|
|
18
|
+
options.key = k
|
19
|
+
end
|
20
|
+
|
21
|
+
opts.on("-f", "--field [field-name]", "Get value for given field. Key should also be specified with -k or --key") do |f|
|
22
|
+
options.field = f
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
opts.parse!(ARGV)
|
27
|
+
|
28
|
+
agile_keychain = AgileKeychain.new
|
29
|
+
|
30
|
+
if(options.list)
|
31
|
+
agile_keychain.list
|
32
|
+
elsif(options.key)
|
33
|
+
master_password = ask("Enter your master password: ") { |q| q.echo = "*" }
|
34
|
+
agile_keychain.load(master_password, options.key, options.field)
|
35
|
+
else
|
36
|
+
puts opts
|
37
|
+
end
|
data/lib/1pass.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require "1pass/version"
|
2
|
+
require "keychain"
|
3
|
+
|
4
|
+
class AgileKeychain
|
5
|
+
def initialize(path=nil)
|
6
|
+
path = path || "#{ENV["HOME"]}/Library/Application Support/1Password/1Password.agilekeychain"
|
7
|
+
@keychain = Keychain.new(path)
|
8
|
+
end
|
9
|
+
|
10
|
+
def list
|
11
|
+
@keychain.content.items.map {|i| puts i.name}
|
12
|
+
end
|
13
|
+
|
14
|
+
def load(master_password, key_name, field_name=nil)
|
15
|
+
@keychain.unlock(master_password)
|
16
|
+
key = @keychain.get(key_name)
|
17
|
+
return unless key
|
18
|
+
puts field_name ? key.find(field_name) : key.fields
|
19
|
+
end
|
20
|
+
end
|
data/lib/content.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
class Content
|
2
|
+
attr_reader :items
|
3
|
+
|
4
|
+
def initialize(data)
|
5
|
+
@items = data.map {|item| ContentItem.new item}
|
6
|
+
end
|
7
|
+
|
8
|
+
def find(name)
|
9
|
+
@items.select {|i| i.name == name}.first
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
class ContentItem
|
14
|
+
attr_reader :uuid, :name, :type
|
15
|
+
|
16
|
+
def initialize(item)
|
17
|
+
@uuid, @type, @name, @url, @timestamp, @folder, @strength, @trashed = item
|
18
|
+
end
|
19
|
+
end
|
data/lib/decrypt.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
|
3
|
+
class Decrypt
|
4
|
+
def self.base64_decode(base64_encoded_string)
|
5
|
+
decoded_data = base64_encoded_string.unpack('m')[0]
|
6
|
+
salt = decoded_data[8..15]
|
7
|
+
data = decoded_data[16..decoded_data.length-1]
|
8
|
+
{salt: salt, data: data}
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.aes_decrypt(data, key, iv)
|
12
|
+
decipher = OpenSSL::Cipher::AES.new(128, :CBC)
|
13
|
+
decipher.decrypt
|
14
|
+
decipher.key = key
|
15
|
+
decipher.iv = iv
|
16
|
+
begin
|
17
|
+
plain = decipher.update(data)
|
18
|
+
plain << decipher.final
|
19
|
+
rescue OpenSSL::Cipher::CipherError
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.derive_pbkdf2(password, salt, iterations)
|
25
|
+
key_and_iv = OpenSSL::PKCS5.pbkdf2_hmac_sha1(password, salt, iterations, 32)
|
26
|
+
{key: key_and_iv[0,16], iv: key_and_iv[16..-1]}
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.derive_openssl(key, content_salt)
|
30
|
+
key = key[0,1024]
|
31
|
+
key_and_iv = ""
|
32
|
+
prev = ""
|
33
|
+
|
34
|
+
while key_and_iv.length < 32 do
|
35
|
+
prev = Digest::MD5.digest(prev + key + content_salt)
|
36
|
+
key_and_iv << prev
|
37
|
+
end
|
38
|
+
|
39
|
+
{key: key_and_iv[0,16], iv: key_and_iv[16..-1]}
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.decrypt_pbkdf2(master_password, data, iterations)
|
43
|
+
encrypted = base64_decode(data)
|
44
|
+
key_and_iv = derive_pbkdf2(master_password, encrypted[:salt], iterations)
|
45
|
+
aes_decrypt(encrypted[:data], key_and_iv[:key], key_and_iv[:iv])
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.decrypt_ssl(master_key, validation)
|
49
|
+
encrypted = base64_decode(validation)
|
50
|
+
key_and_iv = derive_openssl(master_key, encrypted[:salt])
|
51
|
+
aes_decrypt(encrypted[:data], key_and_iv[:key], key_and_iv[:iv])
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'decrypt'
|
2
|
+
|
3
|
+
class EncryptionKey
|
4
|
+
attr_reader :items
|
5
|
+
|
6
|
+
def initialize(data)
|
7
|
+
@items = data['list'].map {|k| EncryptionKeyItem.new k}
|
8
|
+
end
|
9
|
+
|
10
|
+
def unlock(password)
|
11
|
+
@items.collect {|ek| ek.unlock password}.all?
|
12
|
+
end
|
13
|
+
|
14
|
+
def get(identifier)
|
15
|
+
@items.select {|ek| ek.identifier == identifier}.first
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class EncryptionKeyItem
|
20
|
+
attr_reader :identifier, :decrypted_master_key
|
21
|
+
|
22
|
+
def initialize(hash_)
|
23
|
+
@identifier, @data, @validation, @iterations = hash_.values_at("identifier", "data", "validation", "iterations")
|
24
|
+
end
|
25
|
+
|
26
|
+
def unlock(password)
|
27
|
+
@decrypted_master_key = Decrypt.decrypt_pbkdf2(password, @data, @iterations)
|
28
|
+
return false unless @decrypted_master_key
|
29
|
+
validation_key = Decrypt.decrypt_ssl(@decrypted_master_key, @validation)
|
30
|
+
@decrypted_master_key == validation_key
|
31
|
+
end
|
32
|
+
end
|
data/lib/key.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
class Key
|
2
|
+
attr_reader :key_id, :encrypted
|
3
|
+
|
4
|
+
def types
|
5
|
+
{"webforms.WebForm" => WebForm,
|
6
|
+
"passwords.Password" => Password}
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(hash_)
|
10
|
+
@key_id, @encrypted, @type_name = hash_.values_at("keyID", "encrypted", "typeName")
|
11
|
+
end
|
12
|
+
|
13
|
+
def decrypt(encryption_key)
|
14
|
+
decrypted_master_key = encryption_key.get(@key_id).decrypted_master_key
|
15
|
+
return unless decrypted_master_key
|
16
|
+
decrypted_content = JSON.parse Decrypt.decrypt_ssl(decrypted_master_key, @encrypted)
|
17
|
+
types[@type_name].new decrypted_content
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class WebForm
|
22
|
+
attr_reader :fields
|
23
|
+
|
24
|
+
def initialize(hash_)
|
25
|
+
@notes_plain, @fields = hash_.values_at("notesPlain", "fields")
|
26
|
+
end
|
27
|
+
|
28
|
+
def find(name)
|
29
|
+
field = @fields.select {|f| f["name"] == name || f["designation"] == name}.first
|
30
|
+
field["value"]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class Password
|
35
|
+
attr_reader :fields
|
36
|
+
|
37
|
+
def find(name)
|
38
|
+
@fields[name]
|
39
|
+
end
|
40
|
+
|
41
|
+
def initialize(hash_)
|
42
|
+
@fields = hash_
|
43
|
+
end
|
44
|
+
end
|
data/lib/keychain.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'encryption_key'
|
3
|
+
require 'key'
|
4
|
+
require 'content'
|
5
|
+
|
6
|
+
class Keychain
|
7
|
+
attr_reader :encryption_key, :content
|
8
|
+
|
9
|
+
def initialize(path)
|
10
|
+
@path = path
|
11
|
+
load_encryption_keys
|
12
|
+
load_contents
|
13
|
+
end
|
14
|
+
|
15
|
+
def unlock(password)
|
16
|
+
@unlocked = @encryption_key.unlock(password)
|
17
|
+
end
|
18
|
+
|
19
|
+
def get(name)
|
20
|
+
item = @content.find(name)
|
21
|
+
return unless item
|
22
|
+
key = Key.new load_file(item.uuid + ".1password")
|
23
|
+
key.decrypt(@encryption_key)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
def load_encryption_keys
|
28
|
+
@encryption_key = EncryptionKey.new load_file("encryptionKeys.js")
|
29
|
+
end
|
30
|
+
|
31
|
+
def load_contents
|
32
|
+
@content = Content.new load_file("contents.js")
|
33
|
+
end
|
34
|
+
|
35
|
+
def load_file(file_name)
|
36
|
+
file = File.join(@path, "data", "default", file_name)
|
37
|
+
JSON.parse(IO.read(file))
|
38
|
+
end
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: 1pass
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Lokeshwaran
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-08-15 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '1.3'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '1.3'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rake
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
description: Command line client for AgileBits 1Password
|
47
|
+
email:
|
48
|
+
- dlokesh@gmail.com
|
49
|
+
executables:
|
50
|
+
- 1pass
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- lib/1pass.rb
|
55
|
+
- lib/1pass/version.rb
|
56
|
+
- lib/content.rb
|
57
|
+
- lib/decrypt.rb
|
58
|
+
- lib/encryption_key.rb
|
59
|
+
- lib/key.rb
|
60
|
+
- lib/keychain.rb
|
61
|
+
- bin/1pass
|
62
|
+
homepage: https://github.com/dlokesh/1pass
|
63
|
+
licenses:
|
64
|
+
- MIT
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ! '>='
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
segments:
|
76
|
+
- 0
|
77
|
+
hash: -2624167293769510500
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ! '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
segments:
|
85
|
+
- 0
|
86
|
+
hash: -2624167293769510500
|
87
|
+
requirements: []
|
88
|
+
rubyforge_project:
|
89
|
+
rubygems_version: 1.8.25
|
90
|
+
signing_key:
|
91
|
+
specification_version: 3
|
92
|
+
summary: 1pass-0.1.0
|
93
|
+
test_files: []
|