This commit is contained in:
Dennis Coyle 2018-04-16 10:50:10 -04:00
commit c4a09f8b66
16 changed files with 922 additions and 35 deletions

18
cmd/storj/main.go Normal file
View File

@ -0,0 +1,18 @@
// Copyright (C) 2018 Storj Labs, Inc.
// See LICENSE for copying information.
package main
import (
"log"
"os"
"storj.io/storj/internal/app/cli"
)
func main() {
err := cli.New().Run(os.Args)
if err != nil {
log.Fatal(err)
}
}

10
go.mod
View File

@ -1 +1,9 @@
module "github.com/Storj/storj"
module "storj.io/storj"
require (
"github.com/spf13/viper" v1.0.2
"github.com/tyler-smith/go-bip39" v0.0.0-20160629163856-8e7a99b3e716
"github.com/urfave/cli" v1.20.0
"github.com/zeebo/errs" v0.1.0
"golang.org/x/crypto" v0.0.0-20180410182641-f70185d77e82
)

80
internal/app/cli/app.go Normal file
View File

@ -0,0 +1,80 @@
// Copyright (C) 2018 Storj Labs, Inc.
// See LICENSE for copying information.
package cli
import (
"fmt"
"os"
"github.com/urfave/cli"
"storj.io/storj/pkg/client"
)
// New creates a new storj cli application with the respective commands and metainfo.
func New() *cli.App {
app := cli.NewApp()
app.Name = "storj"
app.Version = "0.0.2"
app.Usage = "command line interface to the Storj network"
app.Commands = []cli.Command{
{
Name: "get-info",
Usage: "prints bridge api information",
ArgsUsage: " ", // no args
Category: "bridge api information",
Action: func(c *cli.Context) error {
getInfo()
return nil
},
},
{
Name: "list-buckets",
Usage: "lists the available buckets",
ArgsUsage: " ", // no args
Category: "working with buckets and files",
Action: func(c *cli.Context) error {
listBuckets()
return nil
},
},
}
cli.AppHelpTemplate = fmt.Sprintf(`%s
ENVIRONMENT VARIABLES:
STORJ_BRIDGE the bridge host (e.g. https://api.storj.io)
STORJ_BRIDGE_USER bridge username
STORJ_BRIDGE_PASS bridge password
STORJ_ENCRYPTION_KEY file encryption key
`, cli.AppHelpTemplate)
return app
}
func getInfo() {
env := client.NewEnv()
info, err := client.GetInfo(env)
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
fmt.Printf("Storj bridge: %s\n\n"+
"Title: %s\n"+
"Description: %s\n"+
"Version: %s\n"+
"Host: %s\n",
env.URL, info.Title, info.Description, info.Version, info.Host)
}
func listBuckets() {
buckets, err := client.GetBuckets(client.NewEnv())
if err != nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)
}
for _, b := range buckets {
fmt.Printf("ID: %s\tDecrypted: %t\t\tCreated: %s\tName: %s\n",
b.ID, b.Decrypted, b.Created, b.Name)
}
}

57
pkg/client/buckets.go Normal file
View File

@ -0,0 +1,57 @@
// Copyright (C) 2018 Storj Labs, Inc.
// See LICENSE for copying information.
package client
import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"strconv"
)
// Bucket metdata
type Bucket struct {
ID string
Name string
Created string
Decrypted bool
}
// GetBuckets returns the list of buckets
func GetBuckets(env Env) ([]Bucket, error) {
client := &http.Client{}
req, err := http.NewRequest("GET", env.URL+"/buckets", nil)
if err != nil {
return []Bucket{}, err
}
req.SetBasicAuth(env.User, env.Password)
resp, err := client.Do(req)
if err != nil {
return []Bucket{}, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return []Bucket{}, UnexpectedStatusCode.New(strconv.Itoa(resp.StatusCode))
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return []Bucket{}, err
}
var buckets []Bucket
err = json.Unmarshal(b, &buckets)
if err != nil {
return buckets, err
}
for i, b := range buckets {
decryptedName, err := decryptBucketName(b.Name, env.Mnemonic)
if err != nil {
log.Printf("Could not decrypt bucket name %s: %s\n", b.Name, err)
continue
}
buckets[i].Name = decryptedName
buckets[i].Decrypted = true
}
return buckets, err
}

130
pkg/client/buckets_test.go Normal file
View File

