kasefet 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +4 -0
- data/DESIGN.md +73 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/PHILOSOPHY.md +38 -0
- data/README.md +59 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/exe/kasefet +6 -0
- data/kasefet.gemspec +26 -0
- data/lib/kasefet/cli.rb +94 -0
- data/lib/kasefet/config.rb +81 -0
- data/lib/kasefet/encrypted_flat_kv.rb +64 -0
- data/lib/kasefet/flat_kv.rb +52 -0
- data/lib/kasefet/master_key.rb +94 -0
- data/lib/kasefet/version.rb +3 -0
- data/lib/kasefet/wallet.rb +48 -0
- data/lib/kasefet.rb +5 -0
- metadata +121 -0
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
data/.travis.yml
ADDED
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
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
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
data/exe/kasefet
ADDED
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
|
data/lib/kasefet/cli.rb
ADDED
@@ -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,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
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: []
|