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.
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module OnePass
2
+ VERSION = "0.1.0"
3
+ end
@@ -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
@@ -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
@@ -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
@@ -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: []