@ -0,0 +1,130 @@
// Copyright (C) 2018 Storj Labs, Inc.
// See LICENSE for copying information.
package client
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/julienschmidt/httprouter"
"github.com/stretchr/testify/assert"
)
const (
testEncryptedBucketName = "cqiNhd3Y16uXRBpRKbcGdrhVvouLRFlBM5O1jMUOr6OKJUVGpvv0LLaBv+6kqzyVvp5jFw=="
testEncryptedBucketNameDiffMnemonic = "Yq3Ky6jJ7dwWiC9MEcb5nhAl5P0xfYe6jCwwwlzd1a1kZxKYLcft/WkOC8dhcwLb3Ka9xA=="
testDecryptedBucketName = "test"
)
func TestGetBuckets(t *testing.T) {
var tsResponse string
router := httprouter.New()
router.GET("/buckets", basicAuth(func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprintf(w, tsResponse)
}, testBridgeUser, testBridgePass))
ts := httptest.NewServer(router)
defer ts.Close()
for i, tt := range []struct {
env Env
response string
buckets []Bucket
errString string
}{
{NewNoAuthTestEnv(ts), "", nil, "unexpected status code: 401"},
{NewBadPassTestEnv(ts), "", nil, "unexpected status code: 401"},
{NewTestEnv(ts), "", []Bucket{}, "unexpected end of JSON input"},
{NewTestEnv(ts), "{", []Bucket{}, "unexpected end of JSON input"},
{NewTestEnv(ts), "{}", []Bucket{}, "json: cannot unmarshal object into Go value of type []client.Bucket"},
{NewTestEnv(ts), "[]", []Bucket{}, ""},
{NewTestEnv(ts),
`[
{
"id": "e3eca45f4d294132c07b49f4",
"name": "cqiNhd3Y16uXRBpRKbcGdrhVvouLRFlBM5O1jMUOr6OKJUVGpvv0LLaBv+6kqzyVvp5jFw==",
"created": "2016-10-12T14:40:21.259Z"
}
]`, []Bucket{
Bucket{
ID: "e3eca45f4d294132c07b49f4",
Name: testDecryptedBucketName,
Created: "2016-10-12T14:40:21.259Z",
Decrypted: true,
},
}, ""},
{NewNoMnemonicTestEnv(ts),
`[
{
"id": "e3eca45f4d294132c07b49f4",
"name": "cqiNhd3Y16uXRBpRKbcGdrhVvouLRFlBM5O1jMUOr6OKJUVGpvv0LLaBv+6kqzyVvp5jFw==",
"created": "2016-10-12T14:40:21.259Z"
}
]`, []Bucket{
Bucket{
ID: "e3eca45f4d294132c07b49f4",
Name: testEncryptedBucketName,
Created: "2016-10-12T14:40:21.259Z",
Decrypted: false,
},
}, ""},
{NewTestEnv(ts),
`[
{
"id": "e3eca45f4d294132c07b49f4",
"name": "Yq3Ky6jJ7dwWiC9MEcb5nhAl5P0xfYe6jCwwwlzd1a1kZxKYLcft/WkOC8dhcwLb3Ka9xA==",
"created": "2016-10-12T14:40:21.259Z"
}
]`, []Bucket{
Bucket{
ID: "e3eca45f4d294132c07b49f4",
Name: testEncryptedBucketNameDiffMnemonic,
Created: "2016-10-12T14:40:21.259Z",
Decrypted: false,
},
}, ""},
{NewTestEnv(ts),
`[
{
"id": "e3eca45f4d294132c07b49f4",
"name": "test",
"created": "2016-10-12T14:40:21.259Z"
}
]`, []Bucket{
Bucket{
ID: "e3eca45f4d294132c07b49f4",
Name: testDecryptedBucketName,
Created: "2016-10-12T14:40:21.259Z",
Decrypted: false,
},
}, ""},
{NewNoMnemonicTestEnv(ts),
`[
{
"id": "e3eca45f4d294132c07b49f4",
"name": "test",
"created": "2016-10-12T14:40:21.259Z"
}
]`, []Bucket{
Bucket{
ID: "e3eca45f4d294132c07b49f4",
Name: testDecryptedBucketName,
Created: "2016-10-12T14:40:21.259Z",
Decrypted: false,
},
}, ""},
} {
tsResponse = tt.response
buckets, err := GetBuckets(tt.env)
errTag := fmt.Sprintf("Test case #%d", i)
if tt.errString != "" {
assert.EqualError(t, err, tt.errString, errTag)
continue
}
if assert.NoError(t, err, errTag) {
assert.Equal(t, tt.buckets, buckets, errTag)
}
}
}

