From 83e3240e6ee164c979c6a421461e97556b3439f7 Mon Sep 17 00:00:00 2001 From: Jim Myhrberg Date: Sun, 7 Jan 2018 13:03:51 +0000 Subject: [PATCH] Initial working version --- seedsafe.go | 131 ++++++++++++++++++++ seedsafe_test.go | 66 ++++++++++ vendor/golang.org/x/crypto/LICENSE | 27 ++++ vendor/golang.org/x/crypto/PATENTS | 22 ++++ vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go | 77 ++++++++++++ vendor/vendor.json | 13 ++ 6 files changed, 336 insertions(+) create mode 100644 seedsafe.go create mode 100644 seedsafe_test.go create mode 100644 vendor/golang.org/x/crypto/LICENSE create mode 100644 vendor/golang.org/x/crypto/PATENTS create mode 100644 vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go create mode 100644 vendor/vendor.json diff --git a/seedsafe.go b/seedsafe.go new file mode 100644 index 0000000..bf9b452 --- /dev/null +++ b/seedsafe.go @@ -0,0 +1,131 @@ +package seedsafe + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/sha512" + "encoding/base64" + "errors" + + "golang.org/x/crypto/pbkdf2" +) + +// Sep is the separator used to split salt and ciphertext. +var Sep = []byte("|") + +// Encrypt a payload with given password. +func Encrypt(plaintext []byte, password []byte) ([]byte, error) { + salt, err := randomBytes(32) + if err != nil { + return nil, err + } + + key := generateKey(password, salt) + ciphertext, err := encrypt(plaintext, key) + if err != nil { + return nil, err + } + + output := base64encode(salt) + output = append(output, Sep...) + output = append(output, base64encode(ciphertext)...) + return output, nil +} + +// Decrypt a payload with given password. +func Decrypt(text []byte, password []byte) ([]byte, error) { + separated := bytes.Split(text, Sep) + salt, err := base64decode(separated[0]) + if err != nil { + return nil, err + } + + ciphertext, err := base64decode(separated[1]) + if err != nil { + return nil, err + } + + key := generateKey(password, salt) + + plaintext, err := decrypt(ciphertext, key) + if err != nil { + return nil, err + } + + return plaintext, nil +} + +func encrypt(plaintext []byte, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + nonce, err := randomBytes(gcm.NonceSize()) + if err != nil { + return nil, err + } + + return gcm.Seal(nonce, nonce, plaintext, nil), nil +} + +func decrypt(ciphertext []byte, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + gcm, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + + if len(ciphertext) < gcm.NonceSize() { + return nil, errors.New("malformed ciphertext") + } + + return gcm.Open( + nil, + ciphertext[:gcm.NonceSize()], + ciphertext[gcm.NonceSize():], + nil, + ) +} + +func randomBytes(n int) ([]byte, error) { + b := make([]byte, n) + _, err := rand.Read(b) + if err != nil { + return nil, err + } + + return b, nil +} + +func generateKey(password []byte, salt []byte) []byte { + return pbkdf2.Key(password, salt, 65536, 32, sha512.New) +} + +func base64encode(input []byte) []byte { + output := make([]byte, base64.StdEncoding.EncodedLen(len(input))) + base64.StdEncoding.Encode(output, input) + + return output +} + +func base64decode(input []byte) ([]byte, error) { + output := make([]byte, base64.StdEncoding.DecodedLen(len(input))) + l, err := base64.StdEncoding.Decode(output, input) + if err != nil { + return nil, err + } + + return output[:l], nil +} diff --git a/seedsafe_test.go b/seedsafe_test.go new file mode 100644 index 0000000..71f6272 --- /dev/null +++ b/seedsafe_test.go @@ -0,0 +1,66 @@ +package seedsafe + +import ( + "fmt" + "testing" +) + +func TestEncryptAndDecrypt(t *testing.T) { + examples := []struct { + Password string + Text string + }{ + { + "puck-outrage-stole-locale-brood", + "tundra thiamine actinium cray gushy grainy lets ruminant edging " + + "bunting embalm railhead purify swatch times added graham " + + "friction blast liniment iberian octet yank suddenly", + }, + } + + for _, e := range examples { + encrypted, err := Encrypt([]byte(e.Text), []byte(e.Password)) + if err != nil { + t.Fatalf("Error during encryption: %s", err) + } + + decrypted, err := Decrypt(encrypted, []byte(e.Password)) + if err != nil { + t.Fatalf("Error during decryption: %s", err) + } + + fmt.Println(e.Text) + fmt.Println(string(encrypted)) + fmt.Println(string(decrypted)) + + if string(decrypted) != e.Text { + t.Fatalf("\nExpected: %s\n Got: %s", e.Text, decrypted) + } + + if len(decrypted) != len(e.Text) { + t.Fatalf("\nExpected length: %d\nGot: %d", + len(e.Text), len(decrypted)) + } + } +} + +func TestRandomBytes(t *testing.T) { + examples := []int{2, 4, 8, 16, 32, 64, 128, 256, 512} + + for _, n := range examples { + salt, _ := randomBytes(n) + if len(salt) != n { + t.Fatalf("%d length expected, got %d", n, len(salt)) + } + } +} + +func TestGenerateKey(t *testing.T) { + salt, _ := randomBytes(32) + password := []byte("hello") + key := generateKey(password, salt) + + if len(key) != 32 { + t.Fatalf("Key length should be 256, but it is %d", len(key)) + } +} diff --git a/vendor/golang.org/x/crypto/LICENSE b/vendor/golang.org/x/crypto/LICENSE new file mode 100644 index 0000000..6a66aea --- /dev/null +++ b/vendor/golang.org/x/crypto/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name of Google Inc. 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 +OWNER 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. diff --git a/vendor/golang.org/x/crypto/PATENTS b/vendor/golang.org/x/crypto/PATENTS new file mode 100644 index 0000000..7330990 --- /dev/null +++ b/vendor/golang.org/x/crypto/PATENTS @@ -0,0 +1,22 @@ +Additional IP Rights Grant (Patents) + +"This implementation" means the copyrightable works distributed by +Google as part of the Go project. + +Google hereby grants to You a perpetual, worldwide, non-exclusive, +no-charge, royalty-free, irrevocable (except as stated in this section) +patent license to make, have made, use, offer to sell, sell, import, +transfer and otherwise run, modify and propagate the contents of this +implementation of Go, where such license applies only to those patent +claims, both currently owned or controlled by Google and acquired in +the future, licensable by Google that are necessarily infringed by this +implementation of Go. This grant does not include claims that would be +infringed only as a consequence of further modification of this +implementation. If you or your agent or exclusive licensee institute or +order or agree to the institution of patent litigation against any +entity (including a cross-claim or counterclaim in a lawsuit) alleging +that this implementation of Go or any code incorporated within this +implementation of Go constitutes direct or contributory patent +infringement, or inducement of patent infringement, then any patent +rights granted to you under this License for this implementation of Go +shall terminate as of the date such litigation is filed. diff --git a/vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go b/vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go new file mode 100644 index 0000000..593f653 --- /dev/null +++ b/vendor/golang.org/x/crypto/pbkdf2/pbkdf2.go @@ -0,0 +1,77 @@ +// Copyright 2012 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +/* +Package pbkdf2 implements the key derivation function PBKDF2 as defined in RFC +2898 / PKCS #5 v2.0. + +A key derivation function is useful when encrypting data based on a password +or any other not-fully-random data. It uses a pseudorandom function to derive +a secure encryption key based on the password. + +While v2.0 of the standard defines only one pseudorandom function to use, +HMAC-SHA1, the drafted v2.1 specification allows use of all five FIPS Approved +Hash Functions SHA-1, SHA-224, SHA-256, SHA-384 and SHA-512 for HMAC. To +choose, you can pass the `New` functions from the different SHA packages to +pbkdf2.Key. +*/ +package pbkdf2 // import "golang.org/x/crypto/pbkdf2" + +import ( + "crypto/hmac" + "hash" +) + +// Key derives a key from the password, salt and iteration count, returning a +// []byte of length keylen that can be used as cryptographic key. The key is +// derived based on the method described as PBKDF2 with the HMAC variant using +// the supplied hash function. +// +// For example, to use a HMAC-SHA-1 based PBKDF2 key derivation function, you +// can get a derived key for e.g. AES-256 (which needs a 32-byte key) by +// doing: +// +// dk := pbkdf2.Key([]byte("some password"), salt, 4096, 32, sha1.New) +// +// Remember to get a good random salt. At least 8 bytes is recommended by the +// RFC. +// +// Using a higher iteration count will increase the cost of an exhaustive +// search but will also make derivation proportionally slower. +func Key(password, salt []byte, iter, keyLen int, h func() hash.Hash) []byte { + prf := hmac.New(h, password) + hashLen := prf.Size() + numBlocks := (keyLen + hashLen - 1) / hashLen + + var buf [4]byte + dk := make([]byte, 0, numBlocks*hashLen) + U := make([]byte, hashLen) + for block := 1; block <= numBlocks; block++ { + // N.B.: || means concatenation, ^ means XOR + // for each block T_i = U_1 ^ U_2 ^ ... ^ U_iter + // U_1 = PRF(password, salt || uint(i)) + prf.Reset() + prf.Write(salt) + buf[0] = byte(block >> 24) + buf[1] = byte(block >> 16) + buf[2] = byte(block >> 8) + buf[3] = byte(block) + prf.Write(buf[:4]) + dk = prf.Sum(dk) + T := dk[len(dk)-hashLen:] + copy(U, T) + + // U_n = PRF(password, U_(n-1)) + for n := 2; n <= iter; n++ { + prf.Reset() + prf.Write(U) + U = U[:0] + U = prf.Sum(U) + for x := range U { + T[x] ^= U[x] + } + } + } + return dk[:keyLen] +} diff --git a/vendor/vendor.json b/vendor/vendor.json new file mode 100644 index 0000000..1cf7fd9 --- /dev/null +++ b/vendor/vendor.json @@ -0,0 +1,13 @@ +{ + "comment": "", + "ignore": "test", + "package": [ + { + "checksumSHA1": "1MGpGDQqnUoRpv7VEcQrXOBydXE=", + "path": "golang.org/x/crypto/pbkdf2", + "revision": "0fcca4842a8d74bfddc2c96a073bd2a4d2a7a2e8", + "revisionTime": "2017-11-25T19:00:56Z" + } + ], + "rootPath": "github.com/jimeh/seedsafe" +}