Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 25 additions & 25 deletions api/edx25519_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,22 @@ func TestEdX25519Marshal(t *testing.T) {
key.CreatedAt = clock.NowMillis()
key.UpdatedAt = clock.NowMillis()

b, err := msgpack.Marshal(key)
b, err := json.MarshalIndent(key, "", " ")
require.NoError(t, err)
expected := `([]uint8) (len=239 cap=412) {
expected := `{
"id": "kex1fzlrdfy4wlyaturcqkfq92ywj7lft9awtdg70d2yftzhspmc45qsvghhep",
"type": "edx25519",
"priv": "7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+9IvjaklXfJ1fB4BZICqI6XvpWXrltR57VESsV4B3itAQ==",
"pub": "SL42pJV3ydXweAWSAqiOl76Vl65bUee1RErFeAd4rQE=",
"cts": 1234567890001,
"uts": 1234567890002,
"notes": "some test notes"
}`
require.Equal(t, expected, string(b))

b, err = msgpack.Marshal(key)
require.NoError(t, err)
expected = `([]uint8) (len=239 cap=412) {
00000000 87 a2 69 64 d9 3e 6b 65 78 31 66 7a 6c 72 64 66 |..id.>kex1fzlrdf|
00000010 79 34 77 6c 79 61 74 75 72 63 71 6b 66 71 39 32 |y4wlyaturcqkfq92|
00000020 79 77 6a 37 6c 66 74 39 61 77 74 64 67 37 30 64 |ywj7lft9awtdg70d|
Expand All @@ -34,26 +47,13 @@ func TestEdX25519Marshal(t *testing.T) {
00000080 d5 f0 78 05 92 02 a8 8e 97 be 95 97 ae 5b 51 e7 |..x..........[Q.|
00000090 b5 44 4a c5 78 07 78 ad 01 a3 70 75 62 c4 20 48 |.DJ.x.x...pub. H|
000000a0 be 36 a4 95 77 c9 d5 f0 78 05 92 02 a8 8e 97 be |.6..w...x.......|
000000b0 95 97 ae 5b 51 e7 b5 44 4a c5 78 07 78 ad 01 a5 |...[Q..DJ.x.x...|
000000c0 6e 6f 74 65 73 af 73 6f 6d 65 20 74 65 73 74 20 |notes.some test |
000000d0 6e 6f 74 65 73 a3 63 74 73 d3 00 00 01 1f 71 fb |notes.cts.....q.|
000000e0 04 51 a3 75 74 73 d3 00 00 01 1f 71 fb 04 52 |.Q.uts.....q..R|
000000b0 95 97 ae 5b 51 e7 b5 44 4a c5 78 07 78 ad 01 a3 |...[Q..DJ.x.x...|
000000c0 63 74 73 d3 00 00 01 1f 71 fb 04 51 a3 75 74 73 |cts.....q..Q.uts|
000000d0 d3 00 00 01 1f 71 fb 04 52 a5 6e 6f 74 65 73 af |.....q..R.notes.|
000000e0 73 6f 6d 65 20 74 65 73 74 20 6e 6f 74 65 73 |some test notes|
}
`
require.Equal(t, expected, spew.Sdump(b))

b, err = json.MarshalIndent(key, "", " ")
require.NoError(t, err)
expected = `{
"id": "kex1fzlrdfy4wlyaturcqkfq92ywj7lft9awtdg70d2yftzhspmc45qsvghhep",
"type": "edx25519",
"priv": "7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+9IvjaklXfJ1fB4BZICqI6XvpWXrltR57VESsV4B3itAQ==",
"pub": "SL42pJV3ydXweAWSAqiOl76Vl65bUee1RErFeAd4rQE=",
"notes": "some test notes",
"cts": 1234567890001,
"uts": 1234567890002
}`
require.Equal(t, expected, string(b))
}

func TestEdX25519MarshalPublic(t *testing.T) {
Expand All @@ -74,10 +74,10 @@ func TestEdX25519MarshalPublic(t *testing.T) {
00000040 68 68 65 70 a4 74 79 70 65 a8 65 64 78 32 35 35 |hhep.type.edx255|
00000050 31 39 a3 70 75 62 c4 20 48 be 36 a4 95 77 c9 d5 |19.pub. H.6..w..|
00000060 f0 78 05 92 02 a8 8e 97 be 95 97 ae 5b 51 e7 b5 |.x..........[Q..|
00000070 44 4a c5 78 07 78 ad 01 a5 6e 6f 74 65 73 af 73 |DJ.x.x...notes.s|
00000080 6f 6d 65 20 74 65 73 74 20 6e 6f 74 65 73 a3 63 |ome test notes.c|
00000090 74 73 d3 00 00 01 1f 71 fb 04 51 a3 75 74 73 d3 |ts.....q..Q.uts.|
000000a0 00 00 01 1f 71 fb 04 52 |....q..R|
00000070 44 4a c5 78 07 78 ad 01 a3 63 74 73 d3 00 00 01 |DJ.x.x...cts....|
00000080 1f 71 fb 04 51 a3 75 74 73 d3 00 00 01 1f 71 fb |.q..Q.uts.....q.|
00000090 04 52 a5 6e 6f 74 65 73 af 73 6f 6d 65 20 74 65 |.R.notes.some te|
000000a0 73 74 20 6e 6f 74 65 73 |st notes|
}
`
require.Equal(t, expected, spew.Sdump(b))
Expand All @@ -88,9 +88,9 @@ func TestEdX25519MarshalPublic(t *testing.T) {
"id": "kex1fzlrdfy4wlyaturcqkfq92ywj7lft9awtdg70d2yftzhspmc45qsvghhep",
"type": "edx25519",
"pub": "SL42pJV3ydXweAWSAqiOl76Vl65bUee1RErFeAd4rQE=",
"notes": "some test notes",
"cts": 1234567890001,
"uts": 1234567890002
"uts": 1234567890002,
"notes": "some test notes"
}`
require.Equal(t, expected, string(b))
}
68 changes: 68 additions & 0 deletions api/encode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package api

import (
"github.com/keys-pub/keys"
"github.com/keys-pub/keys/encoding"
"github.com/pkg/errors"
"github.com/vmihailenco/msgpack/v4"
)

// Generic key brand.
const keyBrand = "KEY"

// EncodeKey a key with an optional password.
func EncodeKey(key *Key, password string) (string, error) {
if key == nil {
return "", errors.Errorf("no key to encode")
}
marshaled, err := msgpack.Marshal(key)
if err != nil {
return "", err
}
out := keys.EncryptWithPassword(marshaled, password)
return encoding.EncodeSaltpack(out, keyBrand), nil
}

// DecodeKey a key with an optional password.
func DecodeKey(msg string, password string) (*Key, error) {
decoded, brand, err := encoding.DecodeSaltpack(msg, false)
if err != nil {
return nil, errors.Errorf("failed to decode key")
}
b, err := keys.DecryptWithPassword(decoded, password)
if err != nil {
return nil, errors.Errorf("failed to decode key")
}

switch brand {
case keyBrand:
var key Key
if err := msgpack.Unmarshal(b, &key); err != nil {
return nil, errors.Errorf("failed to unmarshal key")
}
if err := key.Check(); err != nil {
return nil, errors.Wrapf(err, "invalid key")
}
return &key, nil
case edx25519Brand:
if len(b) != 64 {
return nil, errors.Errorf("invalid number of bytes for ed25519 seed")
}
sk := keys.NewEdX25519KeyFromPrivateKey(keys.Bytes64(b))
return NewKey(sk), nil
case x25519Brand:
if len(b) != 32 {
return nil, errors.Errorf("invalid number of bytes for x25519 private key")
}
bk := keys.NewX25519KeyFromPrivateKey(keys.Bytes32(b))
return NewKey(bk), nil
default:
return nil, errors.Errorf("invalid key")
}
}

// For EdX25519 key that only contains 64 private key bytes.
const edx25519Brand string = "EDX25519 KEY"

// For X25519 key that only contains 32 private key bytes.
const x25519Brand string = "X25519 KEY"
72 changes: 72 additions & 0 deletions api/encode_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package api_test

import (
"testing"

"github.com/keys-pub/keys"
"github.com/keys-pub/keys/api"
"github.com/keys-pub/keys/encoding"
"github.com/keys-pub/keys/tsutil"
"github.com/stretchr/testify/require"
"github.com/vmihailenco/msgpack/v4"
)

func TestEncode(t *testing.T) {
clock := tsutil.NewTestClock()

key := api.NewKey(keys.GenerateEdX25519Key()).
Created(clock.NowMillis()).
WithLabel("test")

encoded, err := api.EncodeKey(key, "")
require.NoError(t, err)

out, err := api.DecodeKey(encoded, "")
require.NoError(t, err)
require.Equal(t, key, out)

encoded, err = api.EncodeKey(key, "testpassword")
require.NoError(t, err)

out, err = api.DecodeKey(encoded, "testpassword")
require.NoError(t, err)
require.Equal(t, key, out)

_, err = api.DecodeKey(encoded, "invalidpassword")
require.EqualError(t, err, "failed to decode key")

_, err = api.DecodeKey("invaliddata", "")
require.EqualError(t, err, "failed to decode key")

// Empty
var empty struct{}
_, err = api.DecodeKey(encodeStruct(empty, ""), "")
require.EqualError(t, err, "invalid key")

// Invalid msgpack
_, err = api.DecodeKey(encodeBytes([]byte("????"), ""), "")
require.EqualError(t, err, "invalid key")
}

func encodeStruct(i interface{}, password string) string {
b, err := msgpack.Marshal(i)
if err != nil {
panic(err)
}
return encodeBytes(b, password)
}

func encodeBytes(b []byte, password string) string {
return encoding.EncodeSaltpack(keys.EncryptWithPassword(b, password), "")
}

func TestDecodeOld(t *testing.T) {
msg := `BEGIN EDX25519 KEY MESSAGE.
AY6gPAVx9JSUsLg 3K8CNqUyNY87qiL FNNp7UBsIcvObJK mRtDzpcwQU1XpYa
64FF0g4O0sDrhV4 qlp52vdQ5PG77D8 046ZdckukUl6reZ inOEqkDuOg5hynz
k95BEExR31Sqenh rdqT3ADIdPu8f4f aXQaFejAp3Cb.
END EDX25519 KEY MESSAGE.`
out, err := api.DecodeKey(msg, "testpassword")
require.NoError(t, err)
require.Equal(t, keys.ID("kex10x6fdaazp2zy85m6cj7w57y4u0cc99xa3nmwjdldk9l4ajm3yadq70g0js"), out.ID)
}
114 changes: 48 additions & 66 deletions api/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ package api

import (
"github.com/keys-pub/keys"
"github.com/keys-pub/keys/encoding"
"github.com/keys-pub/keys/saltpack"
"github.com/pkg/errors"
"github.com/vmihailenco/msgpack/v4"
)
Expand All @@ -20,11 +18,15 @@ type Key struct {
Private []byte `json:"priv,omitempty" msgpack:"priv,omitempty"`
Public []byte `json:"pub,omitempty" msgpack:"pub,omitempty"`

// Optional fields
Notes string `json:"notes,omitempty" msgpack:"notes,omitempty"`

CreatedAt int64 `json:"cts,omitempty" msgpack:"cts,omitempty"`
UpdatedAt int64 `json:"uts,omitempty" msgpack:"uts,omitempty"`

// Optional fields
Labels []string `json:"labels,omitempty" msgpack:"labels,omitempty"`
Notes string `json:"notes,omitempty" msgpack:"notes,omitempty"`

// Application specific fields
Token string `json:"token,omitempty" msgpack:"token,omitempty"`
}

// NewKey creates api.Key from keys.Key interface.
Expand All @@ -37,81 +39,61 @@ func NewKey(k keys.Key) *Key {
}
}

// EncryptKey creates encrypted key from a sender to a recipient.
func EncryptKey(key *Key, sender *keys.EdX25519Key, recipient keys.ID, armored bool) ([]byte, error) {
b, err := msgpack.Marshal(key)
if err != nil {
return nil, err
}
enc, err := saltpack.Signcrypt(b, armored, sender, recipient, sender.ID())
if err != nil {
return nil, err
}
return enc, nil
// Created marks the key as created with the specified time.
func (k *Key) Created(ts int64) *Key {
k.CreatedAt = ts
k.UpdatedAt = ts
return k
}

// DecryptKey decrypts a key from a sender.
func DecryptKey(b []byte, kr saltpack.Keyring, armored bool) (*Key, *keys.EdX25519PublicKey, error) {
dec, pk, err := saltpack.SigncryptOpen(b, armored, kr)
if err != nil {
return nil, nil, errors.Wrapf(err, "failed to decrypt key")
}
var key Key
if err := msgpack.Unmarshal(dec, &key); err != nil {
return nil, nil, err
}
if err := key.Check(); err != nil {
return nil, nil, err
}
return &key, pk, nil
// Updated marks the key as created with the specified time.
func (k *Key) Updated(ts int64) *Key {
k.UpdatedAt = ts
return k
}

// Check if key is valid (has valid ID and type).
func (k *Key) Check() error {
if _, err := keys.ParseID(string(k.ID)); err != nil {
return err
}
if k.Type == "" {
return errors.Errorf("invalid key type")
// WithLabel returns key with label added.
func (k *Key) WithLabel(label string) *Key {
k.Labels = append(k.Labels, label)
return k
}

// HasLabel returns true if key has label.
func (k Key) HasLabel(label string) bool {
for _, l := range k.Labels {
if l == label {
return true
}
}
return nil
return false
}

// EncryptWithPassword creates an encrypted key using a password.
func (k *Key) EncryptWithPassword(password string) (string, error) {
// Copy creates a copy of the key.
func (k *Key) Copy() *Key {
b, err := msgpack.Marshal(k)
if err != nil {
return "", err
return nil
}
var out Key
if err := msgpack.Unmarshal(b, &out); err != nil {
return nil
}
out := keys.EncryptWithPassword(b, password)
return encoding.EncodeSaltpack(out, "KEY"), nil
return &out
}

// DecryptKeyWithPassword decrypts a key using a password.
func DecryptKeyWithPassword(s string, password string) (*Key, error) {
decoded, brand, err := encoding.DecodeSaltpack(s, false)
if err != nil {
return nil, errors.Wrapf(err, "failed to parse saltpack")
}
// Check if small format (from keys.EncodeSaltpackKey).
switch brand {
case string(keys.EdX25519Brand), string(keys.X25519Brand):
k, err := keys.DecodeSaltpackKey(s, password, false)
if err != nil {
return nil, err
}
return NewKey(k), nil
// Check if key is valid (has valid ID and type).
func (k *Key) Check() error {
if k.ID == "" {
return errors.Errorf("empty id")
}
decrypted, err := keys.DecryptWithPassword(decoded, password)
if err != nil {
return nil, err
if _, err := keys.ParseID(string(k.ID)); err != nil {
return err
}
var key Key
if err := msgpack.Unmarshal(decrypted, &key); err != nil {
return nil, err
if k.Type == "" {
return errors.Errorf("empty type")
}
if err := key.Check(); err != nil {
return nil, err
if len(k.Public) == 0 && len(k.Private) == 0 {
return errors.Errorf("no key data")
}
return &key, nil
return nil
}
Loading