102
pkg/client/crypto.go Normal file
View File

@ -0,0 +1,102 @@
// Copyright (C) 2018 Storj Labs, Inc.
// See LICENSE for copying information.
package client
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/sha256"
"crypto/sha512"
"encoding/base64"
"encoding/hex"
bip39 "github.com/tyler-smith/go-bip39"
)
const (
bucketNameMagic = "398734aab3c4c30c9f22590e83a95f7e43556a45fc2b3060e0c39fde31f50272"
gcmDigestSize = 16
ivSize = 32
)
var bucketMetaMagic = []byte{
66, 150, 71, 16, 50, 114, 88, 160, 163, 35, 154, 65, 162, 213, 226, 215,
70, 138, 57, 61, 52, 19, 210, 170, 38, 164, 162, 200, 86, 201, 2, 81}
func sha256Sum(str string) string {
checksum := sha256.Sum256([]byte(str))
return hex.EncodeToString(checksum[:])
}
func mnemonicToSeed(mnemonic string) (string, error) {
seed, err := bip39.NewSeedWithErrorChecking(mnemonic, "")
if err != nil {
return "", err
}
return hex.EncodeToString(seed), nil
}
func getDeterministicKey(key, id string) (string, error) {
input, err := hex.DecodeString(key + id)
if err != nil {
return "", err
}
checksum := sha512.Sum512(input)
str := hex.EncodeToString(checksum[:])
return str[0 : len(str)/2], nil
}
func generateBucketKey(mnemonic, bucketID string) (string, error) {
seed, err := mnemonicToSeed(mnemonic)
if err != nil {
return "", err
}
return getDeterministicKey(seed, bucketNameMagic)
}
func decryptMeta(encryptedName string, key []byte) ([]byte, error) {
nameBase64, err := base64.StdEncoding.DecodeString(encryptedName)
if err != nil {
return []byte{}, err
}
if len(nameBase64) <= gcmDigestSize+ivSize {
return []byte{}, CryptoError.New("Invalid encrypted name")
}
block, err := aes.NewCipher(key)
if err != nil {
return []byte{}, err
}
aesgcm, err := cipher.NewGCMWithNonceSize(block, ivSize)
if err != nil {
return []byte{}, err
}
digest := nameBase64[:gcmDigestSize]
iv := nameBase64[gcmDigestSize : gcmDigestSize+ivSize]
cipherText := nameBase64[gcmDigestSize+ivSize:]
return aesgcm.Open(nil, iv, append(cipherText, digest...), nil)
}
func decryptBucketName(name, mnemonic string) (string, error) {
bucketKey, err := generateBucketKey(mnemonic, bucketNameMagic)
if err != nil {
return "", err
}
bucketKeyHex, err := hex.DecodeString(bucketKey)
if err != nil {
return "", err
}
sig := hmac.New(sha512.New, bucketKeyHex)
_, err = sig.Write(bucketMetaMagic)
if err != nil {
return "", err
}
hmacSHA512 := sig.Sum(nil)
key := hmacSHA512[0 : len(hmacSHA512)/2]
decryptedName, err := decryptMeta(name, key)
if err != nil {
return "", err
}
return string(decryptedName), nil
}

38
pkg/client/crypto_test.go Normal file
View File

@ -0,0 +1,38 @@
// Copyright (C) 2018 Storj Labs, Inc.
// See LICENSE for copying information.
package client
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
const testMnemonic = "uncle obtain april oxygen cover patient layer abuse off text royal normal"
func TestDecryptBucketName(t *testing.T) {
for i, tt := range []struct {
encryptedName string
mnemonic string
decryptedName string
errString string
}{
{"", "", "", "Invalid mnemonic"},
{"", testMnemonic, "", "Invalid encrypted name"},
{testEncryptedBucketName, "", "", "Invalid mnemonic"},
{testEncryptedBucketName, testMnemonic, testDecryptedBucketName, ""},
{testEncryptedBucketNameDiffMnemonic, testMnemonic, "", "cipher: message authentication failed"},
} {
decryptedName, err := decryptBucketName(tt.encryptedName, tt.mnemonic)
errTag := fmt.Sprintf("Test case #%d", i)
if tt.errString != "" {
assert.EqualError(t, err, tt.errString, errTag)
continue
}
if assert.NoError(t, err, errTag) {
assert.Equal(t, tt.decryptedName, decryptedName, errTag)
}
}
}

