kasefet 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ef83c9b70e27855bb629f577ac6fd37de8b5477d
4
+ data.tar.gz: b2b865ac6e19bcb17cc5143c5c3f0e0b7554f3e2
5
+ SHA512:
6
+ metadata.gz: ddd53fc18dd123c69c9b4113765b72c06d0240eede4efb976b10a7dec3b7886d2aa00ae2b9d1f7c6a90f9dfb2f032c853246bcb574255f8daff075edf3771182
7
+ data.tar.gz: 813b83d332a7bd0817d746978811ace68885ddfe9bf4d897f85a71871d1edf439b914a39425f563c77c8c6650129e232eb54226802e5c15e5d488d671afbf380
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.0
4
+ before_install: gem install bundler -v 1.10.6
data/DESIGN.md ADDED
@@ -0,0 +1,73 @@
1
+ # Key-value flat file system
2
+
3
+ We reuse this concept in several places, so here's the generic concept. I need to design a system of files that will survive a naive synchronization program like rsync, syncthing, or dropbox. The basic idea is to namespace everything by hexdigest (inspired by git). We need to go a step further, and allow multiple values to not overwrite each other. The way to do this is to mark each value with the timestamp it was created and the source of the value. For example, this means the key `kasefet.name_salt` would have values stored in the directory `db/c39d7d6b239c2e20a31672ed979fed2b45c88748d06ba3be6bff85767b5d3d`, like this:
4
+
5
+ ```
6
+ root
7
+ |--db
8
+ |--c39d7d6b239c2e20a31672ed979fed2b45c88748d06ba3be6bff85767b5d3d
9
+ |--20160224.184012.laptop
10
+ |--20160223.140554.phone
11
+ ```
12
+
13
+ Device names can either be configured locally, or set via stable digest from the uname, or just use the hostname (may cause issues in VM images). The accuracy of the name can be configured (pid, tid), but should be stable throughout the lifetime of a system.
14
+
15
+ To select the value for a given key, just look for the last file in the directory (lexicographically). If the format YYYYMMDD-HHMMSS.device.extension is followed, then there should only be conflicts if two devices edited the value at the exact same time. In such a case, the device name itself is used as a tie breaker. Additional precision may be used, but the level of precision should not change throughout the lifetime of a system.
16
+
17
+ This is stable enough for most purposes (and well within my use cases, assuming clock drift is below change velocity).
18
+
19
+ I leave it as an exercise to the sync program to correctly identify deleted values, although keeping the history of a value is typically preferred. Please note that editing a file directly should NEVER happen. Once a file is written, it should never change (This can be enforced with a digest of the content in the filename).
20
+
21
+ ## Encryption
22
+
23
+ When encrypting files in such a format, I like to keep them prefixed to the encrypted content and then the auth tag. Note that the first version only supports AES-256-GCM.
24
+
25
+ # File format
26
+
27
+ A kasefet wallet has the following layout:
28
+
29
+ ```
30
+ wallet
31
+ |--key
32
+ |--index
33
+ | |--index
34
+ |--metadata (flat file kv directory)
35
+ |--ksft (flat file kv directory)
36
+ ```
37
+
38
+ ## key file
39
+
40
+ The key file contains the master encryption key for the wallet. This key is used to encrypt the flatfile kv values. When encrypted, the keyfile has this format:
41
+
42
+ ```
43
+ +------------------------------------------------------+
44
+ | pbkdf2 salt (if any) | iv | auth_tag | encrypted key |
45
+ +------------------------------------------------------+
46
+ ```
47
+
48
+ ### Rotating credentials
49
+
50
+ When rotating credentials, it is recommended to simply reencrypt the keyfile with a new passphrase, and ensure consensus.
51
+
52
+ ## index directory
53
+
54
+ The index directory holds a single file, which is the index of logical key names to physical key names. If more than one file is in this directory, it's a strong hint that the index needs to be rebuilt (sync programs that stick extra files everywhere are a plague on mankind)
55
+
56
+ ## metadata directory
57
+
58
+ The metadata file contains wallet-level settings (optionally with an extension if not a simple key=value format file).
59
+
60
+ Should be encrypted. The standard way to test if a password is correct is to attempt to decrypt and parse this file, looking for the kasefet.wallet_version key.
61
+
62
+ Required keys are:
63
+
64
+ ```
65
+ kasefet.wallet_version
66
+ kasefet.name_salt
67
+ ```
68
+
69
+ ## ksft directory
70
+
71
+ Key names shouldn't be exposed, so we salt the keyname before pushing it into the flatfile kv driver. I'm on the fence if values should be moved when a key is renamed, or if the key should stay constant (maintained through an index file that knows how to map keys to digests)
72
+
73
+ The content of these files varies, and I'm not sure what format I'll use. It'll be more or less free text, that much I do know.
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in kasefet.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2016 Steven Karas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all 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,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/PHILOSOPHY.md ADDED
@@ -0,0 +1,38 @@
1
+ # The name
2
+
3
+ Kasefet is the hebrew word for "safe", (the locked box meaning of safe). Seemed as good as any, and wasn't taken on rubygems
4
+
5
+ # A bit of history
6
+
7
+ In the beginning, I used Keepass as part of PortableApps. I found quickly that this wasn't ideal, since it didn't work easily on Ubuntu (just because it doesn't crash, doesn't mean it's good). At the time, I thought it would be enough to simply leave the database in a shared mount between the two (I was dual booting for a while). After that, I put the database on Dropbox and started to access it from my phone. That didn't go over well. Dropbox would sometimes update and move the file out from underneath the android app, and cause other issues. Worse, it would sometimes do this after I had already added new credentials to the database from my phone (leaving me with conflicted copies at best, or sometimes just making them "disappear").
8
+
9
+ After a while, I came to the conclusion that the file format just wasn't good, and that there had to be a better way to do it, but I also took stock of how much time it would take me to do this, and decided against it.
10
+
11
+ The final impetus for me to start this project came when I started working on a new side project, but didn't want to mix credentials from different scopes into the same database. Worse, I wanted to share a portion of my password wallet, but not all of it. All of this pointed towards the need to "layer" multiple wallets together.
12
+
13
+ # Goals for Kasefet
14
+
15
+ - System integration
16
+ - autotype
17
+ - clipboard
18
+ - Dumb sync
19
+ - Flat files make this possible
20
+ - Support for using multiple wallets simultaneously
21
+ - Best practice encryption
22
+ - CLI and GUI interfaces
23
+ - Cross platform support (Ubuntu, Mac, and Android, because that's what I use)
24
+ - API access (as in, run a server with the files, and access the credentials via the api)
25
+
26
+ It's a big project, and I doubt I'll get very far on my own, but like I've always said: "Aim for the sky, and hit the tree"
27
+
28
+ # Personal time
29
+
30
+ I don't have a ton of time to dedicate to this project, so here are my weekend goals (I only have a single day on the weekend to give, and maybe not even that):
31
+
32
+ 1. gem structure + edit files
33
+ 2. encryption
34
+ 3. layer multiple wallets
35
+ 4. clipboard integration (mac)
36
+ 5. clipboard (ubuntu)
37
+ 6. autotype (mac)
38
+ 7. autotype (ubuntu)
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # Kasefet
2
+
3
+ Kasefet is a password wallet built around flat files.
4
+
5
+ NOTE: This version of kasefet is not meant to be highly secure. It is a reference implementation
6
+
7
+ ## Current Status
8
+
9
+ Kasefet is currently a work in progress. This is the todo list for version 1.0, and is based entirely on my own requirements:
10
+
11
+ - [x] Flat file wallet
12
+ - [x] SSL encrypted wallet
13
+ - [ ] support for multiple wallets
14
+ - [x] editor integration
15
+ - [ ] clipboard integration (Mac)
16
+ - [ ] autotype (Mac)
17
+ - [ ] clipboard integration (Ubuntu)
18
+ - [ ] autotype (Ubuntu)
19
+
20
+ ## Installation
21
+
22
+ ```
23
+ $ gem install kasefet
24
+ ```
25
+
26
+ ## Usage
27
+
28
+ ```
29
+ # Create a simple record of username:password under the name "Gmail"
30
+ $ kasefet add Gmail example@example.com:sekret
31
+
32
+ # Open the record for Facebook in an editor
33
+ $ kasefet edit Facebook
34
+
35
+ # Print the contents of the Gmail credential file to STDOUT
36
+ $ kasefet show Gmail
37
+ ```
38
+
39
+ ```
40
+ # Create a new wallet (default format is encrypted)
41
+ $ kasefet new --format=plain ~/my-new-wallet
42
+
43
+ # Add another wallet to search through
44
+ $ kasefet addwallet ~/my-new-wallet
45
+ ```
46
+
47
+ ## Development
48
+
49
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. Run `bundle exec kasefet` to use the gem in this directory, ignoring other installed copies of this gem.
50
+
51
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
52
+
53
+ ## Contributing
54
+
55
+ Bug reports and pull requests are welcome on GitHub at https://github.com/stevenkaras/kasefet.
56
+
57
+ ## License
58
+
59
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "kasefet"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/exe/kasefet ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
4
+ require "kasefet/cli"
5
+
6
+ Kasefet::CLI.new.start
data/kasefet.gemspec ADDED
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'kasefet/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "kasefet"
8
+ spec.version = Kasefet::VERSION
9
+ spec.authors = ["Steven Karas"]
10
+ spec.email = ["steven.karas@gmail.com"]
11
+
12
+ spec.summary = %q{flat file-oriented password wallet}
13
+ spec.description = %q{Kasefet is a password wallet built around flat files}
14
+ spec.homepage = "https://github.com/stevenkaras/kasefet"
15
+ spec.license = "MIT"
16
+
17
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ spec.bindir = "exe"
19
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20
+ spec.require_paths = ["lib"]
21
+
22
+ spec.add_development_dependency "bundler", "~> 1.10"
23
+ spec.add_development_dependency "rake", "~> 10.0"
24
+ spec.add_development_dependency "thunder", "~> 0.7"
25
+ spec.add_development_dependency "minitest"
26
+ end
@@ -0,0 +1,94 @@
1
+ require "thunder"
2
+
3
+ require "kasefet/config"
4
+ require "kasefet/wallet"
5
+
6
+ class Kasefet
7
+ class CLI
8
+ GlobalConfigLocations = [
9
+ "~/.kasefet",
10
+ "~/.config/kasefet",
11
+ ]
12
+
13
+ DefaultWalletLocation = "~/.wallet"
14
+
15
+ include Thunder
16
+
17
+ def load_config(options)
18
+ config_file = options[:config]
19
+ config_file ||= GlobalConfigLocations.find { |file| File.exist?(File.expand_path(file)) }
20
+ config_file ||= File.expand_path(GlobalConfigLocations.first)
21
+ @config = Kasefet::Config.new(config_file)
22
+ end
23
+
24
+ def load_wallet
25
+ wallet_dir = @config["wallet"]
26
+ wallet_dir = @config["wallet"] = File.expand_path(DefaultWalletLocation) unless wallet_dir
27
+ @wallet = Kasefet::Wallet.new(directory: wallet_dir)
28
+ end
29
+
30
+ def determine_editor
31
+ [
32
+ ENV["VISUAL"],
33
+ ENV["EDITOR"],
34
+ "vi",
35
+ "nano",
36
+ "ed"
37
+ ].find do |editor|
38
+ next unless editor
39
+ # TODO: do some cursory checks to determine if this editor exists
40
+ true
41
+ end
42
+ end
43
+
44
+ desc "edit KEYNAME", "open the contents of KEYNAME in an editor, and save the changes"
45
+ def edit(keyname, *content, **options)
46
+ load_config(options)
47
+ load_wallet
48
+
49
+ require 'tmpdir'
50
+ require 'pathname'
51
+ Dir.mktmpdir do |tmpdir|
52
+ tmpdir = Pathname.new(tmpdir)
53
+ content = @wallet.load(keyname)
54
+ File.binwrite(tmpdir + keyname, content)
55
+
56
+ # invoke the editor
57
+ editor = determine_editor()
58
+ result = system(*editor.split(" "), (tmpdir + keyname).to_s)
59
+
60
+ if result.nil?
61
+ puts "Failed to launch editor: `#{editor}`"
62
+ elsif !result
63
+ puts "`#{editor}` exited with error status: #{$?}. Not saving contents"
64
+ else
65
+ new_content = File.binread(tmpdir + keyname)
66
+ @wallet.store(keyname, new_content)
67
+ end
68
+ end
69
+ end
70
+
71
+ desc "add KEYNAME CONTENTS...", "store the given CONTENTS in KEYNAME"
72
+ def add(keyname, *content, **options)
73
+ load_config(options)
74
+ load_wallet
75
+
76
+ content = content.join(" ")
77
+
78
+ @wallet.store(keyname, content)
79
+
80
+ return content
81
+ end
82
+
83
+ desc "show KEYNAME", "print the contents of KEYNAME to stdout"
84
+ def show(keyname, **options)
85
+ load_config(options)
86
+ load_wallet
87
+
88
+ content = @wallet.load(keyname)
89
+
90
+ puts content
91
+ return content
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,81 @@
1
+ class Kasefet
2
+ class Config
3
+ def initialize(file)
4
+ @file = file
5
+ @settings = {}
6
+ return unless File.exists?(file)
7
+ load
8
+ end
9
+
10
+ attr_accessor :file
11
+
12
+ def [](key)
13
+ return key.split(".").reduce(@settings) { |hash, segment| hash ? hash[segment] : nil }
14
+ end
15
+
16
+ def []=(key, value)
17
+ last_segment = key.split(".")[0..-2].reduce(@settings) { |hash, segment| hash[segment] ||= {} }
18
+ last_segment[key.split(".")[-1]] = value
19
+ end
20
+
21
+ def load
22
+ case File.extname(@file)
23
+ when ".yaml", ".yml"
24
+ require 'yaml'
25
+ @settings = YAML.load(File.read(file))
26
+ when ".json"
27
+ require 'json'
28
+ @settings = JSON.parse(File.read(file))
29
+ else
30
+ # try the key=value format
31
+ content = File.read(file)
32
+ content.split("\n").each do |line|
33
+ key, value = line.split("=")
34
+ value = value.to_i if value =~ /\d+/
35
+ if self[key]
36
+ self[key] = [self[key]] unless self[key].is_a? Array
37
+ self[key] << value
38
+ else
39
+ self[key] = value
40
+ end
41
+ end
42
+ end
43
+ end
44
+
45
+ def save
46
+ case File.extname(@file)
47
+ when ".json"
48
+ require 'json'
49
+ File.write(@file, @settings.to_json)
50
+ when ".yaml", ".yml"
51
+ require 'yaml'
52
+ File.write(@file, @settings.to_yaml)
53
+ else
54
+ to_write = flatten_hash(@settings).map do |key, value|
55
+ if value.is_a? Array
56
+ value.map do |array_value|
57
+ "#{key}=#{array_value}"
58
+ end.join("\n")
59
+ else
60
+ "#{key}=#{value}"
61
+ end
62
+ end.join("\n")
63
+ File.write(@file, to_write)
64
+ end
65
+ end
66
+
67
+ def flatten_hash(hash)
68
+ hash.reduce({}) do |result, pair|
69
+ key, value = pair
70
+ if value.is_a? Hash
71
+ flatten_hash(value).each do |suffix, subvalue|
72
+ result["#{key}.#{suffix}"] = subvalue
73
+ end
74
+ else
75
+ result[key] = value
76
+ end
77
+ result
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,64 @@
1
+ require 'socket'
2
+
3
+ require 'kasefet/flat_kv'
4
+
5
+ class Kasefet
6
+ # FlatKV where values are stored encrypted on the disk
7
+ class EncryptedFlatKV < Kasefet::FlatKV
8
+ CipherIVLength = 12
9
+ CipherAuthTagLength = 16
10
+
11
+ def initialize(cipher_key:, **options)
12
+ super(**options)
13
+ @cipher = OpenSSL::Cipher.new("aes-256-gcm")
14
+ @cipher_key = cipher_key
15
+ end
16
+
17
+ attr_accessor :cipher_key
18
+
19
+ def reencrypt_all_values!(new_key)
20
+ old_key = @cipher_key
21
+
22
+ Dir[@root + "*" + "*" + "*"].each do |encrypted_file|
23
+ old_encrypted_value = File.binread(encrypted_file)
24
+ value = decrypt_value(old_encrypted_value, old_key)
25
+ new_encrypted_value = encrypt_value(value, new_key)
26
+ File.binwrite(encrypted_file, new_encrypted_value)
27
+ end
28
+ @cipher_key = new_key
29
+ end
30
+
31
+ def encrypt_value(value, cipher_key)
32
+ @cipher.encrypt
33
+ @cipher.key = cipher_key
34
+ iv = @cipher.random_iv
35
+ @cipher.iv = iv
36
+ @cipher.auth_data = ""
37
+ encrypted_value = @cipher.update(value) + @cipher.final
38
+ encrypted_value = @cipher.auth_tag + encrypted_value
39
+ encrypted_value = iv + encrypted_value
40
+ return encrypted_value
41
+ end
42
+
43
+ def decrypt_value(encrypted_value, cipher_key)
44
+ @cipher.decrypt
45
+ @cipher.key = cipher_key
46
+ @cipher.iv = encrypted_value[0...CipherIVLength]
47
+ encrypted_value = encrypted_value[CipherIVLength..-1]
48
+ @cipher.auth_tag = encrypted_value[0...CipherAuthTagLength]
49
+ encrypted_value = encrypted_value[CipherAuthTagLength..-1]
50
+ value = @cipher.update(encrypted_value) + @cipher.final
51
+ return value
52
+ end
53
+
54
+ def [](key)
55
+ encrypted_value = super(key)
56
+ return nil if encrypted_value.nil?
57
+ return decrypt_value(encrypted_value, @cipher_key)
58
+ end
59
+
60
+ def []=(key, value)
61
+ super(key, encrypt_value(value, @cipher_key))
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,52 @@
1
+ require 'openssl'
2
+ require 'pathname'
3
+ require 'socket'
4
+ require 'fileutils'
5
+
6
+ class Kasefet
7
+ # Flat file key value storage
8
+ #
9
+ # Flat-file based key-value storage engine that is designed to be compatible with naive sync programs
10
+ class FlatKV
11
+ # @option [String] :root The root directory of the FlatKV store
12
+ # @option [String] :device_name The device name to use to identify the writer
13
+ # @option [String] :extension The default extension to use for the value files
14
+ def initialize(root:, device_name: Socket.gethostname, extension: "")
15
+ @root = Pathname.new(root)
16
+ @extension = extension
17
+ @device_name = device_name
18
+ end
19
+
20
+ attr_accessor :root, :extension, :device_name
21
+
22
+ def extension=(value)
23
+ value = ".#{value}" unless value.start_with?(".")
24
+ @extension = value
25
+ end
26
+
27
+ def [](key)
28
+ value_file = file_for_key(key)
29
+ return nil unless value_file
30
+ return File.binread(value_file)
31
+ end
32
+
33
+ def []=(key, value)
34
+ key_dir = dir_for_key(key)
35
+ FileUtils.mkdir_p(key_dir)
36
+ value_file_name = Time.now.strftime("%Y%m%d.%H%M%S%6N.") + @device_name + @extension
37
+ File.binwrite(key_dir + value_file_name, value)
38
+ end
39
+
40
+ def file_for_key(key)
41
+ key_dir = dir_for_key(key)
42
+ files = Dir.glob(key_dir + "*#{@extension}")
43
+ return nil if files.empty?
44
+ return files.last
45
+ end
46
+
47
+ def dir_for_key(key)
48
+ digest = OpenSSL::Digest::SHA256.hexdigest(key)
49
+ return @root + digest[0..1] + digest[2..-1]
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,94 @@
1
+ require "openssl"
2
+
3
+ class Kasefet
4
+ class MasterKey
5
+ PBKDF2SaltLength = 32
6
+ CipherKeyLength = 32
7
+ CipherIVLength = 12
8
+ CipherAuthTagLength = 16
9
+
10
+ def passphrase_to_key(passphrase, salt)
11
+ return OpenSSL::PKCS5.pbkdf2_hmac_sha1(passphrase, salt, 20000, CipherKeyLength)
12
+ end
13
+
14
+ def store_key_with_passphrase(passphrase)
15
+ cipher = OpenSSL::Cipher.new("aes-256-gcm")
16
+ salt = SecureRandom.random_bytes(PBKDF2SaltLength)
17
+ master_key = passphrase_to_key(passphrase, salt)
18
+ cipher.encrypt
19
+ cipher.iv = iv = cipher.random_iv
20
+ cipher.key = master_key
21
+ cipher.auth_data = ""
22
+ encrypted_key = cipher.update(@key) + cipher.final
23
+ encrypted_key = cipher.auth_tag + encrypted_key
24
+ encrypted_key = iv + encrypted_key
25
+ encrypted_key = salt + encrypted_key
26
+ File.binwrite(@file, encrypted_key)
27
+ end
28
+
29
+ def store_key_with_keyfile(keyfile)
30
+ cipher = OpenSSL::Cipher.new("aes-256-gcm")
31
+ master_key = File.binread(keyfile)
32
+ cipher.encrypt
33
+ cipher.iv = iv = cipher.random_iv
34
+ cipher.key = master_key
35
+ cipher.auth_data = ""
36
+ encrypted_key = cipher.update(@key) + cipher.final
37
+ encrypted_key = cipher.auth_tag + encrypted_key
38
+ encrypted_key = iv + encrypted_key
39
+ File.binwrite(@file, encrypted_key)
40
+ end
41
+
42
+ def load_key_with_keyfile(keyfile)
43
+ master_key = File.binread(keyfile)
44
+ cipher = OpenSSL::Cipher.new("aes-256-gcm")
45
+ cipher.decrypt
46
+ cipher.iv = @key[0...CipherIVLength]
47
+ @key = @key[CipherIVLength..-1]
48
+ cipher.auth_tag = @key[0...CipherAuthTagLength]
49
+ @key = @key[CipherAuthTagLength..-1]
50
+ cipher.key = master_key
51
+ @key = cipher.update(@key) + cipher.final
52
+ end
53
+
54
+ def load_key_with_passphrase(passphrase)
55
+ cipher = OpenSSL::Cipher.new("aes-256-gcm")
56
+ cipher.decrypt
57
+ salt = @key[0...PBKDF2SaltLength]
58
+ @key = @key[PBKDF2SaltLength..-1]
59
+ cipher.iv = @key[0...CipherIVLength]
60
+ @key = @key[CipherIVLength..-1]
61
+ cipher.auth_tag = @key[0...CipherAuthTagLength]
62
+ @key = @key[CipherAuthTagLength..-1]
63
+ cipher.key = passphrase_to_key(passphrase, salt)
64
+ @key = cipher.update(@key) + cipher.final
65
+ end
66
+
67
+ def initialize(file, passphrase: nil, keyfile: nil)
68
+ @file = file
69
+
70
+ # read in the key
71
+ if File.exist?(@file)
72
+ @key = File.binread(@file)
73
+ if passphrase
74
+ load_key_with_passphrase(passphrase)
75
+ elsif keyfile
76
+ load_key_with_keyfile(keyfile)
77
+ end
78
+ else
79
+ # this is a new wallet, generate a random key
80
+ cipher = OpenSSL::Cipher.new("aes-256-gcm")
81
+ @key = cipher.random_key
82
+ if passphrase
83
+ store_key_with_passphrase(passphrase)
84
+ elsif keyfile
85
+ store_key_with_keyfile(keyfile)
86
+ else
87
+ File.binwrite(@file, @key)
88
+ end
89
+ end
90
+ end
91
+
92
+ attr_accessor :key
93
+ end
94
+ end
@@ -0,0 +1,3 @@
1
+ class Kasefet
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,48 @@
1
+ require "fileutils"
2
+ require "pathname"
3
+ require "securerandom"
4
+ require "kasefet/encrypted_flat_kv"
5
+ require "kasefet/master_key"
6
+
7
+ class Kasefet
8
+ class Wallet
9
+ VERSION = 1
10
+ METADATA_DIR = "metadata"
11
+ CREDENTIALS_DIR = "ksft"
12
+ KSFT_SALT_LENGTH = 16
13
+
14
+ def initialize(directory:, passphrase: nil, keyfile: nil)
15
+ @root = Pathname.new(directory)
16
+ FileUtils.mkdir_p(@root)
17
+
18
+ @master_key = Kasefet::MasterKey.new(@root + "key")
19
+
20
+ @metadata = Kasefet::EncryptedFlatKV.new(root: @root + METADATA_DIR, cipher_key: @master_key.key)
21
+
22
+ @wallet_version = @metadata["kasefet.wallet_version"]
23
+ if @wallet_version.nil?
24
+ @wallet_version = @metadata["kasefet.wallet_version"] = VERSION.to_s
25
+ @metadata["kasefet.name_salt"] = SecureRandom.random_bytes(KSFT_SALT_LENGTH)
26
+ end
27
+ @wallet_version = @wallet_version.to_i
28
+ raise "Unknown Kasefet Wallet version: #{@wallet_version}" unless @wallet_version <= VERSION
29
+
30
+ @name_salt = @metadata["kasefet.name_salt"]
31
+ @credentials = Kasefet::EncryptedFlatKV.new(root: @root + CREDENTIALS_DIR, cipher_key: @master_key.key)
32
+ end
33
+
34
+ attr_accessor :root
35
+
36
+ def salted_keyname(name)
37
+ return "#{@name_salt}/#{name}/#{@name_salt}"
38
+ end
39
+
40
+ def load(name)
41
+ return @credentials[salted_keyname(name)]
42
+ end
43
+
44
+ def store(name, creds)
45
+ @credentials[salted_keyname(name)] = creds
46
+ end
47
+ end
48
+ end
data/lib/kasefet.rb ADDED
@@ -0,0 +1,5 @@
1
+ require "kasefet/version"
2
+
3
+ class Kasefet
4
+ # Your code goes here...
5
+ end
metadata ADDED
@@ -0,0 +1,121 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kasefet
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Steven Karas
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-03-07 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: thunder
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.7'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.7'
55
+ - !ruby/object:Gem::Dependency
56
+ name: minitest
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Kasefet is a password wallet built around flat files
70
+ email:
71
+ - steven.karas@gmail.com
72
+ executables:
73
+ - kasefet
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - ".travis.yml"
79
+ - DESIGN.md
80
+ - Gemfile
81
+ - LICENSE.txt
82
+ - PHILOSOPHY.md
83
+ - README.md
84
+ - Rakefile
85
+ - bin/console
86
+ - bin/setup
87
+ - exe/kasefet
88
+ - kasefet.gemspec
89
+ - lib/kasefet.rb
90
+ - lib/kasefet/cli.rb
91
+ - lib/kasefet/config.rb
92
+ - lib/kasefet/encrypted_flat_kv.rb
93
+ - lib/kasefet/flat_kv.rb
94
+ - lib/kasefet/master_key.rb
95
+ - lib/kasefet/version.rb
96
+ - lib/kasefet/wallet.rb
97
+ homepage: https://github.com/stevenkaras/kasefet
98
+ licenses:
99
+ - MIT
100
+ metadata: {}
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubyforge_project:
117
+ rubygems_version: 2.5.1
118
+ signing_key:
119
+ specification_version: 4
120
+ summary: flat file-oriented password wallet
121
+ test_files: []