-
Notifications
You must be signed in to change notification settings - Fork 87
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 6ad4b61
Showing
9 changed files
with
1,041 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
Copyright (c) 2016, Cloudflare. All rights reserved. | ||
|
||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: | ||
|
||
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. | ||
|
||
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. | ||
|
||
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. | ||
|
||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# gokey | ||
|
||
## A simple vaultless password manager in Go | ||
**gokey** is a password manager, which does not require a password vault. Instead of storing your passwords in a vault it derives your password on the fly from your master password and supplied _realm_ string (for example, resource URL). This way you do not have to manage, backup or sync your password vault (or trust its management to a third party) as your passwords are available immediately anywhere. | ||
|
||
#### example | ||
``` | ||
gokey -p super-secret-master-password -r example.com | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
// Command gokey is a vaultless password and key manager. | ||
package main | ||
|
||
import ( | ||
"flag" | ||
"fmt" | ||
"io" | ||
"io/ioutil" | ||
"log" | ||
"os" | ||
|
||
"github.com/cloudflare/gokey" | ||
) | ||
|
||
var ( | ||
pass, keyType, seedPath, realm, output string | ||
unsafe bool | ||
seedSkipCount int | ||
) | ||
|
||
func init() { | ||
flag.StringVar(&pass, "p", "", "master password") | ||
flag.StringVar(&keyType, "t", "pass", "output type (can be pass, seed, raw, ec256, ec521, rsa2048, rsa4096)") | ||
flag.StringVar(&seedPath, "s", "", "path to master seed file (optional)") | ||
flag.IntVar(&seedSkipCount, "skip", 0, "number of bytes to skip from master seed file (default 0)") | ||
flag.StringVar(&realm, "r", "", "password/key realm (most probably purpose of the password/key)") | ||
flag.StringVar(&output, "o", "", "output path to store generated key/password (default stdout)") | ||
flag.BoolVar(&unsafe, "u", false, "UNSAFE: allow key generation without a seed") | ||
} | ||
|
||
var keyTypes = map[string]int{ | ||
"ec256": gokey.KEYTYPE_EC256, | ||
"ec521": gokey.KEYTYPE_EC521, | ||
"rsa2048": gokey.KEYTYPE_RSA2048, | ||
"rsa4096": gokey.KEYTYPE_RSA4096, | ||
} | ||
|
||
func genSeed(w io.Writer) { | ||
seed, err := gokey.GenerateEncryptedKeySeed(pass) | ||
if err != nil { | ||
log.Fatalln(err) | ||
} | ||
|
||
_, err = w.Write(seed) | ||
if err != nil { | ||
log.Fatalln(err) | ||
} | ||
} | ||
|
||
func genPass(seed []byte, w io.Writer) { | ||
password, err := gokey.GetPass(pass, realm, seed, &gokey.PasswordSpec{10, 3, 3, 1, 1, ""}) | ||
if err != nil { | ||
log.Fatalln(err) | ||
} | ||
|
||
_, err = io.WriteString(w, password) | ||
if err != nil { | ||
log.Fatalln(err) | ||
} | ||
|
||
fmt.Fprintln(w, "") | ||
} | ||
|
||
func genKey(seed []byte, w io.Writer) { | ||
key, err := gokey.GetKey(pass, realm, seed, keyTypes[keyType], unsafe) | ||
if err != nil { | ||
log.Fatalln(err) | ||
} | ||
|
||
err = gokey.EncodeToPem(key, w) | ||
if err != nil { | ||
log.Fatalln(err) | ||
} | ||
} | ||
|
||
// TODO: parametrize size | ||
// generates raw 32 bytes | ||
func genRaw(seed []byte, w io.Writer) { | ||
raw, err := gokey.GetRaw(pass, realm, seed, unsafe) | ||
if err != nil { | ||
log.Fatalln(err) | ||
} | ||
|
||
_, err = io.CopyN(w, raw, 32) | ||
if err != nil { | ||
log.Fatalln(err) | ||
} | ||
} | ||
|
||
func logFatal(format string, args ...interface{}) { | ||
log.Printf(format, args...) | ||
flag.PrintDefaults() | ||
os.Exit(1) | ||
} | ||
|
||
func main() { | ||
flag.Parse() | ||
|
||
if pass == "" { | ||
logFatal("no password provided") | ||
} | ||
|
||
out := os.Stdout | ||
var err error | ||
if output != "" { | ||
out, err = os.OpenFile(output, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600) | ||
if err != nil { | ||
log.Fatalln(err) | ||
} | ||
defer out.Close() | ||
} | ||
|
||
if keyType == "seed" { | ||
genSeed(out) | ||
} else { | ||
if realm == "" { | ||
logFatal("no realm provided") | ||
} | ||
|
||
var seed []byte | ||
if seedPath != "" { | ||
seed, err = ioutil.ReadFile(seedPath) | ||
if err != nil { | ||
log.Fatalln(err) | ||
} | ||
|
||
if (seedSkipCount < 0) || (seedSkipCount >= len(seed)) { | ||
log.Fatalln("invalid skip parameter") | ||
} | ||
seed = seed[seedSkipCount:] | ||
} | ||
|
||
switch keyType { | ||
case "pass": | ||
genPass(seed, out) | ||
case "raw": | ||
genRaw(seed, out) | ||
default: | ||
if _, ok := keyTypes[keyType]; !ok { | ||
logFatal("unknown key type: %v", keyType) | ||
} | ||
genKey(seed, out) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
package gokey | ||
|
||
// we are using Fortuna Generator as our "reproducible" CSPRNG | ||
// this implementation is simplified as we need only a repeatable PRNG and we seed it only once | ||
// also, every instance of this PRNG will be used to generate only one password/key | ||
|
||
import ( | ||
"bytes" | ||
"crypto/aes" | ||
"crypto/cipher" | ||
"crypto/rand" | ||
"crypto/sha256" | ||
"errors" | ||
"io" | ||
|
||
"golang.org/x/crypto/hkdf" | ||
"golang.org/x/crypto/pbkdf2" | ||
) | ||
|
||
const ( | ||
keySeedLength = 256 | ||
) | ||
|
||
type fortunaGenerator struct { | ||
key []byte | ||
counter [16]byte | ||
cipher cipher.Block | ||
buffer *bytes.Buffer | ||
} | ||
|
||
func (g *fortunaGenerator) increment() { | ||
// from cipher/ctr/ctr.go (changed byte order) | ||
for i := 0; i < len(g.counter); i++ { | ||
g.counter[i]++ | ||
if g.counter[i] != 0 { | ||
break | ||
} | ||
} | ||
} | ||
|
||
func (g *fortunaGenerator) isCountZero() bool { | ||
for i := len(g.counter) - 1; i >= 0; i-- { | ||
if g.counter[i] != 0 { | ||
return false | ||
} | ||
} | ||
|
||
return true | ||
} | ||
|
||
func (g *fortunaGenerator) Reseed(seed []byte) { | ||
hash := sha256.New() | ||
hash.Write(g.key) | ||
hash.Write(seed) | ||
g.key = hash.Sum(nil) | ||
|
||
aes, err := aes.NewCipher(g.key) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
g.cipher = aes | ||
if g.buffer == nil { | ||
g.buffer = bytes.NewBuffer(nil) | ||
} | ||
g.increment() | ||
} | ||
|
||
func (g *fortunaGenerator) generateBlocks(blockCount int) ([]byte, error) { | ||
if g.isCountZero() { | ||
return nil, errors.New("PRNG has not been seeded") | ||
} | ||
|
||
r := make([]byte, blockCount*16) | ||
|
||
for i := 0; i < blockCount; i++ { | ||
g.cipher.Encrypt(r[i*16:], g.counter[:]) | ||
g.increment() | ||
} | ||
|
||
return r, nil | ||
} | ||
|
||
func (g *fortunaGenerator) Read(p []byte) (n int, err error) { | ||
// to be reproducible we will generate data in blocks and buffer them | ||
// so, for example, Read(24) == Read(5) + Read(9) | ||
|
||
for len(p) > g.buffer.Len() { | ||
blocks, err := g.generateBlocks(256 / 16) | ||
if err != nil { | ||
return 0, err | ||
} | ||
|
||
g.key, err = g.generateBlocks(2) | ||
if err != nil { | ||
return 0, err | ||
} | ||
|
||
g.cipher, err = aes.NewCipher(g.key) | ||
if err != nil { | ||
return 0, err | ||
} | ||
|
||
g.buffer.Write(blocks) | ||
} | ||
|
||
return g.buffer.Read(p) | ||
} | ||
|
||
func passKey(password, realm string) []byte { | ||
return pbkdf2.Key([]byte(password), []byte(realm), 4096, 32, sha256.New) | ||
} | ||
|
||
func NewDRNG(password, realm string) io.Reader { | ||
rng := &fortunaGenerator{} | ||
rng.Reseed(passKey(password, realm)) | ||
|
||
return rng | ||
} | ||
|
||
func NewDRNGwithSeed(password, realm string, seed []byte) (io.Reader, error) { | ||
uSeed, err := unwrapSeed(password, seed) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// will reuse some of the public seed info | ||
salt := make([]byte, 12+16) | ||
copy(salt[:12], uSeed[:12]) | ||
copy(salt[12:], uSeed[len(uSeed)-16:]) | ||
|
||
hkdf := hkdf.New(sha256.New, uSeed, salt, []byte(realm)) | ||
rngSeed := make([]byte, 32) | ||
_, err = io.ReadFull(hkdf, rngSeed) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
rng := &fortunaGenerator{} | ||
rng.Reseed(rngSeed) | ||
|
||
return rng, nil | ||
} | ||
|
||
func GenerateEncryptedKeySeed(password string) ([]byte, error) { | ||
seed := make([]byte, keySeedLength) | ||
|
||
_, err := rand.Read(seed) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
masterkey := passKey(password, string(seed[:12])) | ||
|
||
aes, err := aes.NewCipher(masterkey) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
gcm, err := cipher.NewGCM(aes) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
pt := seed[12 : len(seed)-16] | ||
|
||
// encrypt in place | ||
gcm.Seal(pt[:0], seed[:12], pt, nil) | ||
|
||
return seed, nil | ||
} | ||
|
||
func unwrapSeed(password string, seed []byte) ([]byte, error) { | ||
masterkey := passKey(password, string(seed[:12])) | ||
|
||
aes, err := aes.NewCipher(masterkey) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
gcm, err := cipher.NewGCM(aes) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
pt := make([]byte, len(seed)) | ||
_, err = gcm.Open(pt[12:], seed[:12], seed[12:], nil) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
copy(pt[:12], seed[:12]) | ||
copy(pt[len(pt)-16:], seed[len(seed)-16:]) | ||
return pt, nil | ||
} |
Oops, something went wrong.