38
pkg/client/env.go Normal file
View File

@ -0,0 +1,38 @@
// Copyright (C) 2018 Storj Labs, Inc.
// See LICENSE for copying information.
package client
import (
"strings"
"github.com/spf13/viper"
)
// DefaultURL of the Storj Bridge API endpoint
const DefaultURL = "https://api.storj.io"
func init() {
viper.SetEnvPrefix("storj")
viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
viper.AutomaticEnv()
viper.SetDefault("bridge", DefaultURL)
}
// Env contains parameters for accessing the Storj network
type Env struct {
URL string
User string
Password string
Mnemonic string
}
// NewEnv creates new Env struct with default values
func NewEnv() Env {
return Env{
URL: viper.GetString("bridge"),
User: viper.GetString("bridge-user"),
Password: sha256Sum(viper.GetString("bridge-pass")),
Mnemonic: viper.GetString("encryption-key"),
}
}

99
pkg/client/env_test.go Normal file
View File

@ -0,0 +1,99 @@
// Copyright (C) 2018 Storj Labs, Inc.
// See LICENSE for copying information.
package client
import (
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/julienschmidt/httprouter"
"github.com/stretchr/testify/assert"
)
const (
testBridgeURL = "http://example.com"
testBridgeUser = "testuser@storj.io"
testBridgePass = "secret"
)
func NewTestEnv(ts *httptest.Server) Env {
return Env{
URL: ts.URL,
User: testBridgeUser,
Password: testBridgePass,
Mnemonic: testMnemonic,
}
}
func NewNoAuthTestEnv(ts *httptest.Server) Env {
return Env{
URL: ts.URL,
}
}
func NewBadPassTestEnv(ts *httptest.Server) Env {
return Env{
URL: ts.URL,
User: testBridgeUser,
Password: "bad password",
Mnemonic: testMnemonic,
}
}
func NewNoMnemonicTestEnv(ts *httptest.Server) Env {
return Env{
URL: ts.URL,
User: testBridgeUser,
Password: testBridgePass,
}
}
func basicAuth(h httprouter.Handle, requiredUser, requiredPassword string) httprouter.Handle {
return func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
// Get the Basic Authentication credentials
user, password, hasAuth := r.BasicAuth()
if hasAuth && user == requiredUser && password == requiredPassword {
// Delegate request to the given handle
h(w, r, ps)
} else {
// Request Basic Authentication otherwise
w.Header().Set("WWW-Authenticate", "Basic realm=Restricted")
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
}
}
func TestNewEnv(t *testing.T) {
for _, tt := range []struct {
env Env
url string
}{
{Env{}, ""},
{NewEnv(), DefaultURL},
{Env{URL: testBridgeURL}, testBridgeURL},
} {
assert.Equal(t, tt.url, tt.env.URL)
}
}
func TestNewEnvVars(t *testing.T) {
os.Setenv("STORJ_BRIDGE", testBridgeURL)
defer os.Unsetenv("STORJ_BRIDGE")
os.Setenv("STORJ_BRIDGE_USER", testBridgeUser)
defer os.Unsetenv("STORJ_BRIDGE_USER")
os.Setenv("STORJ_BRIDGE_PASS", testBridgePass)
defer os.Unsetenv("STORJ_BRIDGE_PASS")
os.Setenv("STORJ_ENCRYPTION_KEY", testMnemonic)
defer os.Unsetenv("STORJ_ENCRYPTION_KEY")
env := NewEnv()
assert.Equal(t, testBridgeURL, env.URL)
assert.Equal(t, testBridgeUser, env.User)
assert.Equal(t, sha256Sum(testBridgePass), env.Password)
assert.Equal(t, testMnemonic, env.Mnemonic)
}

14
pkg/client/errors.go Normal file
View File

@ -0,0 +1,14 @@
// Copyright (C) 2018 Storj Labs, Inc.
// See LICENSE for copying information.
package client
import (
"github.com/zeebo/errs"
)
// UnexpectedStatusCode is an error class for unexpected HTTP response
var UnexpectedStatusCode = errs.Class("unexpected status code")
// CryptoError is an error class for encryption errors
var CryptoError = errs.Class("encryption error")

