diff --git a/internal/security/cipher/salsa_test.go b/internal/security/cipher/salsa_test.go index 1248a901..80ff08e9 100644 --- a/internal/security/cipher/salsa_test.go +++ b/internal/security/cipher/salsa_test.go @@ -22,10 +22,6 @@ import ( "github.com/stretchr/testify/assert" ) -type x struct { - test string -} - // BenchmarkEncryptKey2-8 5000000 356 ns/op 64 B/op 2 allocs/op func Benchmark_Salsa_EncryptKey(b *testing.B) { cipher := new(Salsa) diff --git a/internal/security/cipher/shuffle.go b/internal/security/cipher/shuffle.go new file mode 100644 index 00000000..8fdd82d6 --- /dev/null +++ b/internal/security/cipher/shuffle.go @@ -0,0 +1,89 @@ +/********************************************************************************** +* Copyright (c) 2009-2019 Misakai Ltd. +* This program is free software: you can redistribute it and/or modify it under the +* terms of the GNU Affero General Public License as published by the Free Software +* Foundation, either version 3 of the License, or(at your option) any later version. +* +* This program is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +* PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License along +* with this program. If not, see. +************************************************************************************/ + +package cipher + +import ( + "encoding/base64" + "errors" + + "github.com/emitter-io/emitter/internal/security" + "golang.org/x/crypto/salsa20/salsa" +) + +// Shuffle represents a security cipher which can encrypt/decrypt security keys. +type Shuffle struct { + key [32]byte + nonce [16]byte +} + +// NewShuffle creates a new shuffled salsa cipher. +func NewShuffle(key, nonce []byte) (*Shuffle, error) { + if len(key) != 32 || len(nonce) != 16 { + return nil, errors.New("shuffled: invalid cryptographic key") + } + + cipher := new(Shuffle) + copy(cipher.key[:], key) + copy(cipher.nonce[:], nonce) + return cipher, nil +} + +// EncryptKey encrypts the key and return a base-64 encoded string. +func (c *Shuffle) EncryptKey(k security.Key) (string, error) { + buffer := make([]byte, 24) + copy(buffer[:], k) + + err := c.crypt(buffer) + return base64.RawURLEncoding.EncodeToString(buffer), err +} + +// DecryptKey decrypts the security key from a base64 encoded string. +func (c *Shuffle) DecryptKey(buffer []byte) (security.Key, error) { + if len(buffer) != 32 { + return nil, errors.New("cipher: the key provided is not valid") + } + + // Warning: we do a base64 decode in the same underlying buffer, to save up + // on memory allocations. Keep in mind that the previous data will be lost. + n, err := decodeKey(buffer, buffer) + if err != nil { + return nil, err + } + + // We now need to resize the slice, since we changed it. + buffer = buffer[:n] + c.crypt(buffer) + + // Return the key on the decrypted buffer. + return security.Key(buffer), nil +} + +// crypt encrypts or decrypts the data and shuffles (recommended). +func (c *Shuffle) crypt(data []byte) error { + buffer := data[2:] + salt := data[0:2] + + // Apply the salt to nonce + var nonce [16]byte + for i := 0; i < 16; i += 2 { + nonce[i] = salt[0] ^ c.nonce[i] + nonce[i+1] = salt[1] ^ c.nonce[i+1] + } + + var subKey [32]byte + salsa.HSalsa20(&subKey, &nonce, &c.key, &salsa.Sigma) + salsa.XORKeyStream(buffer, buffer, &nonce, &subKey) + return nil +} diff --git a/internal/security/cipher/shuffle_test.go b/internal/security/cipher/shuffle_test.go new file mode 100644 index 00000000..fd064fcf --- /dev/null +++ b/internal/security/cipher/shuffle_test.go @@ -0,0 +1,159 @@ +/********************************************************************************** +* Copyright (c) 2009-2019 Misakai Ltd. +* This program is free software: you can redistribute it and/or modify it under the +* terms of the GNU Affero General Public License as published by the Free Software +* Foundation, either version 3 of the License, or(at your option) any later version. +* +* This program is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +* PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License along +* with this program. If not, see. +************************************************************************************/ + +package cipher + +import ( + "crypto/rand" + "testing" + "time" + + "github.com/emitter-io/emitter/internal/security" + "github.com/stretchr/testify/assert" +) + +type x struct { + test string +} + +// Benchmark_Shuffled_EncryptKey-8 4477147 263 ns/op 64 B/op 2 allocs/op +func Benchmark_Shuffled_EncryptKey(b *testing.B) { + cipher := new(Shuffle) + key := "A-dOBQDuXhqoFz-GZZdbpSFCtzmFl7Ng" + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = cipher.EncryptKey([]byte(key)) + } +} + +func Test_Shuffled(t *testing.T) { + cipher := new(Shuffle) + key := security.Key(make([]byte, 24)) + key.SetSalt(999) + key.SetMaster(2) + key.SetContract(123) + key.SetSignature(777) + key.SetPermissions(security.AllowReadWrite) + key.SetTarget("a/b/c/") + key.SetExpires(time.Unix(1497683272, 0).UTC()) + + encoded, err := cipher.EncryptKey(key) + assert.NoError(t, err) + assert.Equal(t, "A-dOBQDuXhqoFz-GZZdbpSFCtzmFl7Ng", encoded) + + decoded, err := cipher.DecryptKey([]byte(encoded)) + assert.NoError(t, err) + assert.Equal(t, key, decoded) +} + +// Benchmark_Shuffled_DecryptKey-8 4857697 245 ns/op 0 B/op 0 allocs/op +func Benchmark_Shuffled_DecryptKey(b *testing.B) { + cipher := new(Shuffle) + key := "A-dOBQDuXhqoFz-GZZdbpSFCtzmFl7Ng" + + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, _ = cipher.DecryptKey([]byte(key)) + } +} + +func Test_Shuffled_Errors(t *testing.T) { + cipher := new(Shuffle) + tests := []struct { + key string + err bool + }{ + + { + key: "", + err: true, + }, + { + key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa*", + err: true, + }, + } + + for _, tc := range tests { + _, err := cipher.DecryptKey([]byte(tc.key)) + assert.Equal(t, tc.err, err != nil, tc.key) + + } +} + +func TestNewShuffled(t *testing.T) { + + // Happy path + { + c, err := NewShuffle(make([]byte, 32), make([]byte, 16)) + assert.NoError(t, err) + assert.NotNil(t, c) + } + + // Error case + { + c, err := NewShuffle(nil, nil) + assert.Error(t, err) + assert.Nil(t, c) + } + +} + +func TestShuffled_Entropy(t *testing.T) { + cryptoKey := make([]byte, 32) + rand.Read(cryptoKey) + + nonce := make([]byte, 16) + rand.Read(nonce) + + c, err := NewShuffle(cryptoKey, nonce) + + key1 := makeKey(111) + key2 := makeKey(333) + + k1, err := c.EncryptKey(key1) + assert.NoError(t, err) + + k2, err := c.EncryptKey(key2) + assert.NoError(t, err) + + var diff int + for i := range k1 { + if k1[i] != k2[i] { + diff++ + } + } + + assert.NotEqual(t, k1, k2) + assert.Greater(t, diff, 20) +} + +func printKey(key security.Key) { + println(key.Salt(), key.Contract(), key.Signature(), key.Expires().String()) +} + +func makeKey(salt int) security.Key { + key := security.Key(make([]byte, 24)) + key.SetSalt(uint16(salt)) + key.SetMaster(2) + key.SetContract(123) + key.SetSignature(777) + key.SetPermissions(security.AllowReadWrite) + key.SetTarget("a/b/c/") + key.SetExpires(time.Unix(1497683272, 0).UTC()) + return key +} diff --git a/internal/security/license/license.go b/internal/security/license/license.go index 15dce160..7e340221 100644 --- a/internal/security/license/license.go +++ b/internal/security/license/license.go @@ -44,7 +44,7 @@ type License interface { // New generates a new license and master key. This uses the most up-to-date version // of the license to generate a new one. func New() (string, string) { - license := NewV2() + license := NewV3() if secret, err := license.NewMasterKey(1); err != nil { panic(err) } else if cipher, err := license.Cipher(); err != nil { @@ -63,10 +63,12 @@ func Parse(data string) (License, error) { } switch { - case strings.HasSuffix(data, ":2"): - return parseV2(data[:len(data)-2]) case strings.HasSuffix(data, ":1"): return parseV1(data[:len(data)-2]) + case strings.HasSuffix(data, ":2"): + return parseV2(data[:len(data)-2]) + case strings.HasSuffix(data, ":3"): + return parseV3(data[:len(data)-2]) default: return parseV1(data) } diff --git a/internal/security/license/v3.go b/internal/security/license/v3.go new file mode 100644 index 00000000..11d6a9fa --- /dev/null +++ b/internal/security/license/v3.go @@ -0,0 +1,110 @@ +/********************************************************************************** +* Copyright (c) 2009-2019 Misakai Ltd. +* This program is free software: you can redistribute it and/or modify it under the +* terms of the GNU Affero General Public License as published by the Free Software +* Foundation, either version 3 of the License, or(at your option) any later version. +* +* This program is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +* PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License along +* with this program. If not, see. +************************************************************************************/ + +package license + +import ( + "crypto/rand" + "encoding/base64" + "math" + "math/big" + + "github.com/emitter-io/emitter/internal/security" + "github.com/emitter-io/emitter/internal/security/cipher" + "github.com/golang/snappy" + "github.com/kelindar/binary" +) + +// V3 represents a v3 license. +type V3 struct { + EncryptionKey []byte // Gets or sets the encryption key. + EncryptionSalt []byte // Gets or sets the encryption key. + User uint32 // Gets or sets the contract id. + Sign uint32 // Gets or sets the signature of the contract. + Index uint32 // Gets or sets the current master. +} + +// NewV3 generates a new v2 license. +func NewV3() *V3 { + return &V3{ + EncryptionKey: randN(32), + EncryptionSalt: randN(16), + User: uint32(be.Uint32(randN(4))), + Sign: uint32(be.Uint32(randN(4))), + Index: 1, + } +} + +// parseV3 decodes the license and verifies it. +func parseV3(data string) (*V3, error) { + + // Decode from base64 first + raw, err := base64.RawURLEncoding.DecodeString(data) + if err != nil { + return nil, err + } + + // Uncompress the bytes + raw, err = snappy.Decode(nil, raw) + if err != nil { + return nil, err + } + + // Unmarshal the license + var license V3 + err = binary.Unmarshal(raw, &license) + return &license, err +} + +// Cipher creates a new cipher for the licence +func (l *V3) Cipher() (Cipher, error) { + return cipher.NewShuffle(l.EncryptionKey, l.EncryptionSalt) +} + +// String converts the license to string. +func (l *V3) String() string { + encoded, _ := binary.Marshal(l) + encoded = snappy.Encode(nil, encoded) + + return base64.RawURLEncoding.EncodeToString(encoded) + ":3" +} + +// Contract retuns the contract ID of the license. +func (l *V3) Contract() uint32 { + return l.User +} + +// Signature returns the signature of the license. +func (l *V3) Signature() uint32 { + return l.Sign +} + +// Master returns the secret key index. +func (l *V3) Master() uint32 { + return l.Index +} + +// NewMasterKey generates a new master key. +func (l *V3) NewMasterKey(id uint16) (key security.Key, err error) { + var n *big.Int + if n, err = rand.Int(rand.Reader, big.NewInt(math.MaxInt16)); err == nil { + key = security.Key(make([]byte, 24)) + key.SetSalt(uint16(n.Uint64())) + key.SetMaster(id) + key.SetContract(l.User) + key.SetSignature(l.Sign) + key.SetPermissions(security.AllowMaster) + } + return +} diff --git a/internal/security/license/v3_test.go b/internal/security/license/v3_test.go new file mode 100644 index 00000000..b316df8e --- /dev/null +++ b/internal/security/license/v3_test.go @@ -0,0 +1,62 @@ +/********************************************************************************** +* Copyright (c) 2009-2019 Misakai Ltd. +* This program is free software: you can redistribute it and/or modify it under the +* terms of the GNU Affero General Public License as published by the Free Software +* Foundation, either version 3 of the License, or(at your option) any later version. +* +* This program is distributed in the hope that it will be useful, but WITHOUT ANY +* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A +* PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. +* +* You should have received a copy of the GNU Affero General Public License along +* with this program. If not, see. +************************************************************************************/ + +package license + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewV3(t *testing.T) { + l := NewV3() + l.User = 0x11248139 + l.Sign = 0x10062b50 + l.Index = 0x1 + assert.NotEqual(t, "", l.EncryptionKey) + assert.Len(t, l.EncryptionKey, 32) + + c, err := l.Cipher() + assert.NotNil(t, c) + assert.NoError(t, err) + + text := l.String() + assert.NotEqual(t, "", text) + assert.Equal(t, ":3", text[len(text)-2:]) + + out, err := parseV3(text[:len(text)-2]) + assert.NoError(t, err) + assert.Equal(t, l, out) + + master, err := l.NewMasterKey(9) + assert.NoError(t, err) + assert.Equal(t, 9, int(master.Master())) +} + +func TestParseV3(t *testing.T) { + l, err := Parse("PfA8IPA43P8Jm4LfESjJYjzPIUG71uFkvKriYQjI4ZO2ABfHEBE36u13MmpHU5AumxGZKXG5gpKJAdDWmIABAQ:3") + assert.NoError(t, err) + assert.Equal(t, uint32(0x11248139), l.Contract()) + assert.Equal(t, uint32(0x10062b50), l.Signature()) + assert.Equal(t, uint32(0x1), l.Master()) +} + +func TestParseV3_Invalid(t *testing.T) { + _, err := Parse("``````````:2") + assert.Error(t, err) + + _, err = Parse("xxxxxx:2") + assert.Error(t, err) +}