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)
+}