58
pkg/client/info.go Normal file
View File

@ -0,0 +1,58 @@
// Copyright (C) 2018 Storj Labs, Inc.
// See LICENSE for copying information.
package client
import (
"encoding/json"
"io/ioutil"
"net/http"
"strconv"
)
// Info struct of the GetInfo() response
type Info struct {
Title string
Description string
Version string
Host string
}
type swagger struct {
Info struct {
Title string
Description string
Version string
}
Host string
}
// UnmarshalJSON overrides the unmarshalling for Info to correctly extract the data from the Swagger JSON response
func (info *Info) UnmarshalJSON(b []byte) error {
var s swagger
json.Unmarshal(b, &s)
info.Title = s.Info.Title
info.Description = s.Info.Description
info.Version = s.Info.Version
info.Host = s.Host
return nil
}
// GetInfo returns info about the Storj Bridge server
func GetInfo(env Env) (Info, error) {
info := Info{}
resp, err := http.Get(env.URL)
if err != nil {
return info, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return info, UnexpectedStatusCode.New(strconv.Itoa(resp.StatusCode))
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return info, err
}
err = json.Unmarshal(b, &info)
return info, err
}

120
pkg/client/info_test.go Normal file
View File

@ -0,0 +1,120 @@
// Copyright (C) 2018 Storj Labs, Inc.
// See LICENSE for copying information.
package client
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/julienschmidt/httprouter"
"github.com/stretchr/testify/assert"
)
const (
testTitle = "Storj Bridge"
testDescription = "Some description"
testVersion = "1.2.3"
testHost = "1.2.3.4"
)
func TestUnmarshalJSON(t *testing.T) {
for i, tt := range []struct {
json string
info Info
errString string
}{
{"", Info{}, "unexpected end of JSON input"},
{"{", Info{}, "unexpected end of JSON input"},
{"{}", Info{}, ""},
{`{"info":{}}`, Info{}, ""},
{`{"info":10}`, Info{}, ""},
{`{"info":{"title":10,"description":10,"version":10},"host":10}`, Info{}, ""},
{fmt.Sprintf(`{"info":{"description":"%s","version":"%s"},"host":"%s"}`,
testDescription, testVersion, testHost),
Info{
Description: testDescription,
Version: testVersion,
Host: testHost,
},
""},
{fmt.Sprintf(`{"info":{"title":"%s","version":"%s"},"host":"%s"}`,
testTitle, testVersion, testHost),
Info{
Title: testTitle,
Version: testVersion,
Host: testHost,
},
""},
{fmt.Sprintf(`{"info":{"title":"%s","description":"%s"},"host":"%s"}`,
testTitle, testDescription, testHost),
Info{
Title: testTitle,
Description: testDescription,
Host: testHost,
},
""},
{fmt.Sprintf(`{"info":{"title":"%s","description":"%s","version":"%s"}}`,
testTitle, testDescription, testVersion),
Info{
Title: testTitle,
Description: testDescription,
Version: testVersion,
},
""},
{fmt.Sprintf(`{"info":{"title":"%s","description":"%s","version":"%s"},"host":"%s"}`,
testTitle, testDescription, testVersion, testHost),
Info{
Title: testTitle,
Description: testDescription,
Version: testVersion,
Host: testHost,
},
""},
} {
var info Info
err := json.Unmarshal([]byte(tt.json), &info)
errTag := fmt.Sprintf("Test case #%d", i)
if tt.errString != "" {
assert.EqualError(t, err, tt.errString, errTag)
continue
}
if assert.NoError(t, err, errTag) {
assert.Equal(t, tt.info, info, errTag)
}
}
}
func TestGetInfo(t *testing.T) {
router := httprouter.New()
router.GET("/", func(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprintf(w, `{"info":{"title":"%s","description":"%s","version":"%s"},"host":"%s"}`,
testTitle, testDescription, testVersion, testHost)
})
ts := httptest.NewServer(router)
defer ts.Close()
for i, tt := range []struct {
env Env
errString string
}{
{NewTestEnv(ts), ""},
{Env{URL: ts.URL + "/info"}, "unexpected status code: 404"},
} {
info, err := GetInfo(tt.env)
errTag := fmt.Sprintf("Test case #%d", i)
if tt.errString != "" {
assert.EqualError(t, err, tt.errString, errTag)
continue
}
if assert.NoError(t, err, errTag) {
assert.Equal(t, testTitle, info.Title, errTag)
assert.Equal(t, testDescription, info.Description, errTag)
assert.Equal(t, testVersion, info.Version, errTag)
assert.Equal(t, testHost, info.Host, errTag)
}
}
}

