Skip to content

Commit

Permalink
Added v3 license with shuffled salsa cipher
Browse files Browse the repository at this point in the history
  • Loading branch information
kelindar committed May 1, 2020
1 parent a1b667f commit 1128c70
Show file tree
Hide file tree
Showing 6 changed files with 425 additions and 7 deletions.
4 changes: 0 additions & 4 deletions internal/security/cipher/salsa_test.go
Expand Up @@ -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)
Expand Down
89 changes: 89 additions & 0 deletions 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<http://www.gnu.org/licenses/>.
************************************************************************************/

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
}
159 changes: 159 additions & 0 deletions 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<http://www.gnu.org/licenses/>.
************************************************************************************/

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
}
8 changes: 5 additions & 3 deletions internal/security/license/license.go
Expand Up @@ -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 {
Expand All @@ -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)
}
Expand Down
110 changes: 110 additions & 0 deletions 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<http://www.gnu.org/licenses/>.
************************************************************************************/

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
}

0 comments on commit 1128c70

Please sign in to comment.