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