40
pkg/eestream/bits.go Normal file
View File

@ -0,0 +1,40 @@
// Copyright (C) 2018 Storj Labs, Inc.
// See LICENSE for copying information.
package eestream
import "math/big"
// incrementBytes takes a byte slice buf and treats it like a big-endian
// encoded unsigned integer. it adds amount to it (which must be nonnegative)
// in place. if rollover happens (the most significant bytes don't fit
// anymore), truncated is true.
func incrementBytes(buf []byte, amount int64) (truncated bool,
err error) {
if amount < 0 {
return false, Error.New("amount was negative")
}
// use math/big for the actual incrementing
var val big.Int
val.SetBytes(buf)
val.Add(&val, big.NewInt(amount))
data := val.Bytes()
// we went past the available memory. truncate the most significant bytes
// off
if len(data) > len(buf) {
data = data[len(data)-len(buf):]
truncated = true
}
// math/big doesn't return leading 0 bytes so add them back if they're
// missing
for i := len(buf) - len(data) - 1; i >= 0; i-- {
buf[i] = 0
}
// write the data out inplace
copy(buf[len(buf)-len(data):], data[:])
return truncated, nil
}

63
pkg/eestream/bits_test.go Normal file
View File

@ -0,0 +1,63 @@
// Copyright (C) 2018 Storj Labs, Inc.
// See LICENSE for copying information.
package eestream
import (
"bytes"
"testing"
)
func TestIncrementBytes(t *testing.T) {
for _, test := range []struct {
inbuf []byte
amount int64
err bool
outbuf []byte
truncated bool
}{
{nil, 10, false, nil, true},
{nil, 0, false, nil, false},
{nil, -1, true, nil, false},
{nil, -1, true, nil, false},
{nil, -457, true, nil, false},
{[]byte{0}, 0, false, []byte{0}, false},
{[]byte{0}, 1, false, []byte{1}, false},
{[]byte{0}, 254, false, []byte{0xfe}, false},
{[]byte{1}, 254, false, []byte{0xff}, false},
{[]byte{0}, 255, false, []byte{0xff}, false},
{[]byte{0, 0, 1}, 3, false, []byte{0, 0, 4}, false},
{[]byte{0}, 256, false, []byte{0}, true},
{[]byte{0}, 257, false, []byte{1}, true},
{[]byte{0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, 1,
false, []byte{0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0}, false},
{[]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, 1,
false, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, true},
{[]byte{0xfe, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff}, 1,
false, []byte{0xfe, 0xff, 0xff, 0xff, 0xff, 0, 0, 0, 0, 0}, false},
{[]byte{0xfe, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0, 0xff, 0xfe}, 0xff0001,
false, []byte{0xfe, 0xff, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff},
false},
{[]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0, 0xff, 0xfe}, 0xff0002,
false, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, true},
{[]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0, 0xff, 0xfe}, 0xff0003,
false, []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, true},
} {
trunc, err := incrementBytes(test.inbuf, test.amount)
if err != nil {
if !test.err {
t.Fatalf("unexpected err: %v", err)
}
continue
}
if test.err {
t.Fatalf("err expected but no err happened")
}
if trunc != test.truncated {
t.Fatalf("truncated rv mismatch")
}
if !bytes.Equal(test.outbuf, test.inbuf) {
t.Fatalf("result mismatch")
}
}
}

View File

