1pass 0.1.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/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: []
|