From 40c11414b84a2fe4d80f41cadf08954b7648ee27 Mon Sep 17 00:00:00 2001 From: Gabriel Handford Date: Fri, 1 Jan 2021 18:17:30 -0800 Subject: [PATCH 1/2] tsutil: ParseMillis --- statement.go | 2 +- tsutil/clock.go | 4 ++-- tsutil/tsutil.go | 30 +++++++++++++++++++----------- tsutil/tsutil_test.go | 12 ++++++------ user/result.go | 4 ++-- users/users.go | 6 +++--- 6 files changed, 33 insertions(+), 25 deletions(-) diff --git a/statement.go b/statement.go index 72c4105..5bda26a 100644 --- a/statement.go +++ b/statement.go @@ -223,7 +223,7 @@ func unmarshalJSON(b []byte) (*Statement, error) { if err != nil { return nil, err } - ts := tsutil.ConvertMillis(stf.Timestamp) + ts := tsutil.ParseMillis(stf.Timestamp) if !bytes.Equal(stf.Sig, sigBytes) { return nil, errors.Errorf("sig bytes mismatch") diff --git a/tsutil/clock.go b/tsutil/clock.go index 169572a..f1b9d74 100644 --- a/tsutil/clock.go +++ b/tsutil/clock.go @@ -24,7 +24,7 @@ type testClock struct { // NewTestClock returns a test Clock starting at 1234567890000 millseconds since // epoch. Each access to Now() increases time by 1 millisecond. func NewTestClock() Clock { - t := ConvertMillis(1234567890000) + t := ParseMillis(1234567890000) return &testClock{ t: t, tick: time.Millisecond, @@ -53,7 +53,7 @@ func (c *testClock) Add(dt time.Duration) { // NewTestClockAt creates a Clock starting at timestamp (millis). func NewTestClockAt(ts int64) Clock { - t := ConvertMillis(ts) + t := ParseMillis(ts) return &testClock{ t: t, tick: time.Millisecond, diff --git a/tsutil/tsutil.go b/tsutil/tsutil.go index 3452925..8b69135 100644 --- a/tsutil/tsutil.go +++ b/tsutil/tsutil.go @@ -20,30 +20,38 @@ func Millis(t time.Time) int64 { return int64(t.UnixNano() / int64(time.Millisecond)) } -// MillisNow returns now in milliseconds since epoch. -func MillisNow() int64 { +// NowMillis returns now in milliseconds since epoch. +func NowMillis() int64 { return Millis(time.Now()) } -// ParseMillis returns time.Time from milliseconds since epoch as string. -func ParseMillis(s string) time.Time { - n, err := strconv.ParseInt(s, 10, 64) - if err != nil { +// ParseMillis returns time.Time from milliseconds since epoch. +func ParseMillis(i interface{}) time.Time { + switch v := i.(type) { + case string: + n, err := strconv.ParseInt(v, 10, 64) + if err != nil { + return time.Time{} + } + return millis(n) + case int64: + return millis(v) + case int: + return millis(int64(v)) + default: return time.Time{} } - return ConvertMillis(n) - } -// ConvertMillis returns time.Time from milliseconds since epoch. -func ConvertMillis(n int64) time.Time { +// millis returns time.Time from milliseconds since epoch. +func millis(n int64) time.Time { if n == 0 { return time.Time{} } return time.Unix(0, n*int64(time.Millisecond)).UTC() } -// Days returns milliseconds since epoch to t. +// Days returns days since epoch to t. func Days(t time.Time) int { ms := Millis(t) return int(ms / 1000 / 60 / 60 / 24) diff --git a/tsutil/tsutil_test.go b/tsutil/tsutil_test.go index d6da817..4fcf7a9 100644 --- a/tsutil/tsutil_test.go +++ b/tsutil/tsutil_test.go @@ -13,13 +13,13 @@ import ( func TestParseMillis(t *testing.T) { t1 := time.Now().UTC() ts1 := tsutil.Millis(t1) - t2 := tsutil.ConvertMillis(ts1) + t2 := tsutil.ParseMillis(ts1) require.Equal(t, t1.Format(time.StampMilli), t2.Format(time.StampMilli)) require.Equal(t, int64(0), tsutil.Millis(time.Time{})) - require.Equal(t, time.Time{}, tsutil.ConvertMillis(0)) + require.Equal(t, time.Time{}, tsutil.ParseMillis(0)) - t3 := tsutil.ConvertMillis(1234567890001) + t3 := tsutil.ParseMillis(1234567890001) tf3 := t3.Format(http.TimeFormat) require.Equal(t, "Fri, 13 Feb 2009 23:31:30 GMT", tf3) tf3 = t3.Format(tsutil.RFC3339Milli) @@ -30,13 +30,13 @@ func TestParseMillis(t *testing.T) { require.Equal(t, "2009-02-13T23:31:30.001Z", tf4) require.Equal(t, int64(1234567890001), tsutil.Millis(t4)) - t5 := tsutil.ConvertMillis(1234567890001) + t5 := tsutil.ParseMillis(1234567890001) tf5 := t5.Format(tsutil.RFC3339Milli) require.Equal(t, "2009-02-13T23:31:30.001Z", tf5) } func TestRFC3339Milli(t *testing.T) { - t1 := tsutil.ConvertMillis(1234567890010) + t1 := tsutil.ParseMillis(1234567890010) s1 := t1.Format(tsutil.RFC3339Milli) require.Equal(t, "2009-02-13T23:31:30.010Z", s1) tout, err := time.Parse(tsutil.RFC3339Milli, s1) @@ -45,7 +45,7 @@ func TestRFC3339Milli(t *testing.T) { } func TestDays(t *testing.T) { - t1 := tsutil.ConvertMillis(1234567890001) + t1 := tsutil.ParseMillis(1234567890001) days := tsutil.Days(t1) require.Equal(t, "14288", fmt.Sprintf("%d", days)) } diff --git a/user/result.go b/user/result.go index 563768e..1852844 100644 --- a/user/result.go +++ b/user/result.go @@ -36,12 +36,12 @@ func (r Result) String() string { // IsTimestampExpired returns true if result Timestamp is older than dt. func (r Result) IsTimestampExpired(now time.Time, dt time.Duration) bool { - ts := tsutil.ConvertMillis(r.Timestamp) + ts := tsutil.ParseMillis(r.Timestamp) return (ts.IsZero() || now.Sub(ts) > dt) } // IsVerifyExpired returns true if result VerifiedAt is older than dt. func (r Result) IsVerifyExpired(now time.Time, dt time.Duration) bool { - ts := tsutil.ConvertMillis(r.VerifiedAt) + ts := tsutil.ParseMillis(r.VerifiedAt) return (ts.IsZero() || now.Sub(ts) > dt) } diff --git a/users/users.go b/users/users.go index ae8aeb5..dcf90fc 100644 --- a/users/users.go +++ b/users/users.go @@ -273,7 +273,7 @@ func (u *Users) index(ctx context.Context, keyDoc *keyDocument) error { index = true case user.StatusConnFailure: // If connection failure is recent, still index. - if u.opts.Clock.Now().Sub(tsutil.ConvertMillis(keyDoc.Result.VerifiedAt)) < time.Hour*24*2 { + if u.opts.Clock.Now().Sub(tsutil.ParseMillis(keyDoc.Result.VerifiedAt)) < time.Hour*24*2 { index = true } } @@ -378,10 +378,10 @@ func (u *Users) Expired(ctx context.Context, dt time.Duration, maxAge time.Durat return nil, err } if keyDoc.Result != nil { - ts := tsutil.ConvertMillis(keyDoc.Result.Timestamp) + ts := tsutil.ParseMillis(keyDoc.Result.Timestamp) // If verifiedAt age is too old skip it - vts := tsutil.ConvertMillis(keyDoc.Result.VerifiedAt) + vts := tsutil.ParseMillis(keyDoc.Result.VerifiedAt) if !vts.IsZero() && u.opts.Clock.Now().Sub(vts) > maxAge { continue } From a1195e2939f37ef40eea7167962aaa3c64fb63f3 Mon Sep 17 00:00:00 2001 From: Gabriel Handford Date: Fri, 1 Jan 2021 18:19:29 -0800 Subject: [PATCH 2/2] api: ParseKey --- api/edx25519_test.go | 50 +++++----- api/encode.go | 68 +++++++++++++ api/encode_test.go | 72 ++++++++++++++ api/key.go | 114 +++++++++------------- api/key_test.go | 48 --------- api/parse.go | 35 +++++++ parse_test.go => api/parse_test.go | 36 +++++-- api/rsa_test.go | 12 +-- api/testdata/rsa.json | 4 +- api/testdata/rsa.msgpack | 10 +- api/x25519_test.go | 26 ++--- detect.go | 33 ------- detect_test.go | 37 ------- encode.go | 148 ---------------------------- encode_test.go | 150 ----------------------------- go.sum | 1 + http/auth.go | 2 +- parse.go | 26 ----- ssh.go | 30 ++++++ ssh_test.go | 88 +++++++++++++++++ 20 files changed, 420 insertions(+), 570 deletions(-) create mode 100644 api/encode.go create mode 100644 api/encode_test.go create mode 100644 api/parse.go rename parse_test.go => api/parse_test.go (55%) delete mode 100644 detect.go delete mode 100644 detect_test.go delete mode 100644 encode.go delete mode 100644 encode_test.go delete mode 100644 parse.go diff --git a/api/edx25519_test.go b/api/edx25519_test.go index be98a2e..bc9ad98 100644 --- a/api/edx25519_test.go +++ b/api/edx25519_test.go @@ -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| @@ -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) { @@ -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)) @@ -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)) } diff --git a/api/encode.go b/api/encode.go new file mode 100644 index 0000000..94e2c52 --- /dev/null +++ b/api/encode.go @@ -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" diff --git a/api/encode_test.go b/api/encode_test.go new file mode 100644 index 0000000..e19dc10 --- /dev/null +++ b/api/encode_test.go @@ -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) +} diff --git a/api/key.go b/api/key.go index 24f3238..2bd5b09 100644 --- a/api/key.go +++ b/api/key.go @@ -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" ) @@ -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. @@ -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 } diff --git a/api/key_test.go b/api/key_test.go index 12124b3..a267825 100644 --- a/api/key_test.go +++ b/api/key_test.go @@ -7,9 +7,6 @@ import ( "github.com/keys-pub/keys" "github.com/keys-pub/keys/api" - "github.com/keys-pub/keys/saltpack" - "github.com/keys-pub/keys/tsutil" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -75,51 +72,6 @@ func TestNewKeyX25519Public(t *testing.T) { require.Equal(t, spk, key.AsX25519Public()) } -func TestEncryptKey(t *testing.T) { - alice := keys.NewEdX25519KeyFromSeed(testSeed(0x01)) - bob := keys.NewEdX25519KeyFromSeed(testSeed(0x02)) - clock := tsutil.NewTestClock() - - key := api.NewKey(keys.NewEdX25519KeyFromSeed(testSeed(0xef))) - key.Notes = "some test notes" - key.CreatedAt = clock.NowMillis() - key.UpdatedAt = clock.NowMillis() - - out, err := api.EncryptKey(key, alice, bob.ID(), false) - require.NoError(t, err) - - dec, pk, err := api.DecryptKey(out, saltpack.NewKeyring(bob), false) - require.NoError(t, err) - require.Equal(t, alice.ID(), pk.ID()) - assert.ObjectsAreEqual(dec, key) - - _, _, err = api.DecryptKey(out, saltpack.NewKeyring(), false) - require.EqualError(t, err, "failed to decrypt key: no decryption key found for message") - - _, _, err = api.DecryptKey(out, saltpack.NewKeyring(bob), true) - require.EqualError(t, err, "failed to decrypt key: invalid data") -} - -func TestEncryptKeyWithPassword(t *testing.T) { - clock := tsutil.NewTestClock() - - key := api.NewKey(keys.NewEdX25519KeyFromSeed(testSeed(0x01))) - key.Notes = "some test notes" - key.CreatedAt = clock.NowMillis() - key.UpdatedAt = clock.NowMillis() - - out, err := key.EncryptWithPassword("testpassword") - require.NoError(t, err) - - dec, err := api.DecryptKeyWithPassword(out, "testpassword") - require.NoError(t, err) - assert.ObjectsAreEqual(dec, key) - - // TODO: Invalid password error - _, err = api.DecryptKeyWithPassword(out, "invalidpassword") - require.EqualError(t, err, "failed to decrypt with a password: secretbox open failed") -} - func testSeed(b byte) *[32]byte { return keys.Bytes32(bytes.Repeat([]byte{b}, 32)) } diff --git a/api/parse.go b/api/parse.go new file mode 100644 index 0000000..77c3206 --- /dev/null +++ b/api/parse.go @@ -0,0 +1,35 @@ +package api + +import ( + "strings" + + "github.com/keys-pub/keys" +) + +// ParseKey tries to determine what key type and parses the key bytes. +func ParseKey(b []byte, password string) (*Key, error) { + s := strings.TrimSpace(string(b)) + + kid, err := keys.ParseID(s) + if err == nil { + return NewKey(kid), nil + } + + if strings.HasPrefix(s, "ssh-") { + out, err := keys.ParseSSHPublicKey(s) + if err != nil { + return nil, err + } + return NewKey(out), err + } + + if strings.HasPrefix(s, "-----BEGIN ") { + out, err := keys.ParseSSHKey([]byte(s), []byte(password), true) + if err != nil { + return nil, err + } + return NewKey(out), err + } + + return DecodeKey(s, password) +} diff --git a/parse_test.go b/api/parse_test.go similarity index 55% rename from parse_test.go rename to api/parse_test.go index 3559978..6722a42 100644 --- a/parse_test.go +++ b/api/parse_test.go @@ -1,23 +1,24 @@ -package keys_test +package api_test import ( "testing" "github.com/keys-pub/keys" + "github.com/keys-pub/keys/api" "github.com/stretchr/testify/require" ) -func TestParseKey(t *testing.T) { +func TestParse(t *testing.T) { kid := "kex1nc345hg9nt3eef8rfz3r2uu2psma8umf54tx8z8meyvmnzeglk8s50xu7y" - key, err := keys.ParseKey([]byte(kid), "") + key, err := api.ParseKey([]byte(kid), "") require.NoError(t, err) - require.Equal(t, keys.ID("kex1nc345hg9nt3eef8rfz3r2uu2psma8umf54tx8z8meyvmnzeglk8s50xu7y"), key.ID()) + require.Equal(t, keys.ID("kex1nc345hg9nt3eef8rfz3r2uu2psma8umf54tx8z8meyvmnzeglk8s50xu7y"), key.ID) // SSH public ed25519 edPub := `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ4jWl0FmuOcpONIojVzigw30/NppVZjiPvJGbmLKP2P [email protected]` - key, err = keys.ParseKey([]byte(edPub), "") + key, err = api.ParseKey([]byte(edPub), "") require.NoError(t, err) - require.Equal(t, keys.ID("kex1nc345hg9nt3eef8rfz3r2uu2psma8umf54tx8z8meyvmnzeglk8s50xu7y"), key.ID()) + require.Equal(t, keys.ID("kex1nc345hg9nt3eef8rfz3r2uu2psma8umf54tx8z8meyvmnzeglk8s50xu7y"), key.ID) // SSH private ed25519 edPriv := `-----BEGIN OPENSSH PRIVATE KEY----- @@ -27,9 +28,9 @@ func TestParseKey(t *testing.T) { AAAED2F09VUc5ig2cF/HpYJQM6Jzin26cDxFGELnR5HRIF3Z4jWl0FmuOcpONIojVzigw3 0/NppVZjiPvJGbmLKP2PAAAADWdhYmVAb2subG9jYWw= -----END OPENSSH PRIVATE KEY-----` - key, err = keys.ParseKey([]byte(edPriv), "") + key, err = api.ParseKey([]byte(edPriv), "") require.NoError(t, err) - require.Equal(t, keys.ID("kex1nc345hg9nt3eef8rfz3r2uu2psma8umf54tx8z8meyvmnzeglk8s50xu7y"), key.ID()) + require.Equal(t, keys.ID("kex1nc345hg9nt3eef8rfz3r2uu2psma8umf54tx8z8meyvmnzeglk8s50xu7y"), key.ID) // Saltpack sp := `BEGIN EDX25519 KEY MESSAGE. @@ -37,7 +38,22 @@ func TestParseKey(t *testing.T) { TF3WxURVAhzQmNY uJoEJXKYiWJIY4K gMQTVtndovcxjho KBu5yu4Wm7nM6Bh mjqGVIo5r0NXW4N ZsKF3NJ01o98tpJ 9KrsbBFsBd2V. END EDX25519 KEY MESSAGE.` - key, err = keys.ParseKey([]byte(sp), "testpassword") + key, err = api.ParseKey([]byte(sp), "testpassword") require.NoError(t, err) - require.Equal(t, keys.ID("kex1lm9tc5cmgr0u4tg7q2tl9fxzdcke89c5sl8jnjpkr6erv88m4nvq5cg7n8"), key.ID()) + require.Equal(t, keys.ID("kex1lm9tc5cmgr0u4tg7q2tl9fxzdcke89c5sl8jnjpkr6erv88m4nvq5cg7n8"), key.ID) + + // API + ak := `BEGIN KEY MESSAGE. + Z7SesuE0476OHgx zzBpDJEvtBIyvoh Eu6n8n2GraBGi4C Mn5PBpSxOUXYmtv + egz6h4JDxL2UkJL v47Yc4poDkkUcro 9tysFbWqf6oeXiJ CTrSWP9s6gaLZMg + hBvwd1RW1ifzjDz 1l9Bzi1g3CqWmn1 DUgIxOA6EADOxYP 4RfPgpGpUxGNrBH + JdH0L5km70jlpx3 BD1mfWV3r39HWbd x8lM1c3kIV8P0DF sjt7e5w8x30dNG3 + FXem1iDVpR0C6tk VHAgHbb7Ik44CbZ 3h4KfcJMsv2wvzq kiN9BGWl8swIuGY + KWcZYo1P7lZPaKo K3rnMUvTkis4XWL 1URW1r2810ASXvh XYLetugWobu0PPl + FIinNIJ9w044N8x MYixY8rboFIHdxn fsEueZrN4zXUzBE VxTdljPsRGVOsj1 + G6mQQPYrQy1O. + END KEY MESSAGE.` + key, err = api.ParseKey([]byte(ak), "") + require.NoError(t, err) + require.Equal(t, keys.ID("kex135n03rgjrenhac4r92y4stfcy84rcc3tcgpwm6yl55uc0hg7h4eq3ntlut"), key.ID) } diff --git a/api/rsa_test.go b/api/rsa_test.go index 6a38665..c68742e 100644 --- a/api/rsa_test.go +++ b/api/rsa_test.go @@ -81,10 +81,10 @@ func TestRSAMarshalPublic(t *testing.T) { 00000120 6d 78 77 97 38 6d 50 fe 0c 97 34 be 96 7c 7d 84 |mxw.8mP...4..|}.| 00000130 ae 5b 8f 34 9b 09 40 79 45 7c 0c 0c 6f ee 34 c4 |.[.4..@yE|..o.4.| 00000140 2a 0b 83 26 03 80 4f 71 e4 9f 33 20 08 16 37 51 |*..&..Oq..3 ..7Q| - 00000150 2c 6c bf 2b b8 1b 6f 6b e2 39 84 6d 02 01 03 a5 |,l.+..ok.9.m....| - 00000160 6e 6f 74 65 73 af 73 6f 6d 65 20 74 65 73 74 20 |notes.some test | - 00000170 6e 6f 74 65 73 a3 63 74 73 d3 00 00 01 1f 71 fb |notes.cts.....q.| - 00000180 04 51 a3 75 74 73 d3 00 00 01 1f 71 fb 04 52 |.Q.uts.....q..R| + 00000150 2c 6c bf 2b b8 1b 6f 6b e2 39 84 6d 02 01 03 a3 |,l.+..ok.9.m....| + 00000160 63 74 73 d3 00 00 01 1f 71 fb 04 51 a3 75 74 73 |cts.....q..Q.uts| + 00000170 d3 00 00 01 1f 71 fb 04 52 a5 6e 6f 74 65 73 af |.....q..R.notes.| + 00000180 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)) @@ -95,9 +95,9 @@ func TestRSAMarshalPublic(t *testing.T) { "id": "rsa1lg8lhzpatgmakvrkz866fehw64lkdtly3t2q7d36kfyhmaauyg2sgkhan4", "type": "rsa", "pub": "MIIBBwKCAQBxY8hCshkKiXCUKydkrtQtQSRke28w4JotocDiVqou4k55DEDJakvWbXXDcakV4HA8R2tOGgbxvTjFo8EK470w9O9ipapPUSrRRaBsSOlkaaIs6OYh4FLwZpqMNBVVEtguVUR/C34Y2pS9kRrHs6q+cGhDZolkWT7nGy5eSEvPDHg0EBq11hu6HmPmI3r0BInONqJg2rcK3U++wk1lnbD3ysCZsKOqRUms3n/IWKeTqXXmz2XKJ2t0NSXwiDmA9q0Gm+w0bXh3lzhtUP4MlzS+lnx9hK5bjzSbCUB5RXwMDG/uNMQqC4MmA4BPceSfMyAIFjdRLGy/K7gbb2viOYRtAgED", - "notes": "some test notes", "cts": 1234567890001, - "uts": 1234567890002 + "uts": 1234567890002, + "notes": "some test notes" }` require.Equal(t, expected, string(b)) } diff --git a/api/testdata/rsa.json b/api/testdata/rsa.json index 8c6f6d4..19732b2 100644 --- a/api/testdata/rsa.json +++ b/api/testdata/rsa.json @@ -3,7 +3,7 @@ "type": "rsa", "priv": "MIIEnwIBAAKCAQBxY8hCshkKiXCUKydkrtQtQSRke28w4JotocDiVqou4k55DEDJakvWbXXDcakV4HA8R2tOGgbxvTjFo8EK470w9O9ipapPUSrRRaBsSOlkaaIs6OYh4FLwZpqMNBVVEtguVUR/C34Y2pS9kRrHs6q+cGhDZolkWT7nGy5eSEvPDHg0EBq11hu6HmPmI3r0BInONqJg2rcK3U++wk1lnbD3ysCZsKOqRUms3n/IWKeTqXXmz2XKJ2t0NSXwiDmA9q0Gm+w0bXh3lzhtUP4MlzS+lnx9hK5bjzSbCUB5RXwMDG/uNMQqC4MmA4BPceSfMyAIFjdRLGy/K7gbb2viOYRtAgEDAoIBAEuX2tchZgcGSw1yGkMfOB4rbZhSSiCVvB5r1ew5xsnsNFCy1ducMo7zo9ehG2Pq9X2E8jQRWfZ+JdkX1gdCfiCjSkHDxt+LceDZFZ2F8O2bwXNF7sFAN0rvEbLNY44MkB7jgv9c/rs8YykLZy/NHH71mteZsO2Q1JoSHumFh99cwWHFhLxYh64qFeeH6Gqx6AM2YVBWHgs7OuKOvc8yzUbf8xftPht1kMwwDR1XySiEYtBtn74JflK3DcT8oxOuCZBuX6sMJHKbVP41zDj+FJZBmpAvNfCEYJUr1Hg+DpMLqLUg+D6v5vpliburbk9LxcKFZyyZ9QVe7GoqMLBueGsCgYEAummUj4MMKWJC2mv5rj/dt2pj2/B2HtP2RLypai4et1/Ru9nNk8cjMLzCqXz6/RLuJ7/eD7asFS3y7EqxKxEmW0G8tTHjnzR/3wnpVipuWnwCDGU032HJVd13LMe51GH97qLzuDZjMCz+VlbCNdSslMgWWK0XmRnN7Yqxvh6ao2kCgYEAm7fTRBhFJtKcaJ7d8BQb9l8BNHfjayYOMq5CxoCyxa2pGBv/Mrnxv73Twp9Z/MP0ue5M5nZtGMovpP5cGdJLQ2w5p4H3opcuWeYW9Yyru2EyCEAI/hD/Td3QVP0ukc19BDuPl5WgeIFs218uiVOU4pw3w+Et5B1PZ/F+ZLr5LGUCgYB8RmMKV11w7CyRnVEe1T56Ru09Svlp4qQt0xucHr8k6ovSkTO32hd10yxw/fyot0lv1T61JHK4yUydhyDHYMQ81n3OIUJqIv/qBpuOxvQ8UqwIQ3iU69uOk6TIhSaNlqlJwffQJEIgHf7kOdbOjchjMA7lyLpmETPzscvUFGcXmwKBgGfP4i1lg283EvBp6Uq4EqQ/ViL6l5zECXce1y8Ady5zxhASqiHRS9UpN9cU5qiCoyae3e75nhCGym3+6BE23Nede8UBT8G6HuaZZKOzHSeWIVrVW1QLVN6T4DioybaI/gLSX7pjwFBWSJI/dFuNDexoJS1AyUK+NO/2VEMnUMhDAoGAOsdn3Prnh/mjC95vraHCLap0bRBSexMdx77ImHgtFUUcSaT8DJHs+NZw1RdMSZA0J+zVQ8q7B11jIgz5hMz+chedwoRjTL7a8VRTKHFmmBH0zlEuV7L79w6HkRCQVRg10GUN6heGLv0aOHbPdobcuVDH4sgOqpT1QnOuce34sQs=", "pub": "MIIBBwKCAQBxY8hCshkKiXCUKydkrtQtQSRke28w4JotocDiVqou4k55DEDJakvWbXXDcakV4HA8R2tOGgbxvTjFo8EK470w9O9ipapPUSrRRaBsSOlkaaIs6OYh4FLwZpqMNBVVEtguVUR/C34Y2pS9kRrHs6q+cGhDZolkWT7nGy5eSEvPDHg0EBq11hu6HmPmI3r0BInONqJg2rcK3U++wk1lnbD3ysCZsKOqRUms3n/IWKeTqXXmz2XKJ2t0NSXwiDmA9q0Gm+w0bXh3lzhtUP4MlzS+lnx9hK5bjzSbCUB5RXwMDG/uNMQqC4MmA4BPceSfMyAIFjdRLGy/K7gbb2viOYRtAgED", - "notes": "some test notes", "cts": 1234567890001, - "uts": 1234567890002 + "uts": 1234567890002, + "notes": "some test notes" } \ No newline at end of file diff --git a/api/testdata/rsa.msgpack b/api/testdata/rsa.msgpack index 134dae7..f76d73c 100644 --- a/api/testdata/rsa.msgpack +++ b/api/testdata/rsa.msgpack @@ -1,4 +1,4 @@ -([]uint8) (len=1594 cap=3149) { +([]uint8) (len=1594 cap=3143) { 00000000 87 a2 69 64 d9 3e 72 73 61 31 6c 67 38 6c 68 7a |..id.>rsa1lg8lhz| 00000010 70 61 74 67 6d 61 6b 76 72 6b 7a 38 36 36 66 65 |patgmakvrkz866fe| 00000020 68 77 36 34 6c 6b 64 74 6c 79 33 74 32 71 37 64 |hw64lkdtly3t2q7d| @@ -95,8 +95,8 @@ 000005d0 6d 50 fe 0c 97 34 be 96 7c 7d 84 ae 5b 8f 34 9b |mP...4..|}..[.4.| 000005e0 09 40 79 45 7c 0c 0c 6f ee 34 c4 2a 0b 83 26 03 |.@yE|..o.4.*..&.| 000005f0 80 4f 71 e4 9f 33 20 08 16 37 51 2c 6c bf 2b b8 |.Oq..3 ..7Q,l.+.| - 00000600 1b 6f 6b e2 39 84 6d 02 01 03 a5 6e 6f 74 65 73 |.ok.9.m....notes| - 00000610 af 73 6f 6d 65 20 74 65 73 74 20 6e 6f 74 65 73 |.some test notes| - 00000620 a3 63 74 73 d3 00 00 01 1f 71 fb 04 51 a3 75 74 |.cts.....q..Q.ut| - 00000630 73 d3 00 00 01 1f 71 fb 04 52 |s.....q..R| + 00000600 1b 6f 6b e2 39 84 6d 02 01 03 a3 63 74 73 d3 00 |.ok.9.m....cts..| + 00000610 00 01 1f 71 fb 04 51 a3 75 74 73 d3 00 00 01 1f |...q..Q.uts.....| + 00000620 71 fb 04 52 a5 6e 6f 74 65 73 af 73 6f 6d 65 20 |q..R.notes.some | + 00000630 74 65 73 74 20 6e 6f 74 65 73 |test notes| } diff --git a/api/x25519_test.go b/api/x25519_test.go index 8c65dc4..8d17952 100644 --- a/api/x25519_test.go +++ b/api/x25519_test.go @@ -22,7 +22,7 @@ func TestX25519Marshal(t *testing.T) { b, err := msgpack.Marshal(key) require.NoError(t, err) - expected := `([]uint8) (len=205 cap=389) { + expected := `([]uint8) (len=205 cap=395) { 00000000 87 a2 69 64 d9 3e 6b 62 78 31 30 71 64 79 79 78 |..id.>kbx10qdyyx| 00000010 6c 7a 6c 6d 68 32 74 65 68 78 33 65 6a 32 79 78 |lzlmh2tehx3ej2yx| 00000020 77 64 34 77 7a 71 70 66 39 6e 6c 35 38 61 77 74 |wd4wzqpf9nl58awt| @@ -32,10 +32,10 @@ func TestX25519Marshal(t *testing.T) { 00000060 ef ef ef ef ef ef ef ef ef ef ef ef ef ef ef ef |................| 00000070 ef ef ef ef ef ef ef a3 70 75 62 c4 20 78 1a 42 |........pub. x.B| 00000080 1b e2 fe ee a5 e6 e6 8e 64 a2 19 cd ab 84 00 a4 |........d.......| - 00000090 b3 fd 0f d7 2f 51 7a 52 2a f3 a9 26 56 a5 6e 6f |..../QzR*..&V.no| - 000000a0 74 65 73 af 73 6f 6d 65 20 74 65 73 74 20 6e 6f |tes.some test no| - 000000b0 74 65 73 a3 63 74 73 d3 00 00 01 1f 71 fb 04 51 |tes.cts.....q..Q| - 000000c0 a3 75 74 73 d3 00 00 01 1f 71 fb 04 52 |.uts.....q..R| + 00000090 b3 fd 0f d7 2f 51 7a 52 2a f3 a9 26 56 a3 63 74 |..../QzR*..&V.ct| + 000000a0 73 d3 00 00 01 1f 71 fb 04 51 a3 75 74 73 d3 00 |s.....q..Q.uts..| + 000000b0 00 01 1f 71 fb 04 52 a5 6e 6f 74 65 73 af 73 6f |...q..R.notes.so| + 000000c0 6d 65 20 74 65 73 74 20 6e 6f 74 65 73 |me test notes| } ` require.Equal(t, expected, spew.Sdump(b)) @@ -47,9 +47,9 @@ func TestX25519Marshal(t *testing.T) { "type": "x25519", "priv": "7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+/v7+8=", "pub": "eBpCG+L+7qXm5o5kohnNq4QApLP9D9cvUXpSKvOpJlY=", - "notes": "some test notes", "cts": 1234567890001, - "uts": 1234567890002 + "uts": 1234567890002, + "notes": "some test notes" }` require.Equal(t, expected, string(b)) } @@ -72,10 +72,10 @@ func TestX25519MarshalPublic(t *testing.T) { 00000040 63 37 33 66 a4 74 79 70 65 a6 78 32 35 35 31 39 |c73f.type.x25519| 00000050 a3 70 75 62 c4 20 78 1a 42 1b e2 fe ee a5 e6 e6 |.pub. x.B.......| 00000060 8e 64 a2 19 cd ab 84 00 a4 b3 fd 0f d7 2f 51 7a |.d.........../Qz| - 00000070 52 2a f3 a9 26 56 a5 6e 6f 74 65 73 af 73 6f 6d |R*..&V.notes.som| - 00000080 65 20 74 65 73 74 20 6e 6f 74 65 73 a3 63 74 73 |e test notes.cts| - 00000090 d3 00 00 01 1f 71 fb 04 51 a3 75 74 73 d3 00 00 |.....q..Q.uts...| - 000000a0 01 1f 71 fb 04 52 |..q..R| + 00000070 52 2a f3 a9 26 56 a3 63 74 73 d3 00 00 01 1f 71 |R*..&V.cts.....q| + 00000080 fb 04 51 a3 75 74 73 d3 00 00 01 1f 71 fb 04 52 |..Q.uts.....q..R| + 00000090 a5 6e 6f 74 65 73 af 73 6f 6d 65 20 74 65 73 74 |.notes.some test| + 000000a0 20 6e 6f 74 65 73 | notes| } ` require.Equal(t, expected, spew.Sdump(b)) @@ -86,9 +86,9 @@ func TestX25519MarshalPublic(t *testing.T) { "id": "kbx10qdyyxlzlmh2tehx3ej2yxwd4wzqpf9nl58awt630ffz4uafyetqhsc73f", "type": "x25519", "pub": "eBpCG+L+7qXm5o5kohnNq4QApLP9D9cvUXpSKvOpJlY=", - "notes": "some test notes", "cts": 1234567890001, - "uts": 1234567890002 + "uts": 1234567890002, + "notes": "some test notes" }` require.Equal(t, expected, string(b)) } diff --git a/detect.go b/detect.go deleted file mode 100644 index 18822c9..0000000 --- a/detect.go +++ /dev/null @@ -1,33 +0,0 @@ -package keys - -import ( - "strings" - "unicode/utf8" -) - -// DetectEncoding tries to find out what encoding the bytes are. -// Returns bytes which may be different from input (for example, if whitespace is stripped). -func DetectEncoding(b []byte) ([]byte, Encoding) { - if !utf8.Valid(b) { - return b, UnknownEncoding - } - - s := strings.TrimSpace(string(b)) - - typ := UnknownEncoding - - if _, err := ParseID(s); err == nil { - typ = IDEncoding - } else if len(s) < 100 && (strings.HasPrefix(s, "kex1") || strings.HasPrefix(s, "kbx1")) { - typ = IDEncoding - } else if strings.Contains(s, "BEGIN ") && strings.Contains(s, " MESSAGE") { - typ = SaltpackEncoding - } else if strings.HasPrefix(s, "-----BEGIN ") { - typ = SSHEncoding - } else if strings.HasPrefix(s, "ssh-") { - typ = SSHEncoding - } - - return []byte(s), typ - -} diff --git a/detect_test.go b/detect_test.go deleted file mode 100644 index ab031b7..0000000 --- a/detect_test.go +++ /dev/null @@ -1,37 +0,0 @@ -package keys_test - -import ( - "testing" - - "github.com/keys-pub/keys" - "github.com/stretchr/testify/require" -) - -func TestDetect(t *testing.T) { - _, typ := keys.DetectEncoding([]byte("kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077")) - require.Equal(t, keys.IDEncoding, typ) - - _, typ = keys.DetectEncoding([]byte("kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph07")) - require.Equal(t, keys.IDEncoding, typ) - - _, typ = keys.DetectEncoding([]byte("kex132yw8ht5p8cetl2mvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077")) - require.Equal(t, keys.IDEncoding, typ) - - _, typ = keys.DetectEncoding([]byte("kex132yw8ht5p8cetl2mvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077 ")) - require.Equal(t, keys.IDEncoding, typ) - - _, typ = keys.DetectEncoding([]byte("BEGIN MESSAGE. ok END MESSAGE.")) - require.Equal(t, keys.SaltpackEncoding, typ) - - _, typ = keys.DetectEncoding([]byte("BEGIN MESSAGE. ok ")) - require.Equal(t, keys.SaltpackEncoding, typ) - - _, typ = keys.DetectEncoding([]byte("BEGIN MESSAGE")) - require.Equal(t, keys.SaltpackEncoding, typ) - - _, typ = keys.DetectEncoding([]byte{}) - require.Equal(t, keys.UnknownEncoding, typ) - - _, typ = keys.DetectEncoding(nil) - require.Equal(t, keys.UnknownEncoding, typ) -} diff --git a/encode.go b/encode.go deleted file mode 100644 index af6fdcb..0000000 --- a/encode.go +++ /dev/null @@ -1,148 +0,0 @@ -package keys - -import ( - "strings" - - "github.com/keys-pub/keys/encoding" - "github.com/pkg/errors" -) - -// Encoding is the type of data. -type Encoding string - -const ( - // UnknownEncoding is unknown. - UnknownEncoding Encoding = "" - - // IDEncoding is a key ID string. - IDEncoding Encoding = "id" - - // SaltpackEncoding is armored saltpack encoding. - SaltpackEncoding Encoding = "saltpack" - // SaltpackBinaryEncoding is binary saltpack encoding. - // SaltpackBinaryEncoding Encoding = "saltpack-binary" - - // SSHEncoding is ssh private key "-----BEGIN OPENSSH PRIVATE..." - // or public key as "ssh-ed25519 AAAAC3Nz..." - SSHEncoding Encoding = "ssh" -) - -// EncodeKey encodes the key using the specified encoding. -func EncodeKey(key Key, enc Encoding, password string) (string, error) { - switch enc { - case SaltpackEncoding: - return EncodeSaltpackKey(key, password) - case SSHEncoding: - return EncodeSSHKey(key, password) - default: - return "", errors.Errorf("unrecognized encoding %s", enc) - } -} - -// DecodeKey decodes a key using the specified encoding. -// If you don't know the encoding you can try ParseKey instead. -func DecodeKey(s string, enc Encoding, password string) (Key, error) { - if s == "" { - return nil, errors.Errorf("failed to decode %s key: empty string", enc) - } - switch enc { - case SaltpackEncoding: - return DecodeSaltpackKey(s, password, false) - case SSHEncoding: - if strings.HasPrefix(s, "ssh-ed25519 ") { - if password != "" { - return nil, errors.Errorf("password unsupported for ssh-ed25519 public key") - } - return ParseSSHPublicKey(s) - } - return ParseSSHKey([]byte(s), []byte(password), true) - default: - return nil, errors.Errorf("unsupported encoding %s", enc) - } -} - -// EncodeSSHKey encodes key to SSH. -func EncodeSSHKey(key Key, password string) (string, error) { - switch k := key.(type) { - case *EdX25519Key: - out, err := k.EncodeToSSH([]byte(password)) - if err != nil { - return "", err - } - return string(out), nil - case *EdX25519PublicKey: - if password != "" { - return "", errors.Errorf("password not supported when exporting public key") - } - return string(k.EncodeToSSHAuthorized()), nil - default: - return "", errors.Errorf("unsupported key type") - } -} - -// DecodeSSHKey decodes SSH key. -func DecodeSSHKey(s string, password string) (Key, error) { - return DecodeKey(s, SSHEncoding, password) -} - -// Brand is saltpack brand. -type Brand string - -// EdX25519Brand is saltpack brand for EdX25519 key. -const EdX25519Brand Brand = "EDX25519 KEY" - -// X25519Brand is saltpack brand for X25519 key. -const X25519Brand Brand = "X25519 KEY" - -// EncodeSaltpackKey encrypts a key to saltpack with password. -func EncodeSaltpackKey(key Key, password string) (string, error) { - if key == nil { - return "", errors.Errorf("no key to encode") - } - if key.Private() == nil { - return "", errors.Errorf("no private key") - } - var brand Brand - switch key.Type() { - case EdX25519: - brand = EdX25519Brand - case X25519: - brand = X25519Brand - default: - return "", errors.Errorf("unsupported key %s", key.Type()) - } - b := key.Private() - out := EncryptWithPassword(b, password) - return encoding.EncodeSaltpack(out, string(brand)), nil -} - -// DecodeSaltpackKey decrypts a saltpack encrypted key. -func DecodeSaltpackKey(msg string, password string, isHTML bool) (Key, error) { - encrypted, brand, err := encoding.DecodeSaltpack(msg, isHTML) - if err != nil { - return nil, errors.Wrapf(err, "failed to parse saltpack") - } - b, err := DecryptWithPassword(encrypted, password) - if err != nil { - return nil, errors.Wrapf(err, "failed to decrypt saltpack encoded key") - } - if brand == "" { - return nil, errors.Errorf("unable to determine key type from saltpack brand") - } - switch brand { - case string(EdX25519Brand): - if len(b) != 64 { - return nil, errors.Errorf("invalid number of bytes for ed25519 seed") - } - sk := NewEdX25519KeyFromPrivateKey(Bytes64(b)) - return sk, nil - case string(X25519Brand): - if len(b) != 32 { - return nil, errors.Errorf("invalid number of bytes for x25519 private key") - } - bk := NewX25519KeyFromPrivateKey(Bytes32(b)) - return bk, nil - default: - return nil, errors.Errorf("unknown key type %s", brand) - } -} diff --git a/encode_test.go b/encode_test.go deleted file mode 100644 index ccdc1bc..0000000 --- a/encode_test.go +++ /dev/null @@ -1,150 +0,0 @@ -package keys_test - -import ( - "encoding/hex" - "fmt" - "log" - "testing" - - "github.com/keys-pub/keys" - "github.com/stretchr/testify/require" -) - -func TestEncodeKeyToSaltpack(t *testing.T) { - sk := keys.GenerateEdX25519Key() - msg, err := keys.EncodeSaltpackKey(sk, "testpassword") - require.NoError(t, err) - - _, err = keys.DecodeSaltpackKey(msg, "invalidpassword", false) - require.EqualError(t, err, "failed to decrypt saltpack encoded key: failed to decrypt with a password: secretbox open failed") - - skOut, err := keys.DecodeSaltpackKey(msg, "testpassword", false) - require.NoError(t, err) - - require.Equal(t, sk.Type(), skOut.Type()) - require.Equal(t, sk.Private(), skOut.Private()) - require.Equal(t, sk.Public(), skOut.Public()) -} - -func ExampleDecodeSaltpackKey() { - msg := `BEGIN EDX25519 KEY MESSAGE. - AY6gPAVx9JSUsLg 3K8CNqUyNY87qiL FNNp7UBsIcvObJK mRtDzpcwQU1XpYa - 64FF0g4O0sDrhV4 qlp52vdQ5PG77D8 046ZdckukUl6reZ inOEqkDuOg5hynz - k95BEExR31Sqenh rdqT3ADIdPu8f4f aXQaFejAp3Cb. - END EDX25519 KEY MESSAGE.` - key, err := keys.DecodeSaltpackKey(msg, "testpassword", true) - if err != nil { - log.Fatal(err) - } - fmt.Printf("%s\n", key.ID()) - // Output: kex10x6fdaazp2zy85m6cj7w57y4u0cc99xa3nmwjdldk9l4ajm3yadq70g0js -} - -func TestEncodeKeyDecodeKey(t *testing.T) { - sk := keys.NewEdX25519KeyFromSeed(testSeed(0x01)) - - // Saltpack (password) - msg, err := keys.EncodeKey(sk, keys.SaltpackEncoding, "testpassword") - require.NoError(t, err) - out, err := keys.DecodeKey(msg, keys.SaltpackEncoding, "testpassword") - require.NoError(t, err) - require.Equal(t, sk.Type(), out.Type()) - require.Equal(t, sk.Private(), out.Private()) - require.Equal(t, sk.Public(), out.Public()) - - // Saltpack (no password) - msg, err = keys.EncodeKey(sk, keys.SaltpackEncoding, "") - require.NoError(t, err) - out, err = keys.DecodeKey(msg, keys.SaltpackEncoding, "") - require.NoError(t, err) - require.Equal(t, sk.Type(), out.Type()) - require.Equal(t, sk.Private(), out.Private()) - require.Equal(t, sk.Public(), out.Public()) - - // Saltpack (public) - _, err = keys.EncodeKey(sk.PublicKey(), keys.SaltpackEncoding, "") - require.EqualError(t, err, "no private key") - - // SSH (public) - msg, err = keys.EncodeKey(sk.PublicKey(), keys.SSHEncoding, "") - require.NoError(t, err) - out, err = keys.DecodeKey(msg, keys.SSHEncoding, "") - require.NoError(t, err) - require.Equal(t, sk.PublicKey().Type(), out.Type()) - require.Equal(t, sk.PublicKey().Public(), out.Public()) - - // SSH (password) - msg, err = keys.EncodeKey(sk, keys.SSHEncoding, "testpassword") - require.NoError(t, err) - out, err = keys.DecodeKey(msg, keys.SSHEncoding, "testpassword") - require.NoError(t, err) - require.Equal(t, sk.Type(), out.Type()) - require.Equal(t, sk.Private(), out.Private()) - require.Equal(t, sk.Public(), out.Public()) - - // SSH (password, helper) - msg, err = keys.EncodeSSHKey(sk, "testpassword") - require.NoError(t, err) - out, err = keys.DecodeSSHKey(msg, "testpassword") - require.NoError(t, err) - require.Equal(t, sk.Type(), out.Type()) - require.Equal(t, sk.Private(), out.Private()) - require.Equal(t, sk.Public(), out.Public()) - - // SSH (no password) - msg, err = keys.EncodeKey(sk, keys.SSHEncoding, "") - require.NoError(t, err) - out, err = keys.DecodeKey(msg, keys.SSHEncoding, "") - require.NoError(t, err) - require.Equal(t, sk.Type(), out.Type()) - require.Equal(t, sk.Private(), out.Private()) - require.Equal(t, sk.Public(), out.Public()) - - // SSH (no password, helper) - msg, err = keys.EncodeSSHKey(sk, "") - require.NoError(t, err) - out, err = keys.DecodeSSHKey(msg, "") - require.NoError(t, err) - require.Equal(t, sk.Type(), out.Type()) - require.Equal(t, sk.Private(), out.Private()) - require.Equal(t, sk.Public(), out.Public()) - - // SSH - pk, err := keys.DecodeSSHKey("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIqI4910CfGV/VLbLTy6XXLKZwm/HZQSG/N0iAG0D29c", "") - require.NoError(t, err) - require.Equal(t, "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c", hex.EncodeToString(pk.Public())) - - // Errors - _, err = keys.DecodeKey("", keys.SSHEncoding, "") - require.EqualError(t, err, "failed to decode ssh key: empty string") - _, err = keys.DecodeKey("", keys.SaltpackEncoding, "") - require.EqualError(t, err, "failed to decode saltpack key: empty string") -} - -func ExampleEncodeSSHKey() { - sk := keys.GenerateEdX25519Key() - - privateKey, err := keys.EncodeSSHKey(sk, "testpassword") - if err != nil { - log.Fatal(err) - } - log.Printf("%s\n", privateKey) - - publicKey, err := keys.EncodeSSHKey(sk.PublicKey(), "") - if err != nil { - log.Fatal(err) - } - log.Printf("%s\n", publicKey) - - // Output: - // -} - -func ExampleDecodeSSHKey() { - pk, err := keys.DecodeSSHKey("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIqI4910CfGV/VLbLTy6XXLKZwm/HZQSG/N0iAG0D29c", "") - if err != nil { - log.Fatal(err) - } - fmt.Printf("%s\n", pk.ID()) - // Output: kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077 -} diff --git a/go.sum b/go.sum index 5fa13ef..32f0cd2 100644 --- a/go.sum +++ b/go.sum @@ -68,6 +68,7 @@ golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10 golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc= golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9 h1:sYNJzB4J8toYPQTM6pAkcmBRgw9SnQKP9oXCHfgy604= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= diff --git a/http/auth.go b/http/auth.go index f3ce240..fac074b 100644 --- a/http/auth.go +++ b/http/auth.go @@ -137,7 +137,7 @@ func Authorize(ctx context.Context, auth *AuthRequest) (*AuthResult, error) { if err != nil { return nil, err } - tm := tsutil.ConvertMillis(i) + tm := tsutil.ParseMillis(i) td := auth.Now.Sub(tm) if td < 0 { td = td * -1 diff --git a/parse.go b/parse.go deleted file mode 100644 index bd78bcb..0000000 --- a/parse.go +++ /dev/null @@ -1,26 +0,0 @@ -package keys - -import ( - "github.com/pkg/errors" -) - -// ParseKey tries to determine what key type and parses the key bytes. -func ParseKey(b []byte, password string) (Key, error) { - b, typ := DetectEncoding(b) - logger.Debugf("Encoding: %s", typ) - switch typ { - case IDEncoding: - logger.Debugf("Parsing ID: %s", string(b)) - id, err := ParseID(string(b)) - if err != nil { - return nil, err - } - return id, nil - case SaltpackEncoding: - return DecodeKey(string(b), SaltpackEncoding, password) - case SSHEncoding: - return DecodeKey(string(b), SSHEncoding, password) - default: - return nil, errors.Errorf("unknown key format") - } -} diff --git a/ssh.go b/ssh.go index a2366d2..1d7944c 100644 --- a/ssh.go +++ b/ssh.go @@ -147,3 +147,33 @@ func (k *EdX25519Key) SSHSigner() ssh.Signer { } return signer } + +// EncodeSSHKey encodes key to SSH. +func EncodeSSHKey(key Key, password string) (string, error) { + switch k := key.(type) { + case *EdX25519Key: + out, err := k.EncodeToSSH([]byte(password)) + if err != nil { + return "", err + } + return string(out), nil + case *EdX25519PublicKey: + if password != "" { + return "", errors.Errorf("password not supported when exporting public key") + } + return string(k.EncodeToSSHAuthorized()), nil + default: + return "", errors.Errorf("unsupported key type") + } +} + +// DecodeSSHKey decodes SSH key. +func DecodeSSHKey(s string, password string) (Key, error) { + if strings.HasPrefix(s, "ssh-ed25519 ") { + if password != "" { + return nil, errors.Errorf("password unsupported for ssh-ed25519 public key") + } + return ParseSSHPublicKey(s) + } + return ParseSSHKey([]byte(s), []byte(password), true) +} diff --git a/ssh_test.go b/ssh_test.go index f9617f9..fab87c4 100644 --- a/ssh_test.go +++ b/ssh_test.go @@ -1,6 +1,9 @@ package keys_test import ( + "encoding/hex" + "fmt" + "log" "testing" "github.com/keys-pub/keys" @@ -139,3 +142,88 @@ func TestSSHEncodePublic(t *testing.T) { require.NoError(t, err) require.Equal(t, out.Public(), alice.PublicKey().Bytes()) } + +func TestEncodeDecodeSSHKey(t *testing.T) { + sk := keys.NewEdX25519KeyFromSeed(testSeed(0x01)) + + // SSH (public) + msg, err := keys.EncodeSSHKey(sk.PublicKey(), "") + require.NoError(t, err) + out, err := keys.DecodeSSHKey(msg, "") + require.NoError(t, err) + require.Equal(t, sk.PublicKey().Type(), out.Type()) + require.Equal(t, sk.PublicKey().Public(), out.Public()) + + // SSH (password) + msg, err = keys.EncodeSSHKey(sk, "testpassword") + require.NoError(t, err) + out, err = keys.DecodeSSHKey(msg, "testpassword") + require.NoError(t, err) + require.Equal(t, sk.Type(), out.Type()) + require.Equal(t, sk.Private(), out.Private()) + require.Equal(t, sk.Public(), out.Public()) + + // SSH (password, helper) + msg, err = keys.EncodeSSHKey(sk, "testpassword") + require.NoError(t, err) + out, err = keys.DecodeSSHKey(msg, "testpassword") + require.NoError(t, err) + require.Equal(t, sk.Type(), out.Type()) + require.Equal(t, sk.Private(), out.Private()) + require.Equal(t, sk.Public(), out.Public()) + + // SSH (no password) + msg, err = keys.EncodeSSHKey(sk, "") + require.NoError(t, err) + out, err = keys.DecodeSSHKey(msg, "") + require.NoError(t, err) + require.Equal(t, sk.Type(), out.Type()) + require.Equal(t, sk.Private(), out.Private()) + require.Equal(t, sk.Public(), out.Public()) + + // SSH (no password, helper) + msg, err = keys.EncodeSSHKey(sk, "") + require.NoError(t, err) + out, err = keys.DecodeSSHKey(msg, "") + require.NoError(t, err) + require.Equal(t, sk.Type(), out.Type()) + require.Equal(t, sk.Private(), out.Private()) + require.Equal(t, sk.Public(), out.Public()) + + // SSH + pk, err := keys.DecodeSSHKey("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIqI4910CfGV/VLbLTy6XXLKZwm/HZQSG/N0iAG0D29c", "") + require.NoError(t, err) + require.Equal(t, "8a88e3dd7409f195fd52db2d3cba5d72ca6709bf1d94121bf3748801b40f6f5c", hex.EncodeToString(pk.Public())) + + // Errors + _, err = keys.DecodeSSHKey("", "") + require.EqualError(t, err, "failed to parse ssh key: ssh: no key found") +} + +func ExampleEncodeSSHKey() { + sk := keys.GenerateEdX25519Key() + + privateKey, err := keys.EncodeSSHKey(sk, "testpassword") + if err != nil { + log.Fatal(err) + } + log.Printf("%s\n", privateKey) + + publicKey, err := keys.EncodeSSHKey(sk.PublicKey(), "") + if err != nil { + log.Fatal(err) + } + log.Printf("%s\n", publicKey) + + // Output: + // +} + +func ExampleDecodeSSHKey() { + pk, err := keys.DecodeSSHKey("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIqI4910CfGV/VLbLTy6XXLKZwm/HZQSG/N0iAG0D29c", "") + if err != nil { + log.Fatal(err) + } + fmt.Printf("%s\n", pk.ID()) + // Output: kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077 +}