@ -4,33 +4,38 @@
package eestream
import (
"encoding/binary"
"golang.org/x/crypto/nacl/secretbox"
)
type secretboxEncrypter struct {
blockSize int
key [32]byte
}
func setKey(dst *[32]byte, key []byte) error {
if len((*dst)[:]) != len(key) {
return Error.New("invalid key length, expected %d", len((*dst)[:]))
}
copy((*dst)[:], key)
return nil
blockSize int
key [32]byte
startingNonce [24]byte
}
// NewSecretboxEncrypter returns a Transformer that encrypts the data passing
// through with key.
func NewSecretboxEncrypter(key []byte, encryptedBlockSize int) (
Transformer, error) {
//
// startingNonce is treated as a big-endian encoded unsigned
// integer, and as blocks pass through, their block number and the starting
// nonce is added together to come up with that block's nonce. Encrypting
// different data with the same key and the same nonce is a huge security
// issue. It's safe to always encode new data with a random key and random
// startingNonce. The monotonically-increasing nonce (that rolls over) is to
// protect against data reordering.
//
// When in doubt, generate a new key from crypto/rand and a startingNonce
// from crypto/rand as often as possible.
func NewSecretboxEncrypter(key *[32]byte, startingNonce *[24]byte,
encryptedBlockSize int) (Transformer, error) {
if encryptedBlockSize <= secretbox.Overhead {
return nil, Error.New("block size too small")
}
rv := &secretboxEncrypter{blockSize: encryptedBlockSize - secretbox.Overhead}
return rv, setKey(&rv.key, key)
return &secretboxEncrypter{
blockSize: encryptedBlockSize - secretbox.Overhead,
key: *key,
startingNonce: *startingNonce,
}, nil
}
func (s *secretboxEncrypter) InBlockSize() int {
@ -41,33 +46,43 @@ func (s *secretboxEncrypter) OutBlockSize() int {
return s.blockSize + secretbox.Overhead
}
func calcNonce(blockNum int64) *[24]byte {
var buf [uint32Size]byte
binary.BigEndian.PutUint32(buf[:], uint32(blockNum))
var nonce [24]byte
copy(nonce[:], buf[1:])
return &nonce
func calcNonce(startingNonce *[24]byte, blockNum int64) (rv [24]byte,
err error) {
if copy(rv[:], (*startingNonce)[:]) != len(rv) {
return rv, Error.New("didn't copy memory?!")
}
_, err = incrementBytes(rv[:], blockNum)
return rv, err
}
func (s *secretboxEncrypter) Transform(out, in []byte, blockNum int64) (
[]byte, error) {
return secretbox.Seal(out, in, calcNonce(blockNum), &s.key), nil
n, err := calcNonce(&s.startingNonce, blockNum)
if err != nil {
return nil, err
}
return secretbox.Seal(out, in, &n, &s.key), nil
}
type secretboxDecrypter struct {
blockSize int
key [32]byte
blockSize int
key [32]byte
startingNonce [24]byte
}
// NewSecretboxDecrypter returns a Transformer that decrypts the data passing
// through with key.
func NewSecretboxDecrypter(key []byte, encryptedBlockSize int) (
Transformer, error) {
// through with key. See the comments for NewSecretboxEncrypter about
// startingNonce.
func NewSecretboxDecrypter(key *[32]byte, startingNonce *[24]byte,
encryptedBlockSize int) (Transformer, error) {
if encryptedBlockSize <= secretbox.Overhead {
return nil, Error.New("block size too small")
}
rv := &secretboxDecrypter{blockSize: encryptedBlockSize - secretbox.Overhead}
return rv, setKey(&rv.key, key)
return &secretboxDecrypter{
blockSize: encryptedBlockSize - secretbox.Overhead,
key: *key,
startingNonce: *startingNonce,
}, nil
}
func (s *secretboxDecrypter) InBlockSize() int {
@ -80,7 +95,11 @@ func (s *secretboxDecrypter) OutBlockSize() int {
func (s *secretboxDecrypter) Transform(out, in []byte, blockNum int64) (
[]byte, error) {
rv, success := secretbox.Open(out, in, calcNonce(blockNum), &s.key)
n, err := calcNonce(&s.startingNonce, blockNum)
if err != nil {
return nil, err
}
rv, success := secretbox.Open(out, in, &n, &s.key)
if !success {
return nil, Error.New("failed decrypting")
}

View File

@ -20,15 +20,18 @@ func randData(amount int) []byte {
}
func TestSecretbox(t *testing.T) {
key := randData(32)
encrypter, err := NewSecretboxEncrypter(key, 4*1024)
var key [32]byte
copy(key[:], randData(32))
var firstNonce [24]byte
copy(firstNonce[:], randData(24))
encrypter, err := NewSecretboxEncrypter(&key, &firstNonce, 4*1024)
if err != nil {
t.Fatal(err)
}
data := randData(encrypter.InBlockSize() * 10)
encrypted := TransformReader(bytes.NewReader(data),
encrypter, 0)
decrypter, err := NewSecretboxDecrypter(key, 4*1024)
decrypter, err := NewSecretboxDecrypter(&key, &firstNonce, 4*1024)
if err != nil {
t.Fatal(err)
}