├── .github ├── FUNDING.yml └── workflows │ └── go.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── SECURITY.md ├── address.go ├── address_test.go ├── api ├── as.go ├── edx25519_test.go ├── encode.go ├── encode_test.go ├── key.go ├── key_test.go ├── parse.go ├── parse_test.go ├── rsa_test.go ├── testdata │ ├── rsa.json │ └── rsa.msgpack └── x25519_test.go ├── bech32 ├── bech32.go └── bech32_test.go ├── box.go ├── box_test.go ├── bytes.go ├── cert.go ├── cert_test.go ├── dstore ├── compare.go ├── document.go ├── document_test.go ├── documents.go ├── documents_test.go ├── events │ ├── events.go │ ├── events_test.go │ └── iterator.go ├── iterator.go ├── log.go ├── mem.go ├── mem_events.go ├── mem_test.go ├── options.go ├── path.go ├── path_test.go ├── private_test.go ├── set.go ├── set_test.go └── spew.go ├── ed25519.go ├── edx25519.go ├── edx25519_test.go ├── encoding ├── README.md ├── bip39.go ├── bip39_test.go ├── encode.go ├── encode_test.go ├── is.go ├── is_test.go ├── options.go ├── parse.go ├── parse_test.go ├── saltpack.go └── saltpack_test.go ├── env ├── options.go ├── paths.go ├── paths_darwin_test.go ├── paths_linux_test.go ├── paths_test.go └── paths_windows_test.go ├── errors.go ├── errors_test.go ├── func.go ├── func_test.go ├── go.mod ├── go.sum ├── hkdf.go ├── hkdf_test.go ├── http ├── README.md ├── alias.go ├── auth.go ├── auth_test.go ├── client.go ├── client │ ├── api.go │ ├── client.go │ └── log.go ├── client_test.go ├── example_test.go ├── github_test.go ├── log.go ├── mem.go ├── nonces_test.go ├── reddit_test.go └── request.go ├── id.go ├── id_test.go ├── json ├── marshal.go └── marshal_test.go ├── key.go ├── key_test.go ├── keyring ├── README.md ├── backup.go ├── backup_test.go ├── fs.go ├── fs_test.go ├── keyring.go ├── keyring_darwin.go ├── keyring_linux.go ├── keyring_test.go ├── keyring_windows.go ├── mem.go └── mem_test.go ├── lint.sh ├── log.go ├── noise ├── README.md ├── cipher.go ├── example_test.go ├── noise.go └── noise_test.go ├── package_test.go ├── password.go ├── password_test.go ├── rand.go ├── rand_test.go ├── rsa.go ├── rsa_test.go ├── saltpack ├── README.md ├── boxkey.go ├── detect.go ├── detect_test.go ├── encrypt.go ├── encrypt_examples_test.go ├── encrypt_test.go ├── errors.go ├── keybase_test.go ├── log.go ├── saltpack.go ├── saltpack_test.go ├── sign.go ├── sign_examples_test.go ├── sign_test.go ├── signcrypt.go ├── signcrypt_examples_test.go ├── signcrypt_test.go └── signkey.go ├── secretbox.go ├── secretbox_test.go ├── security.sh ├── sha.go ├── sha_test.go ├── sigchain.go ├── sigchain_test.go ├── sigchains.go ├── sigchains_test.go ├── sodiumbox.go ├── sodiumbox_test.go ├── ssh.go ├── ssh_test.go ├── statement.go ├── statement_example_test.go ├── statement_test.go ├── testdata ├── github │ └── gist.json ├── reddit │ └── charlie.json ├── sc1.spew ├── sc2.spew └── twitter │ ├── 1205589994380783616 │ ├── 1205589994380783616.json │ ├── 1222706272849391616.json │ └── statement.json ├── tsutil ├── clock.go ├── tsutil.go └── tsutil_test.go ├── user ├── echo.go ├── log.go ├── result.go ├── services │ ├── echo.go │ ├── github.go │ ├── github_test.go │ ├── https.go │ ├── https_test.go │ ├── keyspub.go │ ├── keyspub_test.go │ ├── log.go │ ├── proxy.go │ ├── proxy_test.go │ ├── reddit.go │ ├── reddit_test.go │ ├── request.go │ ├── service.go │ ├── service_test.go │ ├── twitter.go │ ├── twitter_test.go │ ├── verify.go │ └── verify_test.go ├── user.go ├── user_test.go ├── validate │ ├── echo.go │ ├── echo_test.go │ ├── github.go │ ├── github_test.go │ ├── https.go │ ├── https_test.go │ ├── reddit.go │ ├── reddit_test.go │ ├── twitter.go │ ├── twitter_test.go │ ├── validate_test.go │ └── validator.go └── verify.go ├── users ├── echo_test.go ├── github_test.go ├── log.go ├── options.go ├── reddit_test.go ├── search.go ├── search_test.go ├── services.go ├── testdata │ ├── kex109x2xh6tle8yls3quqpu9xuhlzffr9fakcv4ymc52cvq366qwnpqyyydgz.json │ └── kex1s08uz8zqqrmzcek0pms0sjknv4wpz33f8p5t57y0d6xsf2sgmd2swgm7er.json ├── twitter_test.go ├── users.go └── users_test.go ├── x25519.go └── x25519_test.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [gabriel] 4 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - master 5 | pull_request: 6 | name: Test 7 | jobs: 8 | security: 9 | name: Review security 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Install Go 13 | uses: actions/setup-go@v2 14 | with: 15 | go-version: "^1.15.0" 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | - name: Run gosec 19 | shell: bash 20 | run: ./security.sh 21 | golangci-lint: 22 | name: Linter 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: golangci-lint 27 | uses: golangci/golangci-lint-action@v2 28 | with: 29 | version: latest 30 | test: 31 | strategy: 32 | matrix: 33 | platform: [ubuntu-latest, macos-latest, windows-latest] 34 | runs-on: ${{ matrix.platform }} 35 | steps: 36 | - name: Install Go 37 | uses: actions/setup-go@v2 38 | with: 39 | go-version: "^1.16.0" 40 | - name: Checkout code 41 | uses: actions/checkout@v2 42 | - name: Test 43 | run: go test ./... 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | keysd/keysd 2 | keys 3 | credentials.json 4 | dist/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // "go.lintTool": "golangci-lint", 3 | // "go.lintFlags": [ 4 | // "--fast" 5 | // ] 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | ​ 3 | Copyright (c) 2019 Gabriel Handford 4 | ​ 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | ​ 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | ​ 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # keys.pub 2 | 3 | [![GoDoc](https://godoc.org/github.com/keys-pub/keys?status.svg)](https://godoc.org/github.com/keys-pub/keys) 4 | 5 | :warning: **Unfortunately, this project is not currently being worked on. I may revisit this in the future, if you would like to sponsor development or hire me please reach out at gabriel@keys.pub.** (see keys-pub/keys#168 for more info) :warning: 6 | 7 | ☢ This project is in development and has not been audited or reviewed. Use at your own risk. ☢ 8 | 9 | ## Documentation 10 | 11 | Visit **[keys.pub](https://keys.pub)**. 12 | 13 | ## Repositories 14 | 15 | | Repo | Description | 16 | | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 17 | | [keys-pub/keys](https://github.com/keys-pub/keys) | Key management, signing and encryption, including [keys/saltpack](https://godoc.org/github.com/keys-pub/keys/saltpack) and [keys/keyring](https://godoc.org/github.com/keys-pub/keys/keyring). | 18 | | [keys-pub/keys-ext](https://github.com/keys-pub/keys-ext) | Extensions: Service (gRPC), command line client, DB, Firestore, HTTP API/Client/Server, Git, Wormhole, etc. | 19 | | [keys-pub/app](https://github.com/keys-pub/app) | Desktop app. | 20 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 0.x.y | :white_check_mark: | 8 | 9 | ## Reporting a Vulnerability 10 | 11 | Contact me at all of: 12 | - gabrielh@gmail.com 13 | - gabrielh@keybase 14 | -------------------------------------------------------------------------------- /address.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // Address is a canonical list of IDs. 11 | type Address struct { 12 | ids []ID 13 | idmap map[ID]bool 14 | } 15 | 16 | // NewAddress returns an Address from a list of IDs. 17 | func NewAddress(ids ...ID) (*Address, error) { 18 | if len(ids) == 0 { 19 | return nil, errors.Errorf("no ids") 20 | } 21 | sort.Slice(ids, func(i, j int) bool { return ids[i].String() < ids[j].String() }) 22 | idmap := make(map[ID]bool, len(ids)) 23 | for _, r := range ids { 24 | if ok := idmap[r]; ok { 25 | return nil, errors.Errorf("duplicate address %s", r) 26 | } 27 | idmap[r] = true 28 | } 29 | return &Address{ 30 | ids: ids, 31 | idmap: idmap, 32 | }, nil 33 | } 34 | 35 | // ParseAddress returns address from a string. 36 | func ParseAddress(saddrs ...string) (*Address, error) { 37 | if len(saddrs) == 0 { 38 | return nil, errors.Errorf("no addresses to parse") 39 | } 40 | ids := []ID{} 41 | for _, saddr := range saddrs { 42 | recs := strings.Split(saddr, ":") 43 | for _, r := range recs { 44 | id, err := ParseID(r) 45 | if err != nil { 46 | return nil, err 47 | } 48 | ids = append(ids, id) 49 | } 50 | } 51 | return NewAddress(ids...) 52 | } 53 | 54 | // Contains returns true if address contains the specified id. 55 | func (a *Address) Contains(id ID) bool { 56 | for _, r := range a.ids { 57 | if r == id { 58 | return true 59 | } 60 | } 61 | return false 62 | } 63 | 64 | // Strings returns IDs as strings. 65 | func (a *Address) Strings() []string { 66 | s := make([]string, 0, len(a.ids)) 67 | for _, r := range a.ids { 68 | s = append(s, r.String()) 69 | } 70 | return s 71 | } 72 | 73 | // String returns a canonical string representation of an address. 74 | // The first address part is less than the second part. 75 | // 76 | // NewAddress("bob", "alice").String() => "alice:bob" 77 | // 78 | func (a *Address) String() string { 79 | return strings.Join(a.Strings(), ":") 80 | } 81 | -------------------------------------------------------------------------------- /address_test.go: -------------------------------------------------------------------------------- 1 | package keys_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/keys-pub/keys" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestAddress(t *testing.T) { 12 | alice := keys.NewEdX25519KeyFromSeed(testSeed(0x01)).ID() 13 | bob := keys.NewEdX25519KeyFromSeed(testSeed(0x02)).ID() 14 | charlie := keys.NewEdX25519KeyFromSeed(testSeed(0x03)).ID() 15 | 16 | aliceBob, err := keys.NewAddress(alice, bob) 17 | require.NoError(t, err) 18 | require.Equal(t, fmt.Sprintf("%s:%s", alice, bob), aliceBob.String()) 19 | bobAlice, err := keys.NewAddress(bob, alice) 20 | require.NoError(t, err) 21 | require.Equal(t, fmt.Sprintf("%s:%s", alice, bob), bobAlice.String()) 22 | 23 | addr, err := keys.ParseAddress(fmt.Sprintf("%s:%s", alice, bob)) 24 | require.NoError(t, err) 25 | require.Equal(t, fmt.Sprintf("%s:%s", alice, bob), addr.String()) 26 | 27 | addr2, err := keys.ParseAddress(fmt.Sprintf("%s:%s:%s", alice, bob, charlie)) 28 | require.NoError(t, err) 29 | require.Equal(t, 3, len(addr2.Strings())) 30 | require.Equal(t, fmt.Sprintf("%s:%s:%s", alice, charlie, bob), addr2.String()) 31 | 32 | empty, err := keys.NewAddress() 33 | require.EqualError(t, err, "no ids") 34 | require.Nil(t, empty) 35 | 36 | dupe, err := keys.NewAddress(alice, alice) 37 | require.EqualError(t, err, fmt.Sprintf("duplicate address %s", alice)) 38 | require.Nil(t, dupe) 39 | 40 | } 41 | -------------------------------------------------------------------------------- /api/encode.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/keys-pub/keys" 5 | "github.com/keys-pub/keys/encoding" 6 | "github.com/pkg/errors" 7 | "github.com/vmihailenco/msgpack/v4" 8 | ) 9 | 10 | // Generic key brand. 11 | const keyBrand = "KEY" 12 | 13 | // EncodeKey a key with an optional password. 14 | func EncodeKey(key *Key, password string) (string, error) { 15 | if key == nil { 16 | return "", errors.Errorf("no key to encode") 17 | } 18 | marshaled, err := msgpack.Marshal(key) 19 | if err != nil { 20 | return "", err 21 | } 22 | out := keys.EncryptWithPassword(marshaled, password) 23 | return encoding.EncodeSaltpack(out, keyBrand), nil 24 | } 25 | 26 | // DecodeKey a key with an optional password. 27 | func DecodeKey(msg string, password string) (*Key, error) { 28 | decoded, brand, err := encoding.DecodeSaltpack(msg, false) 29 | if err != nil { 30 | return nil, errors.Errorf("failed to decode key") 31 | } 32 | b, err := keys.DecryptWithPassword(decoded, password) 33 | if err != nil { 34 | return nil, errors.Errorf("failed to decode key") 35 | } 36 | 37 | switch brand { 38 | case keyBrand: 39 | var key Key 40 | if err := msgpack.Unmarshal(b, &key); err != nil { 41 | return nil, errors.Errorf("failed to unmarshal key") 42 | } 43 | if err := key.Check(); err != nil { 44 | return nil, errors.Wrapf(err, "invalid key") 45 | } 46 | return &key, nil 47 | case edx25519Brand: 48 | if len(b) != 64 { 49 | return nil, errors.Errorf("invalid number of bytes for ed25519 seed") 50 | } 51 | sk := keys.NewEdX25519KeyFromPrivateKey(keys.Bytes64(b)) 52 | return NewKey(sk), nil 53 | case x25519Brand: 54 | if len(b) != 32 { 55 | return nil, errors.Errorf("invalid number of bytes for x25519 private key") 56 | } 57 | bk := keys.NewX25519KeyFromPrivateKey(keys.Bytes32(b)) 58 | return NewKey(bk), nil 59 | default: 60 | return nil, errors.Errorf("invalid key") 61 | } 62 | } 63 | 64 | // For EdX25519 key that only contains 64 private key bytes. 65 | const edx25519Brand string = "EDX25519 KEY" 66 | 67 | // For X25519 key that only contains 32 private key bytes. 68 | const x25519Brand string = "X25519 KEY" 69 | -------------------------------------------------------------------------------- /api/encode_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keys-pub/keys" 7 | "github.com/keys-pub/keys/api" 8 | "github.com/keys-pub/keys/encoding" 9 | "github.com/keys-pub/keys/tsutil" 10 | "github.com/stretchr/testify/require" 11 | "github.com/vmihailenco/msgpack/v4" 12 | ) 13 | 14 | func TestEncode(t *testing.T) { 15 | clock := tsutil.NewTestClock() 16 | 17 | key := api.NewKey(keys.GenerateEdX25519Key()). 18 | Created(clock.NowMillis()). 19 | WithLabels("test") 20 | 21 | encoded, err := api.EncodeKey(key, "") 22 | require.NoError(t, err) 23 | 24 | out, err := api.DecodeKey(encoded, "") 25 | require.NoError(t, err) 26 | require.Equal(t, key, out) 27 | 28 | encoded, err = api.EncodeKey(key, "testpassword") 29 | require.NoError(t, err) 30 | 31 | out, err = api.DecodeKey(encoded, "testpassword") 32 | require.NoError(t, err) 33 | require.Equal(t, key, out) 34 | 35 | _, err = api.DecodeKey(encoded, "invalidpassword") 36 | require.EqualError(t, err, "failed to decode key") 37 | 38 | _, err = api.DecodeKey("invaliddata", "") 39 | require.EqualError(t, err, "failed to decode key") 40 | 41 | // Empty 42 | var empty struct{} 43 | _, err = api.DecodeKey(encodeStruct(empty, ""), "") 44 | require.EqualError(t, err, "invalid key") 45 | 46 | // Invalid msgpack 47 | _, err = api.DecodeKey(encodeBytes([]byte("????"), ""), "") 48 | require.EqualError(t, err, "invalid key") 49 | } 50 | 51 | func encodeStruct(i interface{}, password string) string { 52 | b, err := msgpack.Marshal(i) 53 | if err != nil { 54 | panic(err) 55 | } 56 | return encodeBytes(b, password) 57 | } 58 | 59 | func encodeBytes(b []byte, password string) string { 60 | return encoding.EncodeSaltpack(keys.EncryptWithPassword(b, password), "") 61 | } 62 | 63 | func TestDecodeOld(t *testing.T) { 64 | msg := `BEGIN EDX25519 KEY MESSAGE. 65 | AY6gPAVx9JSUsLg 3K8CNqUyNY87qiL FNNp7UBsIcvObJK mRtDzpcwQU1XpYa 66 | 64FF0g4O0sDrhV4 qlp52vdQ5PG77D8 046ZdckukUl6reZ inOEqkDuOg5hynz 67 | k95BEExR31Sqenh rdqT3ADIdPu8f4f aXQaFejAp3Cb. 68 | END EDX25519 KEY MESSAGE.` 69 | out, err := api.DecodeKey(msg, "testpassword") 70 | require.NoError(t, err) 71 | require.Equal(t, keys.ID("kex10x6fdaazp2zy85m6cj7w57y4u0cc99xa3nmwjdldk9l4ajm3yadq70g0js"), out.ID) 72 | } 73 | -------------------------------------------------------------------------------- /api/parse.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/keys-pub/keys" 7 | ) 8 | 9 | // ParseKey tries to determine what key type and parses the key bytes. 10 | func ParseKey(b []byte, password string) (*Key, error) { 11 | s := strings.TrimSpace(string(b)) 12 | 13 | kid, err := keys.ParseID(s) 14 | if err == nil { 15 | return NewKey(kid), nil 16 | } 17 | 18 | if strings.HasPrefix(s, "ssh-") { 19 | out, err := keys.ParseSSHPublicKey(s) 20 | if err != nil { 21 | return nil, err 22 | } 23 | return NewKey(out), err 24 | } 25 | 26 | if strings.HasPrefix(s, "-----BEGIN ") { 27 | out, err := keys.ParseSSHKey([]byte(s), []byte(password), true) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return NewKey(out), err 32 | } 33 | 34 | return DecodeKey(s, password) 35 | } 36 | -------------------------------------------------------------------------------- /api/parse_test.go: -------------------------------------------------------------------------------- 1 | package api_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keys-pub/keys" 7 | "github.com/keys-pub/keys/api" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestParse(t *testing.T) { 12 | kid := "kex1nc345hg9nt3eef8rfz3r2uu2psma8umf54tx8z8meyvmnzeglk8s50xu7y" 13 | key, err := api.ParseKey([]byte(kid), "") 14 | require.NoError(t, err) 15 | require.Equal(t, keys.ID("kex1nc345hg9nt3eef8rfz3r2uu2psma8umf54tx8z8meyvmnzeglk8s50xu7y"), key.ID) 16 | 17 | // SSH public ed25519 18 | edPub := `ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJ4jWl0FmuOcpONIojVzigw30/NppVZjiPvJGbmLKP2P gabe@ok.local` 19 | key, err = api.ParseKey([]byte(edPub), "") 20 | require.NoError(t, err) 21 | require.Equal(t, keys.ID("kex1nc345hg9nt3eef8rfz3r2uu2psma8umf54tx8z8meyvmnzeglk8s50xu7y"), key.ID) 22 | 23 | // SSH private ed25519 24 | edPriv := `-----BEGIN OPENSSH PRIVATE KEY----- 25 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 26 | QyNTUxOQAAACCeI1pdBZrjnKTjSKI1c4oMN9PzaaVWY4j7yRm5iyj9jwAAAJDRmZP80ZmT 27 | /AAAAAtzc2gtZWQyNTUxOQAAACCeI1pdBZrjnKTjSKI1c4oMN9PzaaVWY4j7yRm5iyj9jw 28 | AAAED2F09VUc5ig2cF/HpYJQM6Jzin26cDxFGELnR5HRIF3Z4jWl0FmuOcpONIojVzigw3 29 | 0/NppVZjiPvJGbmLKP2PAAAADWdhYmVAb2subG9jYWw= 30 | -----END OPENSSH PRIVATE KEY-----` 31 | key, err = api.ParseKey([]byte(edPriv), "") 32 | require.NoError(t, err) 33 | require.Equal(t, keys.ID("kex1nc345hg9nt3eef8rfz3r2uu2psma8umf54tx8z8meyvmnzeglk8s50xu7y"), key.ID) 34 | 35 | // Saltpack 36 | sp := `BEGIN EDX25519 KEY MESSAGE. 37 | GSXg1PCawOlgXTp IoXa8FHPFV82MkC xrXzl7k2Scj2CK0 R9ezilK7VqWsLWv 38 | TF3WxURVAhzQmNY uJoEJXKYiWJIY4K gMQTVtndovcxjho KBu5yu4Wm7nM6Bh 39 | mjqGVIo5r0NXW4N ZsKF3NJ01o98tpJ 9KrsbBFsBd2V. 40 | END EDX25519 KEY MESSAGE.` 41 | key, err = api.ParseKey([]byte(sp), "testpassword") 42 | require.NoError(t, err) 43 | require.Equal(t, keys.ID("kex1lm9tc5cmgr0u4tg7q2tl9fxzdcke89c5sl8jnjpkr6erv88m4nvq5cg7n8"), key.ID) 44 | 45 | // API 46 | ak := `BEGIN KEY MESSAGE. 47 | Z7SesuE0476OHgx zzBpDJEvtBIyvoh Eu6n8n2GraBGi4C Mn5PBpSxOUXYmtv 48 | egz6h4JDxL2UkJL v47Yc4poDkkUcro 9tysFbWqf6oeXiJ CTrSWP9s6gaLZMg 49 | hBvwd1RW1ifzjDz 1l9Bzi1g3CqWmn1 DUgIxOA6EADOxYP 4RfPgpGpUxGNrBH 50 | JdH0L5km70jlpx3 BD1mfWV3r39HWbd x8lM1c3kIV8P0DF sjt7e5w8x30dNG3 51 | FXem1iDVpR0C6tk VHAgHbb7Ik44CbZ 3h4KfcJMsv2wvzq kiN9BGWl8swIuGY 52 | KWcZYo1P7lZPaKo K3rnMUvTkis4XWL 1URW1r2810ASXvh XYLetugWobu0PPl 53 | FIinNIJ9w044N8x MYixY8rboFIHdxn fsEueZrN4zXUzBE VxTdljPsRGVOsj1 54 | G6mQQPYrQy1O. 55 | END KEY MESSAGE.` 56 | key, err = api.ParseKey([]byte(ak), "") 57 | require.NoError(t, err) 58 | require.Equal(t, keys.ID("kex135n03rgjrenhac4r92y4stfcy84rcc3tcgpwm6yl55uc0hg7h4eq3ntlut"), key.ID) 59 | } 60 | -------------------------------------------------------------------------------- /api/testdata/rsa.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "rsa1lg8lhzpatgmakvrkz866fehw64lkdtly3t2q7d36kfyhmaauyg2sgkhan4", 3 | "type": "rsa", 4 | "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=", 5 | "pub": "MIIBBwKCAQBxY8hCshkKiXCUKydkrtQtQSRke28w4JotocDiVqou4k55DEDJakvWbXXDcakV4HA8R2tOGgbxvTjFo8EK470w9O9ipapPUSrRRaBsSOlkaaIs6OYh4FLwZpqMNBVVEtguVUR/C34Y2pS9kRrHs6q+cGhDZolkWT7nGy5eSEvPDHg0EBq11hu6HmPmI3r0BInONqJg2rcK3U++wk1lnbD3ysCZsKOqRUms3n/IWKeTqXXmz2XKJ2t0NSXwiDmA9q0Gm+w0bXh3lzhtUP4MlzS+lnx9hK5bjzSbCUB5RXwMDG/uNMQqC4MmA4BPceSfMyAIFjdRLGy/K7gbb2viOYRtAgED", 6 | "cts": 1234567890001, 7 | "uts": 1234567890002, 8 | "notes": "some test notes" 9 | } -------------------------------------------------------------------------------- /box.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "golang.org/x/crypto/nacl/box" 6 | ) 7 | 8 | // BoxSeal uses nacl.box to encrypt. 9 | func BoxSeal(b []byte, recipient *X25519PublicKey, sender *X25519Key) []byte { 10 | nonce := Rand24() 11 | return sealBox(b, nonce, recipient, sender) 12 | } 13 | 14 | func sealBox(b []byte, nonce *[24]byte, recipient *X25519PublicKey, sender *X25519Key) []byte { 15 | encrypted := box.Seal(nil, b, nonce, recipient.Bytes32(), sender.PrivateKey()) 16 | return append(nonce[:], encrypted...) 17 | } 18 | 19 | // BoxOpen uses nacl.box to decrypt. 20 | func BoxOpen(encrypted []byte, sender *X25519PublicKey, recipient *X25519Key) ([]byte, error) { 21 | return openBox(encrypted, sender, recipient) 22 | } 23 | 24 | func openBox(encrypted []byte, sender *X25519PublicKey, recipient *X25519Key) ([]byte, error) { 25 | if len(encrypted) < 24 { 26 | return nil, errors.Errorf("not enough bytes") 27 | } 28 | var nonce [24]byte 29 | copy(nonce[:], encrypted[:24]) 30 | encrypted = encrypted[24:] 31 | 32 | b, ok := box.Open(nil, encrypted, &nonce, sender.Bytes32(), recipient.PrivateKey()) 33 | if !ok { 34 | return nil, errors.Errorf("box open failed") 35 | } 36 | return b, nil 37 | } 38 | -------------------------------------------------------------------------------- /box_test.go: -------------------------------------------------------------------------------- 1 | package keys_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "testing" 7 | 8 | "github.com/keys-pub/keys" 9 | "github.com/keys-pub/keys/encoding" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestBoxSeal(t *testing.T) { 14 | alice := keys.GenerateX25519Key() 15 | bob := keys.GenerateX25519Key() 16 | 17 | msg := "Hey bob, it's alice. The passcode is 12345." 18 | encrypted := keys.BoxSeal([]byte(msg), bob.PublicKey(), alice) 19 | 20 | out, err := keys.BoxOpen(encrypted, alice.PublicKey(), bob) 21 | require.NoError(t, err) 22 | require.Equal(t, "Hey bob, it's alice. The passcode is 12345.", string(out)) 23 | } 24 | 25 | func ExampleBoxSeal() { 26 | ak := keys.GenerateX25519Key() 27 | bk := keys.GenerateX25519Key() 28 | 29 | msg := "Hey bob, it's alice. The passcode is 12345." 30 | encrypted := keys.BoxSeal([]byte(msg), bk.PublicKey(), ak) 31 | 32 | out, err := keys.BoxOpen(encrypted, ak.PublicKey(), bk) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | fmt.Printf("%s\n", string(out)) 38 | // Output: 39 | // Hey bob, it's alice. The passcode is 12345. 40 | } 41 | 42 | func TestBox(t *testing.T) { 43 | ka := keys.Bytes32(encoding.MustDecode("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a", encoding.Hex)) 44 | kpa := keys.Bytes32(encoding.MustDecode("8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a", encoding.Hex)) 45 | kb := keys.Bytes32(encoding.MustDecode("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb", encoding.Hex)) 46 | kpb := keys.Bytes32(encoding.MustDecode("de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f", encoding.Hex)) 47 | nonce := keys.Bytes24(encoding.MustDecode("69696ee955b62b73cd62bda875fc73d68219e0036b7a0b37", encoding.Hex)) 48 | plain := encoding.MustDecode("be075fc53c81f2d5cf141316ebeb0c7b5228c52a4c62cbd44b66849b64244ffc"+ 49 | "e5ecbaaf33bd751a1ac728d45e6c61296cdc3c01233561f41db66cce314adb31"+ 50 | "0e3be8250c46f06dceea3a7fa1348057e2f6556ad6b1318a024a838f21af1fde"+ 51 | "048977eb48f59ffd4924ca1c60902e52f0a089bc76897040e082f93776384864"+ 52 | "5e0705", encoding.Hex) 53 | cipher := encoding.MustDecode("f3ffc7703f9400e52a7dfb4b3d3305d98e993b9f48681273c29650ba32fc76ce"+ 54 | "48332ea7164d96a4476fb8c531a1186ac0dfc17c98dce87b4da7f011ec48c972"+ 55 | "71d2c20f9b928fe2270d6fb863d51738b48eeee314a7cc8ab932164548e526ae"+ 56 | "90224368517acfeabd6bb3732bc0e9da99832b61ca01b6de56244a9e88d5f9b3"+ 57 | "7973f622a43d14a6599b1f654cb45a74e355a5", encoding.Hex) 58 | 59 | alice := keys.NewX25519KeyFromPrivateKey(ka) 60 | bob := keys.NewX25519KeyFromPrivateKey(kb) 61 | 62 | require.Equal(t, alice.PublicKey().Bytes32(), kpa) 63 | require.Equal(t, bob.PublicKey().Bytes32(), kpb) 64 | 65 | encrypted := alice.BoxSeal(plain, nonce, bob.PublicKey()) 66 | require.Equal(t, cipher, encrypted) 67 | } 68 | -------------------------------------------------------------------------------- /bytes.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | // Bytes64 converts byte slice to *[64]byte or panics. 4 | func Bytes64(b []byte) *[64]byte { 5 | if len(b) != 64 { 6 | panic("not 64 bytes") 7 | } 8 | var b64 [64]byte 9 | copy(b64[:], b) 10 | return &b64 11 | } 12 | 13 | // Bytes32 converts byte slice to *[32]byte or panics. 14 | func Bytes32(b []byte) *[32]byte { 15 | if len(b) != 32 { 16 | panic("not 32 bytes") 17 | } 18 | var b32 [32]byte 19 | copy(b32[:], b) 20 | return &b32 21 | } 22 | 23 | // Bytes24 converts byte slice to *[24]byte or panics. 24 | func Bytes24(b []byte) *[24]byte { 25 | if len(b) != 24 { 26 | panic("not 24 bytes") 27 | } 28 | var b24 [24]byte 29 | copy(b24[:], b) 30 | return &b24 31 | } 32 | 33 | // Bytes16 converts byte slice to *[16]byte or panics. 34 | func Bytes16(b []byte) *[16]byte { 35 | if len(b) != 16 { 36 | panic("not 16 bytes") 37 | } 38 | var b16 [16]byte 39 | copy(b16[:], b) 40 | return &b16 41 | } 42 | -------------------------------------------------------------------------------- /cert_test.go: -------------------------------------------------------------------------------- 1 | package keys_test 2 | 3 | import ( 4 | "crypto/x509" 5 | "testing" 6 | 7 | "github.com/keys-pub/keys" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestGenerateCertificateKey(t *testing.T) { 12 | caCert, err := keys.GenerateCertificateKey("localhost", true, nil) 13 | require.NoError(t, err) 14 | require.True(t, len(caCert.Public()) > 0) 15 | require.True(t, len(caCert.Private()) > 0) 16 | 17 | certPool := x509.NewCertPool() 18 | ok := certPool.AppendCertsFromPEM([]byte(caCert.Public())) 19 | if !ok { 20 | t.Fatal("failed to add to cert pool") 21 | } 22 | 23 | // TODO: Generated cert fails to verify 24 | // xcaCert, xerr := caCert.X509Certificate() 25 | // require.NoError(t, xerr) 26 | 27 | // cert, certErr := GenerateCertificateKey("localhost", false, xcaCert) 28 | // require.NoError(t, certErr) 29 | 30 | xcert, err := caCert.X509Certificate() 31 | require.NoError(t, err) 32 | _, err = xcert.Verify(x509.VerifyOptions{ 33 | DNSName: "localhost", 34 | Roots: certPool, 35 | }) 36 | require.NoError(t, err) 37 | 38 | certKey, err := keys.NewCertificateKey(caCert.Private(), caCert.Public()) 39 | require.NoError(t, err) 40 | require.NotNil(t, certKey) 41 | } 42 | 43 | func TestCertificateKey(t *testing.T) { 44 | public := `-----BEGIN CERTIFICATE----- 45 | MIIBbDCCARKgAwIBAgIQI3ViQTyP8XxlaXUnwbKORjAKBggqhkjOPQQDAjAQMQ4w 46 | DAYDVQQKEwVLZXl1cDAeFw0xOTA3MjQwMjAwMTZaFw0yOTA3MjEwMjAwMTZaMBAx 47 | DjAMBgNVBAoTBUtleXVwMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOXhT88Pe 48 | /Ql5LFyxYUb9a0v+HOKqs2PGO/0CE4UPSj5XpocMUotMSm4Yau1/1j1SV+/Vktin 49 | ixCC7hfVyswyFqNOMEwwDgYDVR0PAQH/BAQDAgKkMBMGA1UdJQQMMAoGCCsGAQUF 50 | BwMBMA8GA1UdEwEB/wQFMAMBAf8wFAYDVR0RBA0wC4IJbG9jYWxob3N0MAoGCCqG 51 | SM49BAMCA0gAMEUCIQDyOYbe6kzrU8Z45/KmkYX3fzfDAvjq3vqSUe5Xaf/KwQIg 52 | CmvKhhT2XYwfNim1eLnU78spAetAyk//C7w+BfxgnPo= 53 | -----END CERTIFICATE-----` 54 | private := `-----BEGIN ECDSA PRIVATE KEY----- 55 | MHcCAQEEIPflp/bXqmjd6AvkzfsGd2q1F+wjlJ8rVL1TEYYl3giVoAoGCCqGSM49 56 | AwEHoUQDQgAEOXhT88Pe/Ql5LFyxYUb9a0v+HOKqs2PGO/0CE4UPSj5XpocMUotM 57 | Sm4Yau1/1j1SV+/VktinixCC7hfVyswyFg== 58 | -----END ECDSA PRIVATE KEY-----` 59 | 60 | certKey, err := keys.NewCertificateKey(private, public) 61 | require.NoError(t, err) 62 | require.NotNil(t, certKey) 63 | 64 | } 65 | -------------------------------------------------------------------------------- /dstore/compare.go: -------------------------------------------------------------------------------- 1 | package dstore 2 | 3 | import ( 4 | "github.com/google/go-cmp/cmp" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | func compare(op string, v1 interface{}, v2 interface{}) (bool, error) { 9 | switch op { 10 | case "==": 11 | return cmp.Equal(v1, v2), nil 12 | // case ">": 13 | // switch v := v1.(type) { 14 | // case string: 15 | // s2, ok := v2.(string) 16 | // if !ok { 17 | // return false, errors.Errorf("invalid compare type") 18 | // } 19 | // return v > s2, nil 20 | // case time.Time: 21 | // t2, ok := v2.(time.Time) 22 | // if !ok { 23 | // return false, errors.Errorf("invalid compare type") 24 | // } 25 | // return v.After(t2), nil 26 | // default: 27 | // return false, errors.Errorf("unsupported compare type: %T", v1) 28 | // } 29 | // case ">=": 30 | // switch v := v1.(type) { 31 | // case string: 32 | // s2, ok := v2.(string) 33 | // if !ok { 34 | // return false, errors.Errorf("invalid compare type") 35 | // } 36 | // return v >= s2, nil 37 | // case time.Time: 38 | // t2, ok := v2.(time.Time) 39 | // if !ok { 40 | // return false, errors.Errorf("invalid compare type") 41 | // } 42 | // return v.Equal(t2) || v.After(t2), nil 43 | // default: 44 | // return false, errors.Errorf("unsupported compare type: %T", v1) 45 | // } 46 | default: 47 | return false, errors.Errorf("unsupported op") 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /dstore/documents_test.go: -------------------------------------------------------------------------------- 1 | package dstore_test 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | "github.com/keys-pub/keys/dstore" 9 | ) 10 | 11 | func ExampleDocuments_DocumentIterator() { 12 | d := dstore.NewMem() 13 | 14 | if err := d.Set(context.TODO(), dstore.Path("tests", 1), dstore.Data([]byte("testdata"))); err != nil { 15 | log.Fatal(err) 16 | } 17 | 18 | iter, err := d.DocumentIterator(context.TODO(), dstore.Path("tests")) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | defer iter.Release() 23 | for { 24 | doc, err := iter.Next() 25 | if err != nil { 26 | log.Fatal(err) 27 | } 28 | if doc == nil { 29 | break 30 | } 31 | fmt.Printf("%s: %s\n", doc.Path, string(doc.Data())) 32 | 33 | } 34 | 35 | // Output: 36 | // /tests/1: testdata 37 | } 38 | -------------------------------------------------------------------------------- /dstore/events/iterator.go: -------------------------------------------------------------------------------- 1 | package events 2 | 3 | // Iterator is an iterator for Event's. 4 | type Iterator interface { 5 | // Next document, or nil. 6 | Next() (*Event, error) 7 | // Release resources associated with the iterator. 8 | Release() 9 | } 10 | 11 | // NewIterator returns an iterator for a Event slice. 12 | func NewIterator(events []*Event) Iterator { 13 | return &iterator{events: events} 14 | } 15 | 16 | type iterator struct { 17 | events []*Event 18 | index int 19 | } 20 | 21 | func (i *iterator) Next() (*Event, error) { 22 | if i.index >= len(i.events) { 23 | return nil, nil 24 | } 25 | d := i.events[i.index] 26 | i.index++ 27 | return d, nil 28 | } 29 | 30 | func (i *iterator) Release() { 31 | i.events = nil 32 | } 33 | -------------------------------------------------------------------------------- /dstore/iterator.go: -------------------------------------------------------------------------------- 1 | package dstore 2 | 3 | // Iterator is an iterator for Document's. 4 | type Iterator interface { 5 | // Next document, or nil. 6 | Next() (*Document, error) 7 | // Release resources associated with the iterator. 8 | Release() 9 | } 10 | 11 | // NewIterator returns an iterator for a Document slice. 12 | func NewIterator(docs ...*Document) Iterator { 13 | return &docsIterator{docs: docs} 14 | } 15 | 16 | type docsIterator struct { 17 | docs []*Document 18 | index int 19 | } 20 | 21 | func (i *docsIterator) Next() (*Document, error) { 22 | if i.index >= len(i.docs) { 23 | return nil, nil 24 | } 25 | d := i.docs[i.index] 26 | i.index++ 27 | return d, nil 28 | } 29 | 30 | func (i *docsIterator) Release() { 31 | i.docs = nil 32 | } 33 | 34 | // CollectionIterator is an iterator for Collection's. 35 | type CollectionIterator interface { 36 | // Next collection, or nil. 37 | Next() (*Collection, error) 38 | // Release resources associated with the iterator. 39 | Release() 40 | } 41 | 42 | // NewCollectionIterator returns an iterator for a Collection slice. 43 | func NewCollectionIterator(cols []*Collection) CollectionIterator { 44 | return &colsIterator{cols: cols} 45 | } 46 | 47 | type colsIterator struct { 48 | cols []*Collection 49 | index int 50 | } 51 | 52 | func (i *colsIterator) Next() (*Collection, error) { 53 | if i.index >= len(i.cols) { 54 | return nil, nil 55 | } 56 | c := i.cols[i.index] 57 | i.index++ 58 | return c, nil 59 | } 60 | 61 | func (i *colsIterator) Release() { 62 | i.cols = nil 63 | } 64 | -------------------------------------------------------------------------------- /dstore/options.go: -------------------------------------------------------------------------------- 1 | package dstore 2 | 3 | // Options ... 4 | type Options struct { 5 | // Prefix to filter on. 6 | Prefix string 7 | // Index is offset into number of documents. 8 | Index int 9 | // Limit is number of documents (max) to return. 10 | Limit int 11 | // NoData to only include only path in Document (no data). 12 | NoData bool 13 | // Where 14 | Where *where 15 | } 16 | 17 | type where struct { 18 | Name string 19 | Op string 20 | Value interface{} 21 | } 22 | 23 | // Option ... 24 | type Option func(*Options) 25 | 26 | // NewOptions parses Options. 27 | func NewOptions(opts ...Option) Options { 28 | var options Options 29 | for _, o := range opts { 30 | o(&options) 31 | } 32 | return options 33 | } 34 | 35 | // Prefix to list. 36 | func Prefix(prefix string) Option { 37 | return func(o *Options) { 38 | o.Prefix = prefix 39 | } 40 | } 41 | 42 | // Where name op value. 43 | func Where(name string, op string, value interface{}) Option { 44 | return func(o *Options) { 45 | o.Where = &where{Name: name, Op: op, Value: value} 46 | } 47 | } 48 | 49 | // Index to start at. 50 | func Index(index int) Option { 51 | return func(o *Options) { 52 | o.Index = index 53 | } 54 | } 55 | 56 | // Limit number of results. 57 | func Limit(limit int) Option { 58 | return func(o *Options) { 59 | o.Limit = limit 60 | } 61 | } 62 | 63 | // NoData don't return data. 64 | func NoData() Option { 65 | return func(o *Options) { 66 | o.NoData = true 67 | } 68 | } 69 | 70 | // SetOptions ... 71 | type SetOptions struct { 72 | MergeAll bool 73 | } 74 | 75 | // SetOption ... 76 | type SetOption func(*SetOptions) 77 | 78 | // NewSetOptions parses Options. 79 | func NewSetOptions(opts ...SetOption) SetOptions { 80 | var options SetOptions 81 | for _, o := range opts { 82 | o(&options) 83 | } 84 | return options 85 | } 86 | 87 | // MergeAll merges values. 88 | func MergeAll() SetOption { 89 | return func(o *SetOptions) { 90 | o.MergeAll = true 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /dstore/path_test.go: -------------------------------------------------------------------------------- 1 | package dstore_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keys-pub/keys/dstore" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestPath(t *testing.T) { 11 | require.Equal(t, "/a/b", dstore.Path("a", "b")) 12 | require.Equal(t, "/a/b/1", dstore.Path("a", "b", 1)) 13 | require.Equal(t, "/a", dstore.Path("", "a")) 14 | require.Equal(t, "/a", dstore.Path("a", "")) 15 | 16 | require.Equal(t, "/", dstore.Path("")) 17 | require.Equal(t, "/", dstore.Path("/")) 18 | require.Equal(t, "/a", dstore.Path("a")) 19 | require.Equal(t, "/a", dstore.Path("/a")) 20 | require.Equal(t, "/a", dstore.Path("/a/")) 21 | require.Equal(t, "/a/b", dstore.Path("/a//b/")) 22 | } 23 | 24 | func TestPathComponents(t *testing.T) { 25 | require.Equal(t, []string{"test", "sub"}, dstore.PathComponents("/test//sub/")) 26 | require.Equal(t, []string{}, dstore.PathComponents("/")) 27 | 28 | require.Equal(t, "", dstore.PathLast("")) 29 | require.Equal(t, "", dstore.PathLast("/")) 30 | require.Equal(t, "a", dstore.PathLast("/a")) 31 | require.Equal(t, "b", dstore.PathLast("/a/b")) 32 | 33 | require.Equal(t, "", dstore.PathFirst("")) 34 | require.Equal(t, "", dstore.PathFirst("/")) 35 | require.Equal(t, "a", dstore.PathFirst("/a")) 36 | require.Equal(t, "a", dstore.PathFirst("/a/b")) 37 | 38 | require.Equal(t, "/a", dstore.PathFrom("/a", 1)) 39 | require.Equal(t, "/b", dstore.PathFrom("/a/b", 1)) 40 | require.Equal(t, "/b/c", dstore.PathFrom("/a/b/c", 1)) 41 | require.Equal(t, "/c", dstore.PathFrom("/a/b/c", 2)) 42 | } 43 | 44 | type obj struct { 45 | S string 46 | } 47 | 48 | func (o obj) String() string { 49 | return o.S 50 | } 51 | 52 | func TestPathValues(t *testing.T) { 53 | require.Equal(t, "/o/abc", dstore.Path("o", &obj{S: "abc"})) 54 | } 55 | -------------------------------------------------------------------------------- /dstore/private_test.go: -------------------------------------------------------------------------------- 1 | package dstore 2 | 3 | // Marshal for testing. 4 | var Marshal = marshal 5 | 6 | // Unmarshal for testing. 7 | var Unmarshal = unmarshal 8 | -------------------------------------------------------------------------------- /dstore/set.go: -------------------------------------------------------------------------------- 1 | package dstore 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | ) 7 | 8 | // StringSet is a set of strings. 9 | type StringSet struct { 10 | strsMap map[string]bool 11 | strs []string 12 | } 13 | 14 | // NewStringSet creates StringSet. 15 | func NewStringSet(s ...string) *StringSet { 16 | return newStringSet(len(s), s...) 17 | } 18 | 19 | // NewStringSetWithCapacity .. 20 | func NewStringSetWithCapacity(capacity int) *StringSet { 21 | return newStringSet(capacity) 22 | } 23 | 24 | func newStringSet(capacity int, s ...string) *StringSet { 25 | strsMap := make(map[string]bool, capacity) 26 | strs := make([]string, 0, capacity) 27 | for _, v := range s { 28 | strsMap[v] = true 29 | strs = append(strs, v) 30 | } 31 | return &StringSet{ 32 | strsMap: strsMap, 33 | strs: strs, 34 | } 35 | } 36 | 37 | // NewStringSetSplit creates StringSet for split string. 38 | func NewStringSetSplit(s string, delim string) *StringSet { 39 | strs := strings.Split(s, delim) 40 | if len(strs) == 1 && strs[0] == "" { 41 | return NewStringSet() 42 | } 43 | return NewStringSet(strs...) 44 | } 45 | 46 | // Contains returns true if set contains string. 47 | func (s *StringSet) Contains(str string) bool { 48 | return s.strsMap[str] 49 | } 50 | 51 | // Add to set. 52 | func (s *StringSet) Add(str string) { 53 | if s.Contains(str) { 54 | return 55 | } 56 | s.strsMap[str] = true 57 | s.strs = append(s.strs, str) 58 | } 59 | 60 | // AddAll to set. 61 | func (s *StringSet) AddAll(strs []string) { 62 | for _, str := range strs { 63 | s.Add(str) 64 | } 65 | } 66 | 67 | // Remove from set. 68 | func (s *StringSet) Remove(str string) { 69 | delete(s.strsMap, str) 70 | keys := make([]string, 0, len(s.strs)) 71 | for _, k := range s.strs { 72 | if k != str { 73 | keys = append(keys, k) 74 | } 75 | } 76 | s.strs = keys 77 | } 78 | 79 | // Size for set. 80 | func (s *StringSet) Size() int { 81 | return len(s.strs) 82 | } 83 | 84 | // Clear set. 85 | func (s *StringSet) Clear() { 86 | s.strsMap = map[string]bool{} 87 | s.strs = []string{} 88 | } 89 | 90 | // Strings returns strings in set. 91 | func (s *StringSet) Strings() []string { 92 | // Copy to prevent caller changing s.strs 93 | keys := make([]string, 0, len(s.strs)) 94 | keys = append(keys, s.strs...) 95 | return keys 96 | } 97 | 98 | // Sorted returns strings in set, sorted. 99 | func (s *StringSet) Sorted() []string { 100 | strs := s.Strings() 101 | sort.Strings(strs) 102 | return strs 103 | } 104 | -------------------------------------------------------------------------------- /dstore/set_test.go: -------------------------------------------------------------------------------- 1 | package dstore_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keys-pub/keys/dstore" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestStringSet(t *testing.T) { 11 | s := dstore.NewStringSet("a", "b", "c") 12 | require.True(t, s.Contains("a")) 13 | require.False(t, s.Contains("z")) 14 | s.Add("z") 15 | require.True(t, s.Contains("z")) 16 | s.Add("z") 17 | require.Equal(t, 4, s.Size()) 18 | s.AddAll([]string{"m", "n"}) 19 | 20 | expected := []string{"a", "b", "c", "z", "m", "n"} 21 | require.Equal(t, expected, s.Strings()) 22 | 23 | s.Clear() 24 | require.False(t, s.Contains("a")) 25 | require.False(t, s.Contains("z")) 26 | require.Equal(t, 0, s.Size()) 27 | } 28 | 29 | func TestStringSetSplit(t *testing.T) { 30 | s := dstore.NewStringSetSplit("a,b,c", ",") 31 | require.Equal(t, 3, s.Size()) 32 | require.True(t, s.Contains("a")) 33 | 34 | s = dstore.NewStringSetSplit("a", ",") 35 | require.Equal(t, 1, s.Size()) 36 | require.True(t, s.Contains("a")) 37 | 38 | s = dstore.NewStringSetSplit("", ",") 39 | require.Equal(t, 0, s.Size()) 40 | } 41 | -------------------------------------------------------------------------------- /dstore/spew.go: -------------------------------------------------------------------------------- 1 | package dstore 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | 7 | "github.com/davecgh/go-spew/spew" 8 | ) 9 | 10 | // Spew writes Iterator to buffer. 11 | func Spew(iter Iterator) (string, error) { 12 | var b bytes.Buffer 13 | if err := SpewOut(iter, &b); err != nil { 14 | return "", err 15 | } 16 | return b.String(), nil 17 | } 18 | 19 | // SpewOut writes Iterator to io.Writer. 20 | func SpewOut(iter Iterator, out io.Writer) error { 21 | for { 22 | doc, err := iter.Next() 23 | if err != nil { 24 | return err 25 | } 26 | if doc == nil { 27 | break 28 | } 29 | if _, err := out.Write([]byte(spew.Sdump(doc))); err != nil { 30 | return err 31 | } 32 | } 33 | return nil 34 | } 35 | -------------------------------------------------------------------------------- /ed25519.go: -------------------------------------------------------------------------------- 1 | // Copyright 2019 Google LLC 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd 6 | 7 | package keys 8 | 9 | import ( 10 | "crypto/sha512" 11 | "math/big" 12 | 13 | "golang.org/x/crypto/curve25519" 14 | "golang.org/x/crypto/ed25519" 15 | ) 16 | 17 | var x25519P, _ = new(big.Int).SetString("57896044618658097711785492504343953926634992332820282019728792003956564819949", 10) 18 | 19 | func ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) []byte { 20 | // ed25519.PublicKey is a little endian representation of the y-coordinate, 21 | // with the most significant bit set based on the sign of the x-coordinate. 22 | bigEndianY := make([]byte, ed25519.PublicKeySize) 23 | for i, b := range pk { 24 | bigEndianY[ed25519.PublicKeySize-i-1] = b 25 | } 26 | bigEndianY[0] &= 0b0111_1111 27 | 28 | // The Montgomery u-coordinate is derived through the bilinear map 29 | // 30 | // u = (1 + y) / (1 - y) 31 | // 32 | // See https://blog.filippo.io/using-ed25519-keys-for-encryption. 33 | y := new(big.Int).SetBytes(bigEndianY) 34 | denom := big.NewInt(1) 35 | denom.ModInverse(denom.Sub(denom, y), x25519P) // 1 / (1 - y) 36 | u := y.Mul(y.Add(y, big.NewInt(1)), denom) 37 | u.Mod(u, x25519P) 38 | 39 | out := make([]byte, curve25519.PointSize) 40 | uBytes := u.Bytes() 41 | for i, b := range uBytes { 42 | out[len(uBytes)-i-1] = b 43 | } 44 | 45 | return out 46 | } 47 | 48 | func ed25519PrivateKeyToCurve25519(pk ed25519.PrivateKey) []byte { 49 | h := sha512.New() 50 | if _, err := h.Write(pk.Seed()); err != nil { 51 | panic(err) 52 | } 53 | out := h.Sum(nil) 54 | return out[:curve25519.ScalarSize] 55 | } 56 | -------------------------------------------------------------------------------- /encoding/README.md: -------------------------------------------------------------------------------- 1 | # Encoding 2 | 3 | ```go 4 | encoding.MustEncode(keys.RandBytes(16), encoding.Base32, encoding.NoPadding(), encoding.Lowercase()) 5 | ``` 6 | -------------------------------------------------------------------------------- /encoding/bip39.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/pkg/errors" 7 | "github.com/tyler-smith/go-bip39" 8 | ) 9 | 10 | // ErrInvalidPhrase if phrase is invalid. 11 | type ErrInvalidPhrase struct { 12 | cause error 13 | } 14 | 15 | // ErrInvalidBIP39Input if invalid number of bytes for encoding. 16 | var ErrInvalidBIP39Input = errors.New("bip39 only accepts 16, 20, 24, 28, 32 bytes") 17 | 18 | func (e ErrInvalidPhrase) Error() string { 19 | return "invalid phrase" 20 | } 21 | 22 | // Cause for ErrInvalidPhrase 23 | func (e ErrInvalidPhrase) Cause() error { 24 | return e.cause 25 | } 26 | 27 | // BytesToPhrase returns a phrase for bytes 28 | func BytesToPhrase(b []byte) (string, error) { 29 | out, err := bip39.NewMnemonic(b) 30 | if err != nil { 31 | if err == bip39.ErrEntropyLengthInvalid { 32 | return "", ErrInvalidBIP39Input 33 | } 34 | return "", err 35 | } 36 | return out, nil 37 | } 38 | 39 | // PhraseToBytes decodes a bip39 mnemonic into bytes 40 | func PhraseToBytes(phrase string, sanitize bool) (*[32]byte, error) { 41 | if sanitize { 42 | phrase = sanitizePhrase(phrase) 43 | } 44 | b, err := bip39.MnemonicToByteArray(phrase, true) 45 | if err != nil { 46 | return nil, ErrInvalidPhrase{cause: err} 47 | } 48 | if l := len(b); l != 32 { 49 | return nil, ErrInvalidPhrase{cause: errors.Errorf("invalid bip39 bytes length")} 50 | } 51 | var b32 [32]byte 52 | copy(b32[:], b[:32]) 53 | return &b32, nil 54 | } 55 | 56 | func sanitizePhrase(phrase string) string { 57 | phrase = strings.TrimSpace(strings.ToLower(phrase)) 58 | return strings.Join(strings.Fields(phrase), " ") 59 | } 60 | 61 | // IsValidPhrase checks is phrase is valid 62 | func IsValidPhrase(phrase string, sanitize bool) bool { 63 | if sanitize { 64 | phrase = sanitizePhrase(phrase) 65 | } 66 | return bip39.IsMnemonicValid(phrase) 67 | } 68 | -------------------------------------------------------------------------------- /encoding/bip39_test.go: -------------------------------------------------------------------------------- 1 | package encoding_test 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "testing" 7 | 8 | "github.com/keys-pub/keys/encoding" 9 | "github.com/pkg/errors" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func randBytes(length int) []byte { 14 | buf := make([]byte, length) 15 | if _, err := rand.Read(buf); err != nil { 16 | panic(err) 17 | } 18 | return buf 19 | } 20 | 21 | func TestPhrase(t *testing.T) { 22 | b, err := encoding.PhraseToBytes("invalid phrase", false) 23 | require.EqualError(t, err, "invalid phrase") 24 | require.Nil(t, b) 25 | 26 | b, err = encoding.PhraseToBytes("shove quiz copper settle harvest victory shell fade soft neck awake churn craft venue pause utility service degree invite inspire swing detect pipe sibling", false) 27 | require.NoError(t, err) 28 | require.Equal(t, "c715fcbfe23697e7715a8ece527440946321e4e85f82c42739d83aadc078e956", hex.EncodeToString(b[:])) 29 | 30 | b, err = encoding.PhraseToBytes("shove quiz copper settle harvest victory shell fade soft neck awake churn", false) 31 | require.EqualError(t, err, "invalid phrase") 32 | require.EqualError(t, errors.Cause(err), "Invalid mnenomic") 33 | require.Nil(t, b) 34 | } 35 | 36 | func TestPhraseFromKey(t *testing.T) { 37 | key := randBytes(32) 38 | phrase, err := encoding.BytesToPhrase(key) 39 | require.NoError(t, err) 40 | keyOut, err := encoding.PhraseToBytes(phrase, false) 41 | require.NoError(t, err) 42 | require.Equal(t, key, keyOut[:]) 43 | } 44 | -------------------------------------------------------------------------------- /encoding/is.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "regexp" 5 | "unicode/utf8" 6 | ) 7 | 8 | // IsAlphaNumeric returns true if string is only a-z, A-Z, 0-9 with optional extra characters. 9 | func IsAlphaNumeric(s string, extra string) bool { 10 | return regexp.MustCompile(`^[a-zA-Z0-9` + extra + `]+$`).MatchString(s) 11 | } 12 | 13 | // IsASCII returns true if bytes are ASCII. 14 | func IsASCII(b []byte) bool { 15 | isASCII := true 16 | for i := 0; i < len(b); i++ { 17 | c := b[i] 18 | if c >= utf8.RuneSelf { 19 | isASCII = false 20 | break 21 | } 22 | } 23 | return isASCII 24 | } 25 | 26 | // HasUpper returns true if string has an uppercase character. 27 | func HasUpper(s string) bool { 28 | for i := 0; i < len(s); i++ { 29 | c := s[i] 30 | if c >= 'A' && c <= 'Z' { 31 | return true 32 | } 33 | } 34 | return false 35 | } 36 | -------------------------------------------------------------------------------- /encoding/is_test.go: -------------------------------------------------------------------------------- 1 | package encoding_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keys-pub/keys/encoding" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestIsASCII(t *testing.T) { 11 | require.True(t, encoding.IsASCII([]byte("ok"))) 12 | require.False(t, encoding.IsASCII([]byte{0xFF})) 13 | } 14 | 15 | func TestIsAlphaNumeric(t *testing.T) { 16 | require.False(t, encoding.IsAlphaNumeric("", "")) 17 | require.True(t, encoding.IsAlphaNumeric("a", "")) 18 | require.True(t, encoding.IsAlphaNumeric("A", "")) 19 | require.True(t, encoding.IsAlphaNumeric("0", "")) 20 | require.True(t, encoding.IsAlphaNumeric("-", "-")) 21 | require.True(t, encoding.IsAlphaNumeric("_", "_")) 22 | require.True(t, encoding.IsAlphaNumeric("Abc-def", "-")) 23 | require.False(t, encoding.IsAlphaNumeric("Abc-def", "_")) 24 | require.False(t, encoding.IsAlphaNumeric(":", "")) 25 | } 26 | 27 | func TestHasUpper(t *testing.T) { 28 | require.False(t, encoding.HasUpper("ok")) 29 | require.True(t, encoding.HasUpper("Ok")) 30 | } 31 | -------------------------------------------------------------------------------- /encoding/options.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import "github.com/pkg/errors" 4 | 5 | // EncodeOptions ... 6 | type EncodeOptions struct { 7 | // NoPadding, for encodings that can disable padding (base64, base32). 8 | NoPadding bool 9 | // Lowercase, for encodings that can be upper or lower case, use lowercase (base32). 10 | Lowercase bool 11 | } 12 | 13 | // EncodeOption ... 14 | type EncodeOption struct { 15 | Apply func(*EncodeOptions) 16 | Encodings []Encoding 17 | Name string 18 | } 19 | 20 | func newEncodeOptions(opts []EncodeOption, encoding Encoding) (EncodeOptions, error) { 21 | var options EncodeOptions 22 | for _, o := range opts { 23 | if !containsEncoding(o.Encodings, encoding) { 24 | return options, errors.Errorf("invalid option: %s", o.Name) 25 | } 26 | o.Apply(&options) 27 | } 28 | return options, nil 29 | } 30 | 31 | // NoPadding ... 32 | func NoPadding() EncodeOption { 33 | apply := func(o *EncodeOptions) { 34 | o.NoPadding = true 35 | } 36 | return EncodeOption{ 37 | Apply: apply, 38 | Encodings: []Encoding{Base64, Base32}, 39 | Name: "no-padding", 40 | } 41 | } 42 | 43 | // Lowercase ... 44 | func Lowercase() EncodeOption { 45 | apply := func(o *EncodeOptions) { 46 | o.Lowercase = true 47 | } 48 | return EncodeOption{ 49 | Apply: apply, 50 | Encodings: []Encoding{Base32}, 51 | Name: "lowercase", 52 | } 53 | } 54 | 55 | func containsEncoding(encs []Encoding, enc Encoding) bool { 56 | for _, e := range encs { 57 | if e == enc { 58 | return true 59 | } 60 | } 61 | return false 62 | } 63 | -------------------------------------------------------------------------------- /encoding/parse.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "html" 5 | "regexp" 6 | "strings" 7 | ) 8 | 9 | // BreakString breaks words and lines. 10 | func BreakString(msg string, wordLen int, lineLen int) string { 11 | words := []string{} 12 | lines := []string{} 13 | for i := 0; i < len(msg); i += wordLen { 14 | end := i + wordLen 15 | if end > len(msg) { 16 | end = len(msg) 17 | } 18 | word := msg[i:end] 19 | if l := len(words); l != 0 && l%lineLen == 0 { 20 | lines = append(lines, strings.Join(words, " ")) 21 | words = []string{} 22 | } 23 | words = append(words, word) 24 | } 25 | lines = append(lines, strings.Join(words, " ")) 26 | return strings.Join(lines, "\n") 27 | } 28 | 29 | func stripTags(body string) string { 30 | re := regexp.MustCompile(`<\/?[^>]+(>|$)`) 31 | return re.ReplaceAllString(body, "") 32 | } 33 | 34 | var regexSpaces = regexp.MustCompile(`[ ]+`) 35 | 36 | // FindSaltpack finds saltpack message in a string starting with "BEGIN {BRAND }MESSAGE." 37 | // and ending with "END {BRAND }MESSAGE". {BRAND } is optional. 38 | // If isHTML is true, we html unescape the string first. 39 | func FindSaltpack(msg string, isHTML bool) (string, string) { 40 | if isHTML { 41 | msg = html.UnescapeString(msg) 42 | } 43 | 44 | re := regexp.MustCompile(`(?s).*BEGIN (.*)MESSAGE\.(.*)END .*MESSAGE.*`) 45 | s := re.FindStringSubmatch(msg) 46 | 47 | brand, out := "", "" 48 | if len(s) >= 2 { 49 | brand = strings.TrimSpace(TrimSaltpack(s[1], []rune{' '})) 50 | } 51 | if len(s) >= 3 { 52 | out = s[2] 53 | out = strings.ReplaceAll(out, "\\n", "") 54 | } 55 | 56 | if isHTML { 57 | out = stripTags(out) 58 | } 59 | out = TrimSaltpack(out, []rune{' ', '\n', '.'}) 60 | out = regexSpaces.ReplaceAllString(out, " ") 61 | out = strings.TrimSpace(out) 62 | out = strings.Trim(out, "\n") 63 | return out, brand 64 | } 65 | -------------------------------------------------------------------------------- /encoding/saltpack.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // TrimSaltpack removes non base63 characters from a string. 11 | func TrimSaltpack(msg string, allow []rune) string { 12 | charsOnly := func(r rune) rune { 13 | // 0-9, A-Z, a-z 14 | if (r >= 0x30 && r <= 0x39) || (r >= 0x41 && r <= 0x5A) || (r >= 0x61 && r <= 0x7A) { 15 | return r 16 | } 17 | for _, a := range allow { 18 | if r == a { 19 | return r 20 | } 21 | } 22 | return -1 23 | } 24 | return strings.Map(charsOnly, msg) 25 | } 26 | 27 | // EncodeSaltpack encodes bytes to saltpack message. 28 | func EncodeSaltpack(b []byte, brand string) string { 29 | return saltpackStart(brand) + "\n" + encodeSaltpack(b) + "\n" + saltpackEnd(brand) 30 | } 31 | 32 | // DecodeSaltpack decodes saltpack message. 33 | func DecodeSaltpack(msg string, isHTML bool) ([]byte, string, error) { 34 | s, brand := FindSaltpack(msg, isHTML) 35 | if s == "" { 36 | return nil, "", nil 37 | } 38 | s = TrimSaltpack(s, nil) 39 | b, err := Decode(s, Base62) 40 | if err != nil { 41 | return nil, "", errors.Wrapf(err, "failed to decode saltpack message") 42 | } 43 | return b, brand, nil 44 | } 45 | 46 | func encodeSaltpack(b []byte) string { 47 | out := MustEncode(b, Base62) 48 | out = out + "." 49 | return BreakString(out, 15, 4) 50 | } 51 | 52 | func decodeSaltpack(s string) ([]byte, error) { 53 | s = TrimSaltpack(s, nil) 54 | return Decode(s, Base62) 55 | } 56 | 57 | // saltpackStart start of a saltpack message. 58 | func saltpackStart(brand string) string { 59 | if brand == "" { 60 | return "BEGIN MESSAGE." 61 | } 62 | return fmt.Sprintf("BEGIN %s MESSAGE.", brand) 63 | } 64 | 65 | // saltpackEnd end of a saltpack message. 66 | func saltpackEnd(brand string) string { 67 | if brand == "" { 68 | return "END MESSAGE." 69 | } 70 | return fmt.Sprintf("END %s MESSAGE.", brand) 71 | } 72 | -------------------------------------------------------------------------------- /encoding/saltpack_test.go: -------------------------------------------------------------------------------- 1 | package encoding_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/keys-pub/keys/encoding" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestSaltpackEncode(t *testing.T) { 12 | b := bytes.Repeat([]byte{0x01}, 128) 13 | msg := encoding.EncodeSaltpack(b, "TEST") 14 | expected := `BEGIN TEST MESSAGE. 15 | 0El6XFXwsUFD8J2 vGxsaboW7rZYnQR BP5d9erwRwd290E l6XFXwsUFD8J2vG 16 | xsaboW7rZYnQRBP 5d9erwRwd290El6 XFXwsUFD8J2vGxs aboW7rZYnQRBP5d 17 | 9erwRwd290El6XF XwsUFD8J2vGxsab oW7rZYnQRBP5d9e rwRwd29. 18 | END TEST MESSAGE.` 19 | require.Equal(t, expected, msg) 20 | 21 | out, brand, err := encoding.DecodeSaltpack(msg, false) 22 | require.NoError(t, err) 23 | require.Equal(t, b, out) 24 | require.Equal(t, "TEST", brand) 25 | 26 | msg = encoding.EncodeSaltpack(b, "") 27 | expected = `BEGIN MESSAGE. 28 | 0El6XFXwsUFD8J2 vGxsaboW7rZYnQR BP5d9erwRwd290E l6XFXwsUFD8J2vG 29 | xsaboW7rZYnQRBP 5d9erwRwd290El6 XFXwsUFD8J2vGxs aboW7rZYnQRBP5d 30 | 9erwRwd290El6XF XwsUFD8J2vGxsab oW7rZYnQRBP5d9e rwRwd29. 31 | END MESSAGE.` 32 | require.Equal(t, expected, msg) 33 | } 34 | -------------------------------------------------------------------------------- /env/options.go: -------------------------------------------------------------------------------- 1 | package env 2 | 3 | // PathOptions ... 4 | type PathOptions struct { 5 | Dirs []string 6 | File string 7 | Mkdir bool 8 | } 9 | 10 | // PathOption ... 11 | type PathOption func(*PathOptions) error 12 | 13 | func newOptions(opts ...PathOption) (PathOptions, error) { 14 | var options PathOptions 15 | for _, o := range opts { 16 | if err := o(&options); err != nil { 17 | return options, err 18 | } 19 | } 20 | return options, nil 21 | } 22 | 23 | // Dir ... 24 | func Dir(dirs ...string) PathOption { 25 | return func(o *PathOptions) error { 26 | o.Dirs = dirs 27 | return nil 28 | } 29 | } 30 | 31 | // File ... 32 | func File(file string) PathOption { 33 | return func(o *PathOptions) error { 34 | o.File = file 35 | return nil 36 | } 37 | } 38 | 39 | // Mkdir ... 40 | func Mkdir() PathOption { 41 | return func(o *PathOptions) error { 42 | o.Mkdir = true 43 | return nil 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /env/paths_darwin_test.go: -------------------------------------------------------------------------------- 1 | package env_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/keys-pub/keys/env" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestPaths(t *testing.T) { 13 | appDir, err := env.AppPath(env.Dir("KeysEnvTest")) 14 | require.NoError(t, err) 15 | require.Equal(t, filepath.Join(env.MustHomeDir(), "/Library/Application Support/KeysEnvTest"), appDir) 16 | exists, err := env.PathExists(appDir) 17 | require.NoError(t, err) 18 | require.False(t, exists) 19 | 20 | logsDir, err := env.LogsPath(env.Dir("KeysEnvTest")) 21 | require.NoError(t, err) 22 | require.Equal(t, filepath.Join(env.MustHomeDir(), "/Library/Logs/KeysEnvTest"), logsDir) 23 | exists, err = env.PathExists(logsDir) 24 | require.NoError(t, err) 25 | require.False(t, exists) 26 | 27 | configPath, err := env.ConfigPath(env.Dir("KeysEnvTest"), env.File("test.txt"), env.Mkdir()) 28 | require.NoError(t, err) 29 | require.Equal(t, configPath, filepath.Join(env.MustHomeDir(), "/Library/Application Support/KeysEnvTest/test.txt")) 30 | configDir, file := filepath.Split(configPath) 31 | require.Equal(t, configDir, filepath.Join(env.MustHomeDir(), "/Library/Application Support/KeysEnvTest")+"/") 32 | require.Equal(t, "test.txt", file) 33 | defer os.RemoveAll(configDir) 34 | exists, err = env.PathExists(configDir) 35 | require.NoError(t, err) 36 | require.True(t, exists) 37 | } 38 | 39 | func TestAllDirs(t *testing.T) { 40 | dirs, err := env.AllDirs("KeysEnvTest") 41 | require.NoError(t, err) 42 | require.Equal(t, []string{ 43 | filepath.Join(env.MustHomeDir(), "/Library/Application Support/KeysEnvTest"), 44 | filepath.Join(env.MustHomeDir(), "/Library/Logs/KeysEnvTest"), 45 | }, dirs) 46 | } 47 | -------------------------------------------------------------------------------- /env/paths_linux_test.go: -------------------------------------------------------------------------------- 1 | package env_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/keys-pub/keys/env" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestPaths(t *testing.T) { 13 | appDir, err := env.AppPath(env.Dir("KeysEnvTest")) 14 | require.NoError(t, err) 15 | require.Equal(t, filepath.Join(env.MustHomeDir(), `/.local/share/KeysEnvTest`), appDir) 16 | exists, err := env.PathExists(appDir) 17 | require.NoError(t, err) 18 | require.False(t, exists) 19 | 20 | logsDir, err := env.LogsPath(env.Dir("KeysEnvTest")) 21 | require.NoError(t, err) 22 | require.Equal(t, filepath.Join(env.MustHomeDir(), `/.cache/KeysEnvTest`), logsDir) 23 | exists, err = env.PathExists(logsDir) 24 | require.NoError(t, err) 25 | require.False(t, exists) 26 | 27 | configPath, err := env.ConfigPath(env.Dir("KeysEnvTest"), env.File("test.txt"), env.Mkdir()) 28 | require.NoError(t, err) 29 | require.Equal(t, filepath.Join(env.MustHomeDir(), "/.config/KeysEnvTest/test.txt"), configPath) 30 | configDir, file := filepath.Split(configPath) 31 | require.Equal(t, filepath.Join(env.MustHomeDir(), "/.config/KeysEnvTest/")+"/", configDir) 32 | require.Equal(t, "test.txt", file) 33 | defer os.RemoveAll(configDir) 34 | exists, err = env.PathExists(configDir) 35 | require.NoError(t, err) 36 | require.True(t, exists) 37 | } 38 | 39 | func TestAppPathXDG(t *testing.T) { 40 | prev := os.Getenv("XDG_DATA_HOME") 41 | defer func() { os.Setenv("XDG_DATA_HOME", prev) }() 42 | err := os.Setenv("XDG_DATA_HOME", "/test/data") 43 | require.NoError(t, err) 44 | 45 | configPath, err := env.AppPath(env.Dir("KeysEnvTest")) 46 | require.NoError(t, err) 47 | require.Equal(t, "/test/data/KeysEnvTest", configPath) 48 | } 49 | 50 | func TestConfigPathXDG(t *testing.T) { 51 | prev := os.Getenv("XDG_CONFIG_HOME") 52 | defer func() { os.Setenv("XDG_CONFIG_HOME", prev) }() 53 | err := os.Setenv("XDG_CONFIG_HOME", "/test/config") 54 | require.NoError(t, err) 55 | 56 | configPath, err := env.ConfigPath(env.Dir("KeysEnvTest")) 57 | require.NoError(t, err) 58 | require.Equal(t, "/test/config/KeysEnvTest", configPath) 59 | } 60 | 61 | func TestLogsPathXDG(t *testing.T) { 62 | prev := os.Getenv("XDG_CACHE_HOME") 63 | defer func() { os.Setenv("XDG_CACHE_HOME", prev) }() 64 | err := os.Setenv("XDG_CACHE_HOME", "/test/cache") 65 | require.NoError(t, err) 66 | 67 | configPath, err := env.LogsPath(env.Dir("KeysEnvTest")) 68 | require.NoError(t, err) 69 | require.Equal(t, "/test/cache/KeysEnvTest", configPath) 70 | } 71 | 72 | func TestAllDirs(t *testing.T) { 73 | dirs, err := env.AllDirs("KeysEnvTest") 74 | require.NoError(t, err) 75 | require.Equal(t, []string{ 76 | filepath.Join(env.MustHomeDir(), "/.local/share/KeysEnvTest"), 77 | filepath.Join(env.MustHomeDir(), "/.config/KeysEnvTest"), 78 | filepath.Join(env.MustHomeDir(), "/.cache/KeysEnvTest"), 79 | }, dirs) 80 | } 81 | -------------------------------------------------------------------------------- /env/paths_test.go: -------------------------------------------------------------------------------- 1 | package env_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "testing" 7 | 8 | "github.com/keys-pub/keys" 9 | 10 | "github.com/keys-pub/keys/env" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestMkdir(t *testing.T) { 15 | dir := "KeysTest-" + keys.RandFileName() 16 | defer func() { _ = os.RemoveAll(dir) }() 17 | 18 | path, err := env.AppPath(env.Dir(dir), env.File("test.txt")) 19 | require.NoError(t, err) 20 | 21 | exists, err := env.PathExists(dir) 22 | require.NoError(t, err) 23 | require.False(t, exists) 24 | 25 | exists, err = env.PathExists(path) 26 | require.NoError(t, err) 27 | require.False(t, exists) 28 | 29 | path, err = env.AppPath(env.Dir(dir), env.File("test.txt"), env.Mkdir()) 30 | require.NoError(t, err) 31 | 32 | dir, file := filepath.Split(path) 33 | require.Equal(t, "test.txt", file) 34 | exists, err = env.PathExists(dir) 35 | require.NoError(t, err) 36 | require.True(t, exists) 37 | 38 | exists, err = env.PathExists(path) 39 | require.NoError(t, err) 40 | require.False(t, exists) 41 | } 42 | -------------------------------------------------------------------------------- /env/paths_windows_test.go: -------------------------------------------------------------------------------- 1 | package env_test 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/keys-pub/keys/env" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestPaths(t *testing.T) { 14 | appDir, err := env.AppPath(env.Dir("KeysEnvTest")) 15 | require.NoError(t, err) 16 | require.True(t, strings.HasSuffix(appDir, `\AppData\Local\KeysEnvTest`)) 17 | exists, err := env.PathExists(appDir) 18 | require.NoError(t, err) 19 | require.False(t, exists) 20 | 21 | logsDir, err := env.LogsPath(env.Dir("KeysEnvTest")) 22 | require.NoError(t, err) 23 | require.True(t, strings.HasSuffix(logsDir, `\AppData\Local\KeysEnvTest`)) 24 | exists, err = env.PathExists(logsDir) 25 | require.NoError(t, err) 26 | require.False(t, exists) 27 | 28 | configPath, err := env.ConfigPath(env.Dir("KeysEnvTest"), env.File("test.txt"), env.Mkdir()) 29 | require.NoError(t, err) 30 | require.Equal(t, filepath.Join(env.MustHomeDir(), `\AppData\Roaming\KeysEnvTest\test.txt`), configPath) 31 | configDir, file := filepath.Split(configPath) 32 | require.Equal(t, filepath.Join(env.MustHomeDir(), `\AppData\Roaming\KeysEnvTest`)+`\`, configDir) 33 | require.Equal(t, "test.txt", file) 34 | defer os.RemoveAll(configDir) 35 | exists, err = env.PathExists(configDir) 36 | require.NoError(t, err) 37 | require.True(t, exists) 38 | } 39 | 40 | func TestAllDirs(t *testing.T) { 41 | dirs, err := env.AllDirs("KeysEnvTest") 42 | require.NoError(t, err) 43 | require.Equal(t, []string{ 44 | filepath.Join(env.MustHomeDir(), `\AppData\Local\KeysEnvTest`), 45 | filepath.Join(env.MustHomeDir(), `\AppData\Roaming\KeysEnvTest`), 46 | }, dirs) 47 | } 48 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/pkg/errors" 7 | ) 8 | 9 | // ErrVerifyFailed if key verify failed. 10 | var ErrVerifyFailed = errors.New("verify failed") 11 | 12 | // ErrNotFound describes a key not found error when a key is required. 13 | type ErrNotFound struct { 14 | ID string 15 | } 16 | 17 | // NewErrNotFound constructs a ErrNotFound. 18 | func NewErrNotFound(id string) error { 19 | return ErrNotFound{ID: id} 20 | } 21 | 22 | func (e ErrNotFound) Error() string { 23 | if e.ID == "" { 24 | return "not found" 25 | } 26 | return fmt.Sprintf("%s not found", e.ID) 27 | } 28 | 29 | type tempError interface { 30 | Temporary() bool 31 | } 32 | 33 | // IsTemporaryError returns true if the error has Temporary() function and that returns true 34 | func IsTemporaryError(err error) bool { 35 | switch err := errors.Cause(err).(type) { 36 | case tempError: 37 | if err.Temporary() { 38 | return true 39 | } 40 | } 41 | return false 42 | } 43 | -------------------------------------------------------------------------------- /errors_test.go: -------------------------------------------------------------------------------- 1 | package keys_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keys-pub/keys" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestNewErrNotFound(t *testing.T) { 11 | require.EqualError(t, keys.NewErrNotFound("123"), "123 not found") 12 | require.EqualError(t, keys.NewErrNotFound(""), "not found") 13 | } 14 | 15 | type errTest struct{} 16 | 17 | func (t errTest) Error() string { 18 | return "temporary error" 19 | } 20 | 21 | func (t errTest) Temporary() bool { 22 | return true 23 | } 24 | 25 | func (t errTest) Timeout() bool { 26 | return true 27 | } 28 | 29 | func TestIsTemporaryError(t *testing.T) { 30 | require.True(t, keys.IsTemporaryError(&errTest{})) 31 | } 32 | -------------------------------------------------------------------------------- /func.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | // RetryE will retry the fn (error) if the error is temporary (such as a temporary net.Error) 4 | func RetryE(fn func() error) error { 5 | err := fn() 6 | if err != nil { 7 | if IsTemporaryError(err) { 8 | logger.Warningf("Temporary error (will attempt a retry): %+v", err) 9 | // Retry 10 | return fn() 11 | } 12 | return err 13 | } 14 | return nil 15 | } 16 | 17 | // RetrySE will retry the fn (string, error) if the error is temporary (such as a temporary net.Error) 18 | func RetrySE(fn func() (string, error)) (string, error) { 19 | s, err := fn() 20 | if err != nil { 21 | if IsTemporaryError(err) { 22 | logger.Warningf("Temporary error (will attempt a retry): %+v", err) 23 | // Retry 24 | return fn() 25 | } 26 | return s, err 27 | } 28 | return s, nil 29 | } 30 | -------------------------------------------------------------------------------- /func_test.go: -------------------------------------------------------------------------------- 1 | package keys_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keys-pub/keys" 7 | "github.com/pkg/errors" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | type testRetry struct { 12 | attempt int 13 | wrap bool 14 | } 15 | 16 | func (t *testRetry) fn() error { 17 | t.attempt = t.attempt + 1 18 | if t.attempt == 1 { 19 | if t.wrap { 20 | return errors.Wrapf(&errTest{}, "failed on attempt %d", t.attempt) 21 | } 22 | return &errTest{} 23 | } 24 | return nil 25 | } 26 | 27 | func TestRetryEDefault(t *testing.T) { 28 | tr := &testRetry{} 29 | err := keys.RetryE(tr.fn) 30 | require.NoError(t, err) 31 | } 32 | 33 | func TestRetryError(t *testing.T) { 34 | err := keys.RetryE(func() error { 35 | return errors.Errorf("error") 36 | }) 37 | require.Error(t, err) 38 | } 39 | 40 | func TestRetrySError(t *testing.T) { 41 | _, err := keys.RetrySE(func() (string, error) { 42 | return "", errors.Errorf("error") 43 | }) 44 | require.Error(t, err) 45 | } 46 | 47 | func TestRetryEWrap(t *testing.T) { 48 | tr := &testRetry{wrap: true} 49 | err := keys.RetryE(tr.fn) 50 | require.NoError(t, err) 51 | } 52 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/keys-pub/keys 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/ScaleFT/sshkeys v0.0.0-20200327173127-6142f742bca5 7 | github.com/danieljoos/wincred v1.1.0 8 | github.com/davecgh/go-spew v1.1.1 9 | github.com/dchest/blake2b v1.0.0 10 | github.com/flynn/noise v1.0.0 11 | github.com/godbus/dbus v4.1.0+incompatible 12 | github.com/golang/protobuf v1.5.2 // indirect 13 | github.com/google/go-cmp v0.5.6 14 | github.com/keybase/go-keychain v0.0.0-20201121013009-976c83ec27a6 15 | github.com/keybase/saltpack v0.0.0-20210611181147-9dd0a21addc6 16 | github.com/keys-pub/secretservice v0.0.0-20200519003656-26e44b8df47f 17 | github.com/kr/text v0.2.0 // indirect 18 | github.com/pkg/errors v0.9.1 19 | github.com/stretchr/objx v0.3.0 // indirect 20 | github.com/stretchr/testify v1.7.0 21 | github.com/tyler-smith/go-bip39 v1.1.0 22 | github.com/vmihailenco/msgpack/v4 v4.3.12 23 | github.com/vmihailenco/tagparser v0.1.2 // indirect 24 | golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e 25 | golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect 26 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect 27 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect 28 | golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 // indirect 29 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 30 | google.golang.org/appengine v1.6.7 // indirect 31 | google.golang.org/protobuf v1.27.1 // indirect 32 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 33 | ) 34 | -------------------------------------------------------------------------------- /hkdf.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import ( 4 | "crypto/sha256" 5 | "io" 6 | 7 | "golang.org/x/crypto/hkdf" 8 | ) 9 | 10 | // HKDFSHA256 uses HKDF with SHA256. 11 | // The `len` for output byte length. 12 | // The `salt` is non-secret salt, optional (can be nil), recommended: hash-length random value. 13 | // The `info` is non-secret context info, optional (can be empty). 14 | func HKDFSHA256(secret []byte, len int, salt []byte, info []byte) []byte { 15 | hash := sha256.New 16 | hkdf := hkdf.New(hash, secret[:], salt, info) 17 | key := make([]byte, len) 18 | if _, err := io.ReadFull(hkdf, key); err != nil { 19 | panic(err) 20 | } 21 | return key 22 | } 23 | -------------------------------------------------------------------------------- /hkdf_test.go: -------------------------------------------------------------------------------- 1 | package keys_test 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/keys-pub/keys" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestHKDFSHA256Vectors(t *testing.T) { 12 | // https://tools.ietf.org/html/rfc5869, appendix A 13 | secret, err := hex.DecodeString("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b") 14 | require.NoError(t, err) 15 | salt, err := hex.DecodeString("000102030405060708090a0b0c") 16 | require.NoError(t, err) 17 | info, err := hex.DecodeString("f0f1f2f3f4f5f6f7f8f9") 18 | require.NoError(t, err) 19 | len := 42 20 | 21 | out := keys.HKDFSHA256(secret, len, salt, info) 22 | expected, err := hex.DecodeString("3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865") 23 | require.NoError(t, err) 24 | require.Equal(t, expected, out) 25 | } 26 | -------------------------------------------------------------------------------- /http/README.md: -------------------------------------------------------------------------------- 1 | # HTTP 2 | 3 | This package extends the net/http package to provide signed requests using a keys.EdX25519Key. 4 | 5 | ```go 6 | key := keys.GenerateEdX25519Key() 7 | 8 | // Vault POST 9 | content := []byte(`[{"data":"dGVzdGluZzE="},{"data":"dGVzdGluZzI="}]`) 10 | contentHash := http.ContentHash(content) 11 | req, err := http.NewAuthRequest("POST", "https://keys.pub/vault/"+key.ID().String(), bytes.NewReader(content), contentHash, time.Now(), key) 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | fmt.Printf("curl -H \"Authorization: %s\" -d %q %q\n", req.Header["Authorization"][0], string(content), req.URL.String()) 16 | 17 | // Vault GET 18 | req, err = http.NewAuthRequest("GET", "https://keys.pub/vault/"+key.ID().String(), nil, "", time.Now(), key) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | fmt.Printf("curl -H \"Authorization: %s\" %q\n", req.Header["Authorization"][0], req.URL.String()) 23 | ``` 24 | -------------------------------------------------------------------------------- /http/client/api.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | // APIResponse ... 11 | type APIResponse struct { 12 | Error *Error `json:"error,omitempty"` 13 | } 14 | 15 | // Error ... 16 | type Error struct { 17 | Message string `json:"message,omitempty"` 18 | Status int `json:"status,omitempty"` 19 | } 20 | 21 | func (e Error) Error() string { 22 | return fmt.Sprintf("%s (%d)", e.Message, e.Status) 23 | } 24 | 25 | func IsConflict(err error) bool { 26 | var rerr Error 27 | return errors.As(err, &rerr) && rerr.Status == http.StatusConflict 28 | } 29 | 30 | // Metadata ... 31 | type Metadata struct { 32 | CreatedAt time.Time `json:"createdAt"` 33 | UpdatedAt time.Time `json:"updatedAt"` 34 | } 35 | -------------------------------------------------------------------------------- /http/client/log.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | pkglog "log" 5 | ) 6 | 7 | var logger = NewLogger(ErrLevel) 8 | 9 | // SetLogger sets logger for the package. 10 | func SetLogger(l Logger) { 11 | logger = l 12 | } 13 | 14 | // Logger interface used in this package. 15 | type Logger interface { 16 | Debugf(format string, args ...interface{}) 17 | Infof(format string, args ...interface{}) 18 | Warningf(format string, args ...interface{}) 19 | Errorf(format string, args ...interface{}) 20 | Fatalf(format string, args ...interface{}) 21 | } 22 | 23 | // LogLevel ... 24 | type LogLevel int 25 | 26 | const ( 27 | // DebugLevel ... 28 | DebugLevel LogLevel = 3 29 | // InfoLevel ... 30 | InfoLevel LogLevel = 2 31 | // WarnLevel ... 32 | WarnLevel LogLevel = 1 33 | // ErrLevel ... 34 | ErrLevel LogLevel = 0 35 | ) 36 | 37 | // NewLogger ... 38 | func NewLogger(lev LogLevel) Logger { 39 | return &defaultLog{Level: lev} 40 | } 41 | 42 | func (l LogLevel) String() string { 43 | switch l { 44 | case DebugLevel: 45 | return "debug" 46 | case InfoLevel: 47 | return "info" 48 | case WarnLevel: 49 | return "warn" 50 | case ErrLevel: 51 | return "err" 52 | default: 53 | return "" 54 | } 55 | } 56 | 57 | type defaultLog struct { 58 | Level LogLevel 59 | } 60 | 61 | func (l defaultLog) Debugf(format string, args ...interface{}) { 62 | if l.Level >= 3 { 63 | pkglog.Printf("[DEBG] "+format+"\n", args...) 64 | } 65 | } 66 | 67 | func (l defaultLog) Infof(format string, args ...interface{}) { 68 | if l.Level >= 2 { 69 | pkglog.Printf("[INFO] "+format+"\n", args...) 70 | } 71 | } 72 | 73 | func (l defaultLog) Warningf(format string, args ...interface{}) { 74 | if l.Level >= 1 { 75 | pkglog.Printf("[WARN] "+format+"\n", args...) 76 | } 77 | } 78 | 79 | func (l defaultLog) Errorf(format string, args ...interface{}) { 80 | if l.Level >= 0 { 81 | pkglog.Printf("[ERR] "+format+"\n", args...) 82 | } 83 | } 84 | 85 | func (l defaultLog) Fatalf(format string, args ...interface{}) { 86 | pkglog.Fatalf(format, args...) 87 | } 88 | -------------------------------------------------------------------------------- /http/client_test.go: -------------------------------------------------------------------------------- 1 | package http_test 2 | -------------------------------------------------------------------------------- /http/example_test.go: -------------------------------------------------------------------------------- 1 | package http_test 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "time" 7 | 8 | "github.com/keys-pub/keys" 9 | "github.com/keys-pub/keys/http" 10 | ) 11 | 12 | func ExampleNewAuthRequest() { 13 | key := keys.GenerateEdX25519Key() 14 | 15 | // Vault POST 16 | content := []byte(`[{"data":"dGVzdGluZzE="},{"data":"dGVzdGluZzI="}]`) 17 | contentHash := http.ContentHash(content) 18 | req, err := http.NewAuthRequest("POST", "https://keys.pub/vault/"+key.ID().String(), bytes.NewReader(content), contentHash, time.Now(), key) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | log.Printf("curl -H \"Authorization: %s\" -d %q %q\n", req.Header["Authorization"][0], string(content), req.URL.String()) 23 | 24 | // Vault GET 25 | req, err = http.NewAuthRequest("GET", "https://keys.pub/vault/"+key.ID().String(), nil, "", time.Now(), key) 26 | if err != nil { 27 | log.Fatal(err) 28 | } 29 | log.Printf("curl -H \"Authorization: %s\" %q\n", req.Header["Authorization"][0], req.URL.String()) 30 | 31 | // Output: 32 | } 33 | -------------------------------------------------------------------------------- /http/github_test.go: -------------------------------------------------------------------------------- 1 | package http_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/keys-pub/keys/encoding" 8 | "github.com/keys-pub/keys/http" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestGithubRequest(t *testing.T) { 13 | client := http.NewClient() 14 | urs := "https://gist.github.com/gabriel/ceea0f3b675bac03425472692273cf52" 15 | req, err := http.NewRequest("GET", urs, nil) 16 | require.NoError(t, err) 17 | res, err := client.Request(context.TODO(), req) 18 | require.NoError(t, err) 19 | 20 | out, brand := encoding.FindSaltpack(string(res), true) 21 | out = encoding.TrimSaltpack(out, nil) 22 | require.Equal(t, "kdZaJI1U5AS7G6iVoUxdP8OtPzEoM6pYhVl0YQZJnotVEwLg9BDb5SUO05pmabUSeCvBfdPoRpPJ8wrcF5PP3wTCKq6Xr2MZHgg6m2QalgJCD6vMqlBQfIg6QsfB27aP5DMuXlJAUVIAvMDHIoptmSriNMzfpwBjRShVLWH70a0GOEqD6L8bkC5EFOwCedvHFpcAQVqULHjcSpeCfZEIOaQ2IP", out) 23 | require.Equal(t, "", brand) 24 | } 25 | -------------------------------------------------------------------------------- /http/mem.go: -------------------------------------------------------------------------------- 1 | package http 2 | 3 | import ( 4 | "context" 5 | "strconv" 6 | "sync" 7 | "time" 8 | 9 | "github.com/keys-pub/keys/tsutil" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // Mem is a in memory key value store. 14 | type Mem struct { 15 | sync.Mutex 16 | kv map[string]*entry 17 | clock tsutil.Clock 18 | } 19 | 20 | // NonceCheck ... 21 | func (m *Mem) NonceCheck(ctx context.Context, nonce string) error { 22 | val, err := m.Get(ctx, nonce) 23 | if err != nil { 24 | return err 25 | } 26 | if val != "" { 27 | return errors.Errorf("nonce collision") 28 | } 29 | if err := m.Set(ctx, nonce, "1"); err != nil { 30 | return err 31 | } 32 | if err := m.Expire(ctx, nonce, time.Hour); err != nil { 33 | return err 34 | } 35 | return nil 36 | } 37 | 38 | // NewMem creates a Mem key value store. 39 | func NewMem(clock tsutil.Clock) *Mem { 40 | return &Mem{ 41 | kv: map[string]*entry{}, 42 | clock: clock, 43 | } 44 | } 45 | 46 | type entry struct { 47 | Value string 48 | Expire time.Time 49 | } 50 | 51 | // Get ... 52 | func (m *Mem) Get(ctx context.Context, k string) (string, error) { 53 | m.Lock() 54 | defer m.Unlock() 55 | e, err := m.get(ctx, k) 56 | if err != nil { 57 | return "", err 58 | } 59 | if e == nil { 60 | return "", nil 61 | } 62 | return e.Value, nil 63 | } 64 | 65 | func (m *Mem) get(ctx context.Context, k string) (*entry, error) { 66 | e, ok := m.kv[k] 67 | if !ok { 68 | return nil, nil 69 | } 70 | if e.Expire.IsZero() { 71 | return e, nil 72 | } 73 | now := m.clock.Now() 74 | if e.Expire.Equal(now) || now.After(e.Expire) { 75 | return nil, nil 76 | } 77 | return e, nil 78 | } 79 | 80 | // Expire ... 81 | func (m *Mem) Expire(ctx context.Context, k string, dt time.Duration) error { 82 | m.Lock() 83 | defer m.Unlock() 84 | t := m.clock.Now() 85 | t = t.Add(dt) 86 | e, err := m.get(ctx, k) 87 | if err != nil { 88 | return err 89 | } 90 | e.Expire = t 91 | return m.set(ctx, k, e) 92 | } 93 | 94 | // Delete .. 95 | func (m *Mem) Delete(ctx context.Context, k string) error { 96 | m.Lock() 97 | defer m.Unlock() 98 | delete(m.kv, k) 99 | return nil 100 | } 101 | 102 | // Set ... 103 | func (m *Mem) Set(ctx context.Context, k string, v string) error { 104 | m.Lock() 105 | defer m.Unlock() 106 | return m.set(ctx, k, &entry{Value: v}) 107 | } 108 | 109 | func (m *Mem) set(ctx context.Context, k string, e *entry) error { 110 | m.kv[k] = e 111 | return nil 112 | } 113 | 114 | // Increment ... 115 | func (m *Mem) Increment(ctx context.Context, k string) (int64, error) { 116 | m.Lock() 117 | defer m.Unlock() 118 | e, err := m.get(ctx, k) 119 | if err != nil { 120 | return 0, err 121 | } 122 | n, err := strconv.ParseInt(e.Value, 10, 64) 123 | if err != nil { 124 | return 0, err 125 | } 126 | n++ 127 | inc := strconv.FormatInt(n, 10) 128 | e.Value = inc 129 | return n, m.set(ctx, k, e) 130 | } 131 | -------------------------------------------------------------------------------- /http/nonces_test.go: -------------------------------------------------------------------------------- 1 | package http_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/keys-pub/keys" 9 | "github.com/keys-pub/keys/encoding" 10 | "github.com/keys-pub/keys/http" 11 | "github.com/keys-pub/keys/tsutil" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestNonces(t *testing.T) { 16 | nonces := http.NewMem(tsutil.NewTestClock()) 17 | 18 | n1 := encoding.MustEncode(keys.RandBytes(32), encoding.Base62) 19 | val, err := nonces.Get(context.TODO(), n1) 20 | require.NoError(t, err) 21 | require.Empty(t, val) 22 | 23 | err = nonces.Set(context.TODO(), n1, "1") 24 | require.NoError(t, err) 25 | 26 | val, err = nonces.Get(context.TODO(), n1) 27 | require.NoError(t, err) 28 | require.Equal(t, "1", val) 29 | } 30 | 31 | func TestNoncesExpiration(t *testing.T) { 32 | nonces := http.NewMem(tsutil.NewTestClock()) 33 | 34 | n1 := encoding.MustEncode(keys.RandBytes(32), encoding.Base62) 35 | val, err := nonces.Get(context.TODO(), n1) 36 | require.NoError(t, err) 37 | require.Empty(t, val) 38 | 39 | err = nonces.Set(context.TODO(), n1, "1") 40 | require.NoError(t, err) 41 | err = nonces.Expire(context.TODO(), n1, time.Millisecond) 42 | require.NoError(t, err) 43 | 44 | val2, err := nonces.Get(context.TODO(), n1) 45 | require.NoError(t, err) 46 | require.Empty(t, val2) 47 | 48 | n2 := encoding.MustEncode(keys.RandBytes(32), encoding.Base62) 49 | err = nonces.Set(context.TODO(), n2, "2") 50 | require.NoError(t, err) 51 | err = nonces.Expire(context.TODO(), n2, time.Minute) 52 | require.NoError(t, err) 53 | 54 | val3, err := nonces.Get(context.TODO(), n2) 55 | require.NoError(t, err) 56 | require.Equal(t, "2", val3) 57 | } 58 | 59 | // func TestNoncesIncrement(t *testing.T) { 60 | // var err error 61 | // nonces := http.NewMem(tsutil.NewTestClock()) 62 | 63 | // n1 := encoding.MustEncode(keys.RandBytes(32), encoding.Base62) 64 | 65 | // err = nonces.Set(context.TODO(), n1, "1") 66 | // require.NoError(t, err) 67 | 68 | // val, err := nonces.Get(context.TODO(), n1) 69 | // require.NoError(t, err) 70 | // require.Equal(t, "1", val) 71 | 72 | // n, err := nonces.Increment(context.TODO(), n1) 73 | // require.NoError(t, err) 74 | // require.Equal(t, int64(2), n) 75 | 76 | // val, err = nonces.Get(context.TODO(), n1) 77 | // require.NoError(t, err) 78 | // require.Equal(t, "2", val) 79 | // } 80 | -------------------------------------------------------------------------------- /id_test.go: -------------------------------------------------------------------------------- 1 | package keys_test 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/keys-pub/keys" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestID(t *testing.T) { 12 | sk := keys.NewEdX25519KeyFromSeed(testSeed(0x01)) 13 | sid := keys.ID("kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077") 14 | require.Equal(t, sid, sk.ID()) 15 | require.Equal(t, sk.Public(), sid.Public()) 16 | require.Equal(t, keys.KeyType("edx25519"), sid.Type()) 17 | 18 | bk := keys.NewX25519KeyFromSeed(testSeed(0x01)) 19 | bid := keys.ID("kbx15nsf9y4k28p83wth93tf7hafhvfajp45d2mge80ems45gz0c5gys57cytk") 20 | require.Equal(t, bid, bk.ID()) 21 | require.Equal(t, bk.Public(), bid.Public()) 22 | require.Equal(t, keys.KeyType("x25519"), bid.Type()) 23 | } 24 | 25 | func TestIDErrors(t *testing.T) { 26 | var err error 27 | 28 | _, err = keys.ParseID("") 29 | require.EqualError(t, err, "failed to parse id: empty string") 30 | 31 | _, err = keys.ParseID("???") 32 | require.EqualError(t, err, "failed to parse id: separator '1' at invalid position: pos=-1, len=3") 33 | } 34 | 35 | func TestIDUUID(t *testing.T) { 36 | id := keys.ID("kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077") 37 | require.Equal(t, "34750f98bd59fcfc946da45aaabe933b", hex.EncodeToString(id.UUID()[:])) 38 | } 39 | 40 | func TestIDSet(t *testing.T) { 41 | s := keys.NewIDSet(keys.ID("a"), keys.ID("b"), keys.ID("c")) 42 | require.True(t, s.Contains(keys.ID("a"))) 43 | require.False(t, s.Contains(keys.ID("z"))) 44 | s.Add("z") 45 | require.True(t, s.Contains(keys.ID("z"))) 46 | s.Add("z") 47 | require.Equal(t, 4, s.Size()) 48 | s.AddAll([]keys.ID{"m", "n"}) 49 | 50 | expected := []keys.ID{keys.ID("a"), keys.ID("b"), keys.ID("c"), keys.ID("z"), keys.ID("m"), keys.ID("n")} 51 | require.Equal(t, expected, s.IDs()) 52 | 53 | s.Clear() 54 | require.False(t, s.Contains(keys.ID("a"))) 55 | require.False(t, s.Contains(keys.ID("z"))) 56 | require.Equal(t, 0, s.Size()) 57 | } 58 | -------------------------------------------------------------------------------- /json/marshal.go: -------------------------------------------------------------------------------- 1 | // Package json provides a simpler JSON marshaller for strings and ints only. 2 | package json 3 | 4 | import ( 5 | "bytes" 6 | "encoding/json" 7 | "regexp" 8 | "strconv" 9 | 10 | "github.com/keys-pub/keys/encoding" 11 | 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | type stringEntry struct { 16 | key string 17 | value string 18 | } 19 | 20 | type intEntry struct { 21 | key string 22 | value int 23 | } 24 | 25 | // String ... 26 | func String(key string, value string) encoding.TextMarshaler { 27 | return stringEntry{key: key, value: value} 28 | } 29 | 30 | // Int ... 31 | func Int(key string, value int) encoding.TextMarshaler { 32 | return intEntry{key: key, value: value} 33 | } 34 | 35 | var isAlphaNumericDot = regexp.MustCompile(`^[a-zA-Z0-9.]+$`).MatchString 36 | var needsEscape = regexp.MustCompile(`^[\"]+$`).MatchString 37 | 38 | func (e stringEntry) MarshalText() ([]byte, error) { 39 | if !isAlphaNumericDot(e.key) { 40 | return nil, errors.Errorf("invalid character in key") 41 | } 42 | if needsEscape(e.value) { 43 | return nil, errors.Errorf("invalid character in value") 44 | } 45 | if !encoding.IsASCII([]byte(e.value)) { 46 | return nil, errors.Errorf("invalid character in value") 47 | } 48 | return []byte(`"` + e.key + `":"` + e.value + `"`), nil 49 | } 50 | 51 | func (e intEntry) MarshalText() ([]byte, error) { 52 | if !isAlphaNumericDot(e.key) { 53 | return nil, errors.Errorf("invalid character in key") 54 | } 55 | 56 | b := []byte{} 57 | b = append(b, '"') 58 | b = append(b, []byte(e.key)...) 59 | b = append(b, '"', ':') 60 | b = append(b, []byte(strconv.Itoa(e.value))...) 61 | 62 | return b, nil 63 | } 64 | 65 | // Marshal values. 66 | func Marshal(vals ...encoding.TextMarshaler) ([]byte, error) { 67 | out := make([][]byte, 0, len(vals)) 68 | for _, val := range vals { 69 | b, err := val.MarshalText() 70 | if err != nil { 71 | return nil, err 72 | } 73 | out = append(out, b) 74 | } 75 | 76 | b := []byte{} 77 | b = append(b, '{') 78 | b = append(b, bytes.Join(out, []byte{','})...) 79 | b = append(b, '}') 80 | 81 | return b, nil 82 | } 83 | 84 | // Unmarshal bytes. 85 | func Unmarshal(data []byte, v interface{}) error { 86 | return json.Unmarshal(data, v) 87 | } 88 | -------------------------------------------------------------------------------- /json/marshal_test.go: -------------------------------------------------------------------------------- 1 | package json_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keys-pub/keys/json" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestMarshal(t *testing.T) { 11 | b, err := json.Marshal( 12 | json.String("key1", "val1"), 13 | json.Int("key2", 2), 14 | ) 15 | require.NoError(t, err) 16 | require.Equal(t, `{"key1":"val1","key2":2}`, string(b)) 17 | 18 | _, err = json.Marshal( 19 | json.String(`"`, ""), 20 | ) 21 | require.EqualError(t, err, "invalid character in key") 22 | _, err = json.Marshal( 23 | json.String("key1", `"`), 24 | ) 25 | require.EqualError(t, err, "invalid character in value") 26 | } 27 | -------------------------------------------------------------------------------- /key.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | // Key with id, type and private and/or public data. 4 | type Key interface { 5 | // ID for the key. 6 | ID() ID 7 | 8 | // Type of key. 9 | Type() KeyType 10 | 11 | // Private key data. 12 | Private() []byte 13 | 14 | // Public key data. 15 | Public() []byte 16 | } 17 | 18 | // KeyType ... 19 | type KeyType string 20 | 21 | var _ Key = &EdX25519Key{} 22 | var _ Key = &EdX25519PublicKey{} 23 | 24 | var _ Key = &X25519Key{} 25 | var _ Key = &X25519PublicKey{} 26 | 27 | var _ Key = &RSAKey{} 28 | var _ Key = &RSAPublicKey{} 29 | 30 | var _ Key = ID("") 31 | -------------------------------------------------------------------------------- /key_test.go: -------------------------------------------------------------------------------- 1 | package keys_test 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/keys-pub/keys" 7 | ) 8 | 9 | func testSeed(b byte) *[32]byte { 10 | return keys.Bytes32(bytes.Repeat([]byte{b}, 32)) 11 | } 12 | -------------------------------------------------------------------------------- /keyring/README.md: -------------------------------------------------------------------------------- 1 | # Keyring 2 | 3 | This package provides a cross platform keyring using system APIs (macOS/keychain, 4 | Windows/wincred, Linux/libsecret). 5 | 6 | For more details visit **[keys.pub](https://keys.pub)**. 7 | 8 | ## macOS 9 | 10 | The Keychain API via the [github.com/keybase/go-keychain](https://github.com/keybase/go-keychain) package. 11 | 12 | ## Windows 13 | 14 | The Windows Credential Manager API via the [github.com/danieljoos/wincred](https://github.com/danieljoos/wincred) package. 15 | 16 | ## Linux 17 | 18 | The SecretService dbus interface via the [github.com/zalando/go-keyring](github.com/zalando/go-keyring) 19 | package. The SecretService dbus interface, which is provided by GNOME Keyring. 20 | 21 | We are still exploring whether to use kwallet or libsecret directly for linux environments that support that instead. 22 | In the meantime, you can fall back to the FS based keyring. 23 | 24 | ## FS 25 | 26 | There is a filesystem based keyring. 27 | 28 | ## Mem 29 | 30 | The is an in memory keyring for ephemeral keys or for testing. 31 | -------------------------------------------------------------------------------- /keyring/backup.go: -------------------------------------------------------------------------------- 1 | package keyring 2 | 3 | import ( 4 | "archive/tar" 5 | "bytes" 6 | "compress/gzip" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "time" 11 | 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | // Backup into {path}.tgz. 16 | func Backup(path string, kr Keyring, now time.Time) error { 17 | tmpPath := path + ".tmp" 18 | defer func() { _ = os.Remove(tmpPath) }() 19 | 20 | file, err := os.Create(tmpPath) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | if err := backup(file, kr, now); err != nil { 26 | _ = file.Close() 27 | return err 28 | } 29 | 30 | if err := file.Close(); err != nil { 31 | return err 32 | } 33 | 34 | if err := os.Rename(tmpPath, path); err != nil { 35 | return err 36 | } 37 | 38 | return nil 39 | } 40 | 41 | func backup(file *os.File, kr Keyring, now time.Time) error { 42 | gz := gzip.NewWriter(file) 43 | tw := tar.NewWriter(gz) 44 | 45 | items, err := kr.Items("") 46 | if err != nil { 47 | _ = tw.Close() 48 | _ = gz.Close() 49 | return err 50 | } 51 | for _, item := range items { 52 | id := item.ID 53 | b := item.Data 54 | header := new(tar.Header) 55 | header.Name = id 56 | header.Size = int64(len(b)) 57 | header.Mode = 0600 58 | header.ModTime = now 59 | if err := tw.WriteHeader(header); err != nil { 60 | _ = tw.Close() 61 | _ = gz.Close() 62 | return err 63 | } 64 | if _, err := io.Copy(tw, bytes.NewReader(item.Data)); err != nil { 65 | _ = tw.Close() 66 | _ = gz.Close() 67 | return err 68 | } 69 | } 70 | 71 | if err := tw.Close(); err != nil { 72 | return err 73 | } 74 | if err := gz.Close(); err != nil { 75 | return err 76 | } 77 | 78 | return nil 79 | } 80 | 81 | // Restore from path.tgz. 82 | func Restore(path string, kr Keyring) error { 83 | file, err := os.OpenFile(path, os.O_RDONLY, 0) // #nosec 84 | if err != nil { 85 | return errors.Wrapf(err, "failed to open backup") 86 | } 87 | 88 | if err := restore(file, kr); err != nil { 89 | _ = file.Close() 90 | return err 91 | } 92 | 93 | return file.Close() 94 | 95 | } 96 | 97 | func restore(file *os.File, kr Keyring) error { 98 | gz, err := gzip.NewReader(file) 99 | if err != nil { 100 | return errors.Wrapf(err, "failed to open gzip") 101 | } 102 | 103 | tr := tar.NewReader(gz) 104 | for { 105 | header, err := tr.Next() 106 | if err == io.EOF { 107 | break 108 | } 109 | if err != nil { 110 | return errors.Wrapf(err, "failed to read next tar") 111 | } 112 | 113 | switch header.Typeflag { 114 | case tar.TypeDir: 115 | continue 116 | case tar.TypeReg: 117 | b, err := ioutil.ReadAll(tr) 118 | if err != nil { 119 | return errors.Wrapf(err, "failed to read tar") 120 | } 121 | 122 | path := header.Name 123 | if err := kr.Set(path, b); err != nil { 124 | return err 125 | } 126 | 127 | default: 128 | return errors.Errorf("invalid tar flag") 129 | } 130 | } 131 | 132 | return nil 133 | } 134 | -------------------------------------------------------------------------------- /keyring/backup_test.go: -------------------------------------------------------------------------------- 1 | package keyring_test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/keys-pub/keys" 9 | "github.com/keys-pub/keys/dstore" 10 | "github.com/keys-pub/keys/keyring" 11 | "github.com/keys-pub/keys/tsutil" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestBackupRestore(t *testing.T) { 16 | var err error 17 | clock := tsutil.NewTestClock() 18 | 19 | kr := keyring.NewMem() 20 | for i := 0; i < 10; i++ { 21 | err := kr.Set(dstore.Path("item", i), []byte(fmt.Sprintf("value%d", i))) 22 | require.NoError(t, err) 23 | } 24 | 25 | tmpFile := keys.RandTempPath() + ".tgz" 26 | defer func() { _ = os.Remove(tmpFile) }() 27 | 28 | err = keyring.Backup(tmpFile, kr, clock.Now()) 29 | require.NoError(t, err) 30 | 31 | kr2 := keyring.NewMem() 32 | err = keyring.Restore(tmpFile, kr2) 33 | require.NoError(t, err) 34 | testEqualKeyrings(t, kr, kr2) 35 | } 36 | 37 | func testEqualKeyrings(t *testing.T, kr1 keyring.Keyring, kr2 keyring.Keyring) { 38 | items1, err := kr1.Items("") 39 | require.NoError(t, err) 40 | items2, err := kr2.Items("") 41 | require.NoError(t, err) 42 | 43 | require.Equal(t, len(items1), len(items2)) 44 | 45 | for i := 0; i < len(items1); i++ { 46 | b1, err := kr1.Get(items1[i].ID) 47 | require.NoError(t, err) 48 | b2, err := kr2.Get(items2[i].ID) 49 | require.NoError(t, err) 50 | require.Equal(t, b1, b2) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /keyring/fs_test.go: -------------------------------------------------------------------------------- 1 | package keyring_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "testing" 7 | 8 | "github.com/keys-pub/keys/keyring" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func testFS(t *testing.T) (keyring.Keyring, func()) { 13 | dir, err := ioutil.TempDir("", "KeysTest.") 14 | require.NoError(t, err) 15 | fs, err := keyring.NewFS(dir) 16 | require.NoError(t, err) 17 | closeFn := func() { 18 | os.RemoveAll(dir) 19 | } 20 | return fs, closeFn 21 | } 22 | 23 | func TestFS(t *testing.T) { 24 | st, closeFn := testFS(t) 25 | defer closeFn() 26 | testKeyring(t, st) 27 | 28 | _, err := st.Get(".") 29 | require.EqualError(t, err, "invalid id .") 30 | _, err = st.Get("..") 31 | require.EqualError(t, err, "invalid id ..") 32 | } 33 | 34 | func TestFSReset(t *testing.T) { 35 | st, closeFn := testFS(t) 36 | defer closeFn() 37 | testReset(t, st) 38 | } 39 | 40 | func TestFSDocuments(t *testing.T) { 41 | st, closeFn := testFS(t) 42 | defer closeFn() 43 | testDocuments(t, st) 44 | } 45 | -------------------------------------------------------------------------------- /keyring/keyring.go: -------------------------------------------------------------------------------- 1 | // Package keyring provides a cross-platform secure keyring. 2 | package keyring 3 | 4 | import ( 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | // NewSystem creates system keyring. 9 | func NewSystem(service string) (Keyring, error) { 10 | if service == "" { 11 | return nil, errors.Errorf("invalid service") 12 | } 13 | return newSystem(service), nil 14 | } 15 | 16 | // Item .. 17 | type Item struct { 18 | ID string 19 | Data []byte 20 | } 21 | 22 | // Keyring is the interface used to store data. 23 | type Keyring interface { 24 | // Name of the keyring implementation. 25 | Name() string 26 | 27 | // Get bytes. 28 | Get(id string) ([]byte, error) 29 | // Set bytes. 30 | Set(id string, data []byte) error 31 | // Delete bytes. 32 | Delete(id string) (bool, error) 33 | 34 | // Exists returns true if exists. 35 | Exists(id string) (bool, error) 36 | 37 | // Reset removes all data. 38 | Reset() error 39 | 40 | Items(prefix string) ([]*Item, error) 41 | } 42 | 43 | // IDs from Keyring. 44 | func IDs(kr Keyring, prefix string) ([]string, error) { 45 | items, err := kr.Items(prefix) 46 | if err != nil { 47 | return nil, err 48 | } 49 | paths := []string{} 50 | for _, item := range items { 51 | paths = append(paths, item.ID) 52 | } 53 | return paths, nil 54 | } 55 | 56 | var _ = reset 57 | 58 | func reset(kr Keyring) error { 59 | ids, err := IDs(kr, "") 60 | if err != nil { 61 | return err 62 | } 63 | for _, id := range ids { 64 | if _, err := kr.Delete(id); err != nil { 65 | return err 66 | } 67 | } 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /keyring/keyring_windows.go: -------------------------------------------------------------------------------- 1 | package keyring 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | 7 | "github.com/danieljoos/wincred" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | func newSystem(service string) Keyring { 12 | return sys{ 13 | service: service, 14 | } 15 | } 16 | 17 | // CheckSystem returns error if wincred is not available. 18 | func CheckSystem() error { 19 | return nil 20 | } 21 | 22 | type sys struct { 23 | service string 24 | } 25 | 26 | func (k sys) Name() string { 27 | return "wincred" 28 | } 29 | 30 | func (k sys) Get(id string) ([]byte, error) { 31 | targetName := k.service + "/" + id 32 | cred, err := wincred.GetGenericCredential(targetName) 33 | if err != nil { 34 | if errors.Cause(err) == wincred.ErrElementNotFound { 35 | return nil, nil 36 | } 37 | return nil, errors.Wrapf(err, "wincred GetGenericCredential failed") 38 | } 39 | if cred == nil { 40 | return nil, nil 41 | } 42 | return cred.CredentialBlob, nil 43 | } 44 | 45 | func (k sys) Set(id string, data []byte) error { 46 | targetName := k.service + "/" + id 47 | cred := wincred.NewGenericCredential(targetName) 48 | cred.CredentialBlob = data 49 | if err := cred.Write(); err != nil { 50 | return errors.Wrapf(err, "wincred Write failed") 51 | } 52 | return nil 53 | } 54 | 55 | func (k sys) Delete(id string) (bool, error) { 56 | targetName := k.service + "/" + id 57 | cred, err := wincred.GetGenericCredential(targetName) 58 | if err != nil { 59 | if errors.Cause(err) == wincred.ErrElementNotFound { 60 | return false, nil 61 | } 62 | return false, errors.Wrapf(err, "wincred GetGenericCredential failed") 63 | } 64 | if cred == nil { 65 | return false, nil 66 | } 67 | if err := cred.Delete(); err != nil { 68 | return false, errors.Wrapf(err, "wincred Delete failed") 69 | } 70 | return true, nil 71 | } 72 | 73 | func (k sys) Exists(id string) (bool, error) { 74 | targetName := k.service + "/" + id 75 | cred, err := wincred.GetGenericCredential(targetName) 76 | if err != nil { 77 | if errors.Cause(err) == wincred.ErrElementNotFound { 78 | return false, nil 79 | } 80 | return false, errors.Wrapf(err, "wincred GetGenericCredential failed") 81 | } 82 | if cred == nil { 83 | return false, nil 84 | } 85 | return true, nil 86 | } 87 | 88 | func (k sys) Items(prefix string) ([]*Item, error) { 89 | creds, err := wincred.List() 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | out := make([]*Item, 0, len(creds)) 95 | for _, cred := range creds { 96 | if strings.HasPrefix(cred.TargetName, k.service+"/") { 97 | id := cred.TargetName[len(k.service+"/"):] 98 | if prefix != "" && !strings.HasPrefix(id, prefix) { 99 | continue 100 | } 101 | item := &Item{ID: id} 102 | b, err := k.Get(id) 103 | if err != nil { 104 | return nil, err 105 | } 106 | item.Data = b 107 | out = append(out, item) 108 | } 109 | } 110 | 111 | sort.Slice(out, func(i, j int) bool { 112 | return out[i].ID < out[j].ID 113 | }) 114 | return out, nil 115 | } 116 | 117 | func (k sys) Reset() error { 118 | return reset(k) 119 | } 120 | -------------------------------------------------------------------------------- /keyring/mem.go: -------------------------------------------------------------------------------- 1 | package keyring 2 | 3 | import ( 4 | "sort" 5 | "strings" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // NewMem returns an in memory keyring useful for testing or ephemeral keys. 11 | func NewMem() Keyring { 12 | return &mem{ 13 | items: map[string][]byte{}, 14 | } 15 | } 16 | 17 | type mem struct { 18 | items map[string][]byte 19 | } 20 | 21 | func (k *mem) Name() string { 22 | return "mem" 23 | } 24 | 25 | func (k *mem) Get(id string) ([]byte, error) { 26 | if id == "" { 27 | return nil, errors.Errorf("invalid id") 28 | } 29 | if b, ok := k.items[id]; ok { 30 | return b, nil 31 | } 32 | return nil, nil 33 | } 34 | 35 | func (k *mem) Set(id string, data []byte) error { 36 | if id == "" { 37 | return errors.Errorf("invalid id") 38 | } 39 | k.items[id] = data 40 | return nil 41 | } 42 | 43 | func (k *mem) Reset() error { 44 | k.items = map[string][]byte{} 45 | return nil 46 | } 47 | 48 | func (k *mem) Exists(id string) (bool, error) { 49 | if id == "" { 50 | return false, errors.Errorf("invalid id") 51 | } 52 | _, ok := k.items[id] 53 | return ok, nil 54 | } 55 | 56 | func (k *mem) Delete(id string) (bool, error) { 57 | if id == "" { 58 | return false, errors.Errorf("invalid id") 59 | } 60 | if _, ok := k.items[id]; ok { 61 | delete(k.items, id) 62 | return true, nil 63 | } 64 | return false, nil 65 | } 66 | 67 | func (k *mem) Items(prefix string) ([]*Item, error) { 68 | out := make([]*Item, 0, len(k.items)) 69 | for id, b := range k.items { 70 | if strings.HasPrefix(id, prefix) { 71 | item := &Item{ID: id, Data: b} 72 | out = append(out, item) 73 | } 74 | } 75 | sort.Slice(out, func(i, j int) bool { 76 | return out[i].ID < out[j].ID 77 | }) 78 | return out, nil 79 | } 80 | -------------------------------------------------------------------------------- /keyring/mem_test.go: -------------------------------------------------------------------------------- 1 | package keyring_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keys-pub/keys/keyring" 7 | ) 8 | 9 | func TestMemKeyring(t *testing.T) { 10 | testKeyring(t, keyring.NewMem()) 11 | } 12 | 13 | func TestMemReset(t *testing.T) { 14 | testReset(t, keyring.NewMem()) 15 | } 16 | 17 | func TestMemDocuments(t *testing.T) { 18 | testDocuments(t, keyring.NewMem()) 19 | } 20 | -------------------------------------------------------------------------------- /lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e -u -o pipefail # Fail on error 4 | 5 | dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 6 | cd "$dir" 7 | 8 | golangci-lint run --timeout 10m 9 | -------------------------------------------------------------------------------- /log.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import ( 4 | pkglog "log" 5 | ) 6 | 7 | var logger = NewLogger(ErrLevel) 8 | 9 | // SetLogger sets logger for the package. 10 | func SetLogger(l Logger) { 11 | logger = l 12 | } 13 | 14 | // Logger interface used in this package. 15 | type Logger interface { 16 | Debugf(format string, args ...interface{}) 17 | Infof(format string, args ...interface{}) 18 | Warningf(format string, args ...interface{}) 19 | Errorf(format string, args ...interface{}) 20 | Fatalf(format string, args ...interface{}) 21 | } 22 | 23 | // LogLevel ... 24 | type LogLevel int 25 | 26 | const ( 27 | // DebugLevel ... 28 | DebugLevel LogLevel = 3 29 | // InfoLevel ... 30 | InfoLevel LogLevel = 2 31 | // WarnLevel ... 32 | WarnLevel LogLevel = 1 33 | // ErrLevel ... 34 | ErrLevel LogLevel = 0 35 | ) 36 | 37 | // NewLogger ... 38 | func NewLogger(lev LogLevel) Logger { 39 | return &defaultLog{Level: lev} 40 | } 41 | 42 | func (l LogLevel) String() string { 43 | switch l { 44 | case DebugLevel: 45 | return "debug" 46 | case InfoLevel: 47 | return "info" 48 | case WarnLevel: 49 | return "warn" 50 | case ErrLevel: 51 | return "err" 52 | default: 53 | return "" 54 | } 55 | } 56 | 57 | type defaultLog struct { 58 | Level LogLevel 59 | } 60 | 61 | func (l defaultLog) Debugf(format string, args ...interface{}) { 62 | if l.Level >= 3 { 63 | pkglog.Printf("[DEBG] "+format+"\n", args...) 64 | } 65 | } 66 | 67 | func (l defaultLog) Infof(format string, args ...interface{}) { 68 | if l.Level >= 2 { 69 | pkglog.Printf("[INFO] "+format+"\n", args...) 70 | } 71 | } 72 | 73 | func (l defaultLog) Warningf(format string, args ...interface{}) { 74 | if l.Level >= 1 { 75 | pkglog.Printf("[WARN] "+format+"\n", args...) 76 | } 77 | } 78 | 79 | func (l defaultLog) Errorf(format string, args ...interface{}) { 80 | if l.Level >= 0 { 81 | pkglog.Printf("[ERR] "+format+"\n", args...) 82 | } 83 | } 84 | 85 | func (l defaultLog) Fatalf(format string, args ...interface{}) { 86 | pkglog.Fatalf(format, args...) 87 | } 88 | -------------------------------------------------------------------------------- /noise/README.md: -------------------------------------------------------------------------------- 1 | # Noise 2 | 3 | The [noise package](https://github.com/keys-pub/keys/blob/master/noise) helps setup a Noise handshake using X25519 keys. 4 | 5 | The default cipher suite used is: 6 | Curve25519 ECDH, ChaCha20-Poly1305 AEAD, BLAKE2b hash. 7 | 8 | The handshake uses the KK pattern: 9 | 10 | - K = Static key for initiator Known to responder 11 | - K = Static key for responder Known to initiator 12 | 13 | One of the Noise participants should be the initiator. 14 | 15 | The order of the handshake writes/reads should be: 16 | 17 | - (1) Initiator: Write 18 | - (2) Responder: Read 19 | - (3) Initiator: Read 20 | - (4) Responder: Write 21 | 22 | When the handshake is complete, use the Cipher to Encrypt/Decrypt. 23 | 24 | See [noiseprotocol.org](http://www.noiseprotocol.org) for more info. 25 | 26 | ## Examples 27 | 28 | - [Handshake + Encrypt/Decrypt](https://github.com/keys-pub/keys/blob/master/noise/example_test.go) 29 | -------------------------------------------------------------------------------- /noise/cipher.go: -------------------------------------------------------------------------------- 1 | package noise 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | ) 6 | 7 | type cipherState struct { 8 | *Handshake 9 | } 10 | 11 | func newCipherState(handshake *Handshake) cipherState { 12 | return cipherState{ 13 | handshake, 14 | } 15 | } 16 | 17 | // Encrypt to out. 18 | func (n cipherState) Encrypt(out, ad, plaintext []byte) ([]byte, error) { 19 | if n.initiator { 20 | if n.csI0 == nil { 21 | return nil, errors.Errorf("no cipher for encrypt (I)") 22 | } 23 | return n.csI0.Encrypt(out, ad, plaintext) 24 | } 25 | if n.csR1 == nil { 26 | return nil, errors.Errorf("no cipher for encrypt (R)") 27 | } 28 | return n.csR1.Encrypt(out, ad, plaintext) 29 | } 30 | 31 | // Decrypt to out. 32 | func (n cipherState) Decrypt(out, ad, ciphertext []byte) ([]byte, error) { 33 | if n.initiator { 34 | if n.csI1 == nil { 35 | return nil, errors.Errorf("no cipher for decrypt (I)") 36 | } 37 | return n.csI1.Decrypt(out, ad, ciphertext) 38 | } 39 | if n.csR0 == nil { 40 | return nil, errors.Errorf("no cipher for decrypt (R)") 41 | } 42 | return n.csR0.Decrypt(out, ad, ciphertext) 43 | } 44 | -------------------------------------------------------------------------------- /noise/example_test.go: -------------------------------------------------------------------------------- 1 | package noise_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/keys-pub/keys" 8 | "github.com/keys-pub/keys/noise" 9 | ) 10 | 11 | func ExampleNewHandshake() { 12 | alice := keys.GenerateX25519Key() 13 | bob := keys.GenerateX25519Key() 14 | 15 | na, err := noise.NewHandshake(alice, bob.PublicKey(), true) 16 | if err != nil { 17 | log.Fatal(err) 18 | } 19 | 20 | nb, err := noise.NewHandshake(bob, alice.PublicKey(), false) 21 | if err != nil { 22 | log.Fatal(err) 23 | } 24 | 25 | // -> s 26 | // <- s 27 | ha, err := na.Write(nil) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | if _, err := nb.Read(ha); err != nil { 32 | log.Fatal(err) 33 | } 34 | // -> e, es, ss 35 | // <- e, ee, se 36 | hb, err := nb.Write(nil) 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | if _, err := na.Read(hb); err != nil { 41 | log.Fatal(err) 42 | } 43 | 44 | // transport I -> R 45 | ca, err := na.Cipher() 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | encrypted, err := ca.Encrypt(nil, nil, []byte("hello")) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | cb, err := nb.Cipher() 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | decrypted, err := cb.Decrypt(nil, nil, encrypted) 59 | if err != nil { 60 | log.Fatal(err) 61 | } 62 | 63 | fmt.Printf("%s", string(decrypted)) 64 | // Output: hello 65 | } 66 | -------------------------------------------------------------------------------- /noise/noise_test.go: -------------------------------------------------------------------------------- 1 | package noise_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keys-pub/keys" 7 | "github.com/keys-pub/keys/noise" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestNewHandshake(t *testing.T) { 12 | alice := keys.GenerateX25519Key() 13 | bob := keys.GenerateX25519Key() 14 | 15 | na, err := noise.NewHandshake(alice, bob.PublicKey(), true) 16 | require.NoError(t, err) 17 | 18 | nb, err := noise.NewHandshake(bob, alice.PublicKey(), false) 19 | require.NoError(t, err) 20 | 21 | // -> s 22 | // <- s 23 | b, err := na.Write([]byte("abcdef")) 24 | require.NoError(t, err) 25 | hb1, err := nb.Read(b) 26 | require.NoError(t, err) 27 | require.Equal(t, "abcdef", string(hb1)) 28 | 29 | require.False(t, na.Complete()) 30 | require.False(t, nb.Complete()) 31 | 32 | // -> e, es, ss 33 | // <- e, ee, se 34 | b, err = nb.Write(nil) 35 | require.NoError(t, err) 36 | hb2, err := na.Read(b) 37 | require.NoError(t, err) 38 | require.Equal(t, "", string(hb2)) 39 | 40 | require.True(t, na.Complete()) 41 | require.True(t, nb.Complete()) 42 | 43 | ca, err := na.Cipher() 44 | require.NoError(t, err) 45 | cb, err := nb.Cipher() 46 | require.NoError(t, err) 47 | 48 | // transport I -> R 49 | encrypted, err := ca.Encrypt(nil, nil, []byte("hello")) 50 | require.NoError(t, err) 51 | decrypted, err := cb.Decrypt(nil, nil, encrypted) 52 | require.NoError(t, err) 53 | require.Equal(t, "hello", string(decrypted)) 54 | 55 | // transport R -> I 56 | encrypted, err = cb.Encrypt(nil, nil, []byte("what time is the meeting?")) 57 | require.NoError(t, err) 58 | decrypted, err = ca.Decrypt(nil, nil, encrypted) 59 | require.NoError(t, err) 60 | require.Equal(t, "what time is the meeting?", string(decrypted)) 61 | } 62 | -------------------------------------------------------------------------------- /package_test.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | var PrivSecretBoxSeal = secretBoxSeal 4 | var PrivSecretBoxOpen = secretBoxOpen 5 | -------------------------------------------------------------------------------- /password_test.go: -------------------------------------------------------------------------------- 1 | package keys_test 2 | 3 | import ( 4 | "log" 5 | "testing" 6 | 7 | "github.com/keys-pub/keys" 8 | "github.com/keys-pub/keys/dstore" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestRandPassword(t *testing.T) { 13 | var pass string 14 | 15 | pass = keys.RandPassword(1) 16 | require.Equal(t, 1, len(pass)) 17 | 18 | pass = keys.RandPassword(16) 19 | require.Equal(t, 16, len(pass)) 20 | 21 | set := dstore.NewStringSet() 22 | for i := 0; i < 1000; i++ { 23 | check := keys.RandPassword(8) 24 | require.Equal(t, 8, len(check)) 25 | require.False(t, set.Contains(check)) 26 | set.Add(check) 27 | } 28 | 29 | pass = keys.RandPassword(128) 30 | require.Equal(t, 128, len(pass)) 31 | 32 | pass = keys.RandPassword(4096) 33 | require.Equal(t, 4096, len(pass)) 34 | } 35 | 36 | func ExampleRandPassword() { 37 | pw := keys.RandPassword(16) 38 | log.Println(pw) 39 | 40 | pwNoSymbols := keys.RandPassword(16, keys.NoSymbols()) 41 | log.Println(pwNoSymbols) 42 | 43 | // Output: 44 | } 45 | -------------------------------------------------------------------------------- /rand.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/hex" 6 | "math/big" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "github.com/keys-pub/keys/encoding" 12 | ) 13 | 14 | // RandBytes returns random bytes of length. 15 | func RandBytes(length int) []byte { 16 | buf := make([]byte, length) 17 | if _, err := rand.Read(buf); err != nil { 18 | panic(err) 19 | } 20 | return buf 21 | } 22 | 23 | // RandPhrase creates random phrase (BIP39 encoded random 32 bytes). 24 | func RandPhrase() string { 25 | b := RandBytes(32) 26 | phrase, err := encoding.BytesToPhrase(b) 27 | if err != nil { 28 | panic(err) 29 | } 30 | return phrase 31 | } 32 | 33 | // RandWords returns random (BIP39) words. 34 | // numWords must be 1 to 24. 35 | func RandWords(numWords int) string { 36 | if numWords <= 0 || numWords > 24 { 37 | panic("invalid number of words specified") 38 | } 39 | words := strings.Split(RandPhrase(), " ") 40 | return strings.Join(words[:numWords], " ") 41 | } 42 | 43 | // Rand16 generates random 16 bytes. 44 | func Rand16() *[16]byte { 45 | b := RandBytes(16) 46 | var b16 [16]byte 47 | copy(b16[:], b[:16]) 48 | return &b16 49 | } 50 | 51 | // Rand24 generates random 24 bytes. 52 | func Rand24() *[24]byte { 53 | b := RandBytes(24) 54 | var b24 [24]byte 55 | copy(b24[:], b[:24]) 56 | return &b24 57 | } 58 | 59 | // Rand32 generates random 32 bytes. 60 | func Rand32() *[32]byte { 61 | b := RandBytes(32) 62 | var b32 [32]byte 63 | copy(b32[:], b[:32]) 64 | return &b32 65 | } 66 | 67 | // RandUsername returns random lowercase string of length. 68 | func RandUsername(length int) string { 69 | r := []rune{} 70 | for i := 0; i < length; i++ { 71 | rn, err := rand.Int(rand.Reader, big.NewInt(26)) 72 | if err != nil { 73 | panic(err) 74 | } 75 | n := rn.Int64() + 0x61 // 0x61 == "a" 76 | r = append(r, rune(n)) 77 | } 78 | return string(r) 79 | } 80 | 81 | // RandHex returns random hex. 82 | func RandHex(numBytes int) string { 83 | return hex.EncodeToString(RandBytes(numBytes)) 84 | } 85 | 86 | // RandBase62 returns random base62. 87 | func RandBase62(numBytes int) string { 88 | return encoding.MustEncode(RandBytes(numBytes), encoding.Base62) 89 | } 90 | 91 | // RandTempPath returns a unique random file name in os.TempDir. 92 | // RandTempPath() => "/tmp/CTGMMOLLZCXMGP7VR4BHKAI7PE" 93 | func RandTempPath() string { 94 | return filepath.Join(os.TempDir(), RandFileName()) 95 | } 96 | 97 | // RandFileName returns a unique random file name. 98 | // RandFileName() => CTGMMOLLZCXMGP7VR4BHKAI7PE 99 | func RandFileName() string { 100 | return encoding.MustEncode(RandBytes(16), encoding.Base32, encoding.NoPadding()) 101 | } 102 | 103 | // RandDigits returns string of random digits of length. 104 | // RandDigits(6) => "745566" 105 | func RandDigits(length int) string { 106 | charSet := numbers 107 | b := make([]byte, 0, length) 108 | for i := 0; i < length; i++ { 109 | b = append(b, randomChar(charSet, b)) 110 | } 111 | return string(b) 112 | } 113 | -------------------------------------------------------------------------------- /rand_test.go: -------------------------------------------------------------------------------- 1 | package keys_test 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/keys-pub/keys" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestRandID(t *testing.T) { 13 | kid := keys.RandID("test") 14 | require.False(t, strings.Contains(kid.String(), "=")) 15 | } 16 | 17 | func TestRandBytes(t *testing.T) { 18 | b1 := keys.RandBytes(32) 19 | require.Equal(t, 32, len(b1)) 20 | for i := 0; i < 1000; i++ { 21 | b2 := keys.RandBytes(32) 22 | require.False(t, bytes.Equal(b1, b2)) 23 | } 24 | } 25 | 26 | func TestRandWords(t *testing.T) { 27 | p6 := keys.RandWords(6) 28 | require.Equal(t, 6, len(strings.Split(p6, " "))) 29 | require.Panics(t, func() { keys.RandWords(0) }) 30 | 31 | p1 := keys.RandWords(24) 32 | require.Equal(t, 24, len(strings.Split(p1, " "))) 33 | require.Panics(t, func() { keys.RandWords(25) }) 34 | 35 | for i := 0; i < 1000; i++ { 36 | p2 := keys.RandWords(24) 37 | require.NotEqual(t, p1, p2) 38 | } 39 | } 40 | 41 | func TestRandUsername(t *testing.T) { 42 | for i := 0; i < 100; i++ { 43 | u := keys.RandUsername(8) 44 | require.Equal(t, 8, len(u)) 45 | } 46 | } 47 | 48 | func TestRandTempPath(t *testing.T) { 49 | p := keys.RandTempPath() 50 | require.NotEmpty(t, p) 51 | p2 := keys.RandTempPath() 52 | require.NotEqual(t, p, p2) 53 | } 54 | 55 | func TestRandDigits(t *testing.T) { 56 | p := keys.RandDigits(10) 57 | require.NotEmpty(t, p) 58 | t.Logf("%s", p) 59 | p2 := keys.RandDigits(10) 60 | require.NotEqual(t, p, p2) 61 | } 62 | -------------------------------------------------------------------------------- /rsa.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rand" 6 | "crypto/rsa" 7 | "crypto/x509" 8 | ) 9 | 10 | // RSA key type. 11 | const RSA KeyType = "rsa" 12 | const rsaKeyHRP = "rsa" 13 | 14 | // RSAPublicKey is the public part of RSA key pair. 15 | type RSAPublicKey struct { 16 | id ID 17 | pk *rsa.PublicKey 18 | } 19 | 20 | // RSAKey implements Key interface for RSA. 21 | type RSAKey struct { 22 | privateKey *rsa.PrivateKey 23 | publicKey *RSAPublicKey 24 | } 25 | 26 | // NewRSAKeyFromBytes constructs RSA from a private key (PKCS1). 27 | func NewRSAKeyFromBytes(privateKey []byte) (*RSAKey, error) { 28 | k, err := x509.ParsePKCS1PrivateKey(privateKey) 29 | if err != nil { 30 | return nil, err 31 | } 32 | return NewRSAKey(k), nil 33 | } 34 | 35 | func keyIDFromRSA(k *rsa.PublicKey) ID { 36 | // SHA256 of PKCS1 public key 37 | b := x509.MarshalPKCS1PublicKey(k) 38 | hasher := crypto.SHA256.New() 39 | _, err := hasher.Write(b) 40 | if err != nil { 41 | panic(err) 42 | } 43 | return MustID(rsaKeyHRP, hasher.Sum(nil)) 44 | } 45 | 46 | // NewRSAKey from rsa.PrivateKey. 47 | func NewRSAKey(k *rsa.PrivateKey) *RSAKey { 48 | pk := NewRSAPublicKey(&k.PublicKey) 49 | return &RSAKey{k, pk} 50 | } 51 | 52 | // PublicKey ... 53 | func (k *RSAKey) PublicKey() *RSAPublicKey { 54 | return k.publicKey 55 | } 56 | 57 | // ID for the key. 58 | func (k *RSAKey) ID() ID { 59 | return k.publicKey.ID() 60 | } 61 | 62 | // Type of key. 63 | func (k *RSAKey) Type() KeyType { 64 | return RSA 65 | } 66 | 67 | // Private key data (PKCS1). 68 | func (k *RSAKey) Private() []byte { 69 | return x509.MarshalPKCS1PrivateKey(k.privateKey) 70 | } 71 | 72 | // Public key data (PKCS1). 73 | func (k *RSAKey) Public() []byte { 74 | return k.publicKey.Public() 75 | } 76 | 77 | // NewRSAPublicKey returns RSA public key. 78 | func NewRSAPublicKey(pk *rsa.PublicKey) *RSAPublicKey { 79 | id := keyIDFromRSA(pk) 80 | return &RSAPublicKey{id, pk} 81 | } 82 | 83 | // NewRSAPublicKeyFromBytes returns RSA public key from PKC1 bytes. 84 | func NewRSAPublicKeyFromBytes(publicKey []byte) (*RSAPublicKey, error) { 85 | pk, err := x509.ParsePKCS1PublicKey(publicKey) 86 | if err != nil { 87 | return nil, err 88 | } 89 | return NewRSAPublicKey(pk), nil 90 | } 91 | 92 | // ID is key identifier. 93 | func (k *RSAPublicKey) ID() ID { 94 | return k.id 95 | } 96 | 97 | // Bytes for public key (PKCS1). 98 | func (k *RSAPublicKey) Bytes() []byte { 99 | return x509.MarshalPKCS1PublicKey(k.pk) 100 | } 101 | 102 | // Public key data. 103 | func (k *RSAPublicKey) Public() []byte { 104 | return k.Bytes() 105 | } 106 | 107 | // Private returns nil. 108 | func (k *RSAPublicKey) Private() []byte { 109 | return nil 110 | } 111 | 112 | // Type of key. 113 | func (k *RSAPublicKey) Type() KeyType { 114 | return RSA 115 | } 116 | 117 | // GenerateRSAKey generates a RSA key. 118 | func GenerateRSAKey() *RSAKey { 119 | priv, err := rsa.GenerateKey(rand.Reader, 4096) 120 | if err != nil { 121 | panic(err) 122 | } 123 | return NewRSAKey(priv) 124 | } 125 | -------------------------------------------------------------------------------- /rsa_test.go: -------------------------------------------------------------------------------- 1 | package keys_test 2 | 3 | import ( 4 | "crypto/rsa" 5 | "math/big" 6 | "testing" 7 | 8 | "github.com/keys-pub/keys" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestRSAKey(t *testing.T) { 13 | key := keys.NewRSAKey(test2048RSAKey) 14 | require.Equal(t, keys.ID("rsa1lg8lhzpatgmakvrkz866fehw64lkdtly3t2q7d36kfyhmaauyg2sgkhan4"), key.ID()) 15 | require.Equal(t, "rsa", string(key.Type())) 16 | 17 | pk := keys.NewRSAPublicKey(&test2048RSAKey.PublicKey) 18 | require.Equal(t, keys.ID("rsa1lg8lhzpatgmakvrkz866fehw64lkdtly3t2q7d36kfyhmaauyg2sgkhan4"), pk.ID()) 19 | require.Equal(t, "rsa", string(pk.Type())) 20 | } 21 | 22 | func fromBase10(base10 string) *big.Int { 23 | i, ok := new(big.Int).SetString(base10, 10) 24 | if !ok { 25 | panic("bad number: " + base10) 26 | } 27 | return i 28 | } 29 | 30 | var test2048RSAKey *rsa.PrivateKey 31 | 32 | func init() { 33 | test2048RSAKey = &rsa.PrivateKey{ 34 | PublicKey: rsa.PublicKey{ 35 | N: fromBase10("14314132931241006650998084889274020608918049032671858325988396851334124245188214251956198731333464217832226406088020736932173064754214329009979944037640912127943488972644697423190955557435910767690712778463524983667852819010259499695177313115447116110358524558307947613422897787329221478860907963827160223559690523660574329011927531289655711860504630573766609239332569210831325633840174683944553667352219670930408593321661375473885147973879086994006440025257225431977751512374815915392249179976902953721486040787792801849818254465486633791826766873076617116727073077821584676715609985777563958286637185868165868520557"), 36 | E: 3, 37 | }, 38 | D: fromBase10("9542755287494004433998723259516013739278699355114572217325597900889416163458809501304132487555642811888150937392013824621448709836142886006653296025093941418628992648429798282127303704957273845127141852309016655778568546006839666463451542076964744073572349705538631742281931858219480985907271975884773482372966847639853897890615456605598071088189838676728836833012254065983259638538107719766738032720239892094196108713378822882383694456030043492571063441943847195939549773271694647657549658603365629458610273821292232646334717612674519997533901052790334279661754176490593041941863932308687197618671528035670452762731"), 39 | Primes: []*big.Int{ 40 | fromBase10("130903255182996722426771613606077755295583329135067340152947172868415809027537376306193179624298874215608270802054347609836776473930072411958753044562214537013874103802006369634761074377213995983876788718033850153719421695468704276694983032644416930879093914927146648402139231293035971427838068945045019075433"), 41 | fromBase10("109348945610485453577574767652527472924289229538286649661240938988020367005475727988253438647560958573506159449538793540472829815903949343191091817779240101054552748665267574271163617694640513549693841337820602726596756351006149518830932261246698766355347898158548465400674856021497190430791824869615170301029"), 42 | }, 43 | } 44 | test2048RSAKey.Precompute() 45 | } 46 | -------------------------------------------------------------------------------- /saltpack/README.md: -------------------------------------------------------------------------------- 1 | # Saltpack 2 | 3 | See [saltpack.org](https://saltpack.org) for more details. 4 | 5 | This [github.com/keys-pub/keys/saltpack](https://github.com/keys-pub/keys/tree/master/saltpack) package allows you to encrypt/decrypt, sign/verify using Saltpack. 6 | 7 | ## Examples 8 | 9 | - [Encrypt/Decrypt](https://github.com/keys-pub/keys/blob/master/saltpack/encrypt_examples_test.go) 10 | - [Signcrypt/Open](https://github.com/keys-pub/keys/blob/master/saltpack/signcrypt_examples_test.go) 11 | - [Sign + Verify](https://github.com/keys-pub/keys/blob/master/saltpack/sign_examples_test.go) 12 | -------------------------------------------------------------------------------- /saltpack/detect.go: -------------------------------------------------------------------------------- 1 | package saltpack 2 | 3 | import ( 4 | "bytes" 5 | 6 | ksaltpack "github.com/keybase/saltpack" 7 | "github.com/keys-pub/keys" 8 | ) 9 | 10 | // Encoding for saltpack (encrypt, signcrypt, sign). 11 | type Encoding string 12 | 13 | const ( 14 | // UnknownEncoding is unknown. 15 | UnknownEncoding Encoding = "" 16 | // EncryptEncoding used saltpack.Encrypt 17 | EncryptEncoding Encoding = "encrypt" 18 | // SigncryptEncoding used saltpack.Signcrypt 19 | SigncryptEncoding Encoding = "signcrypt" 20 | // SignEncoding used saltpack.Sign 21 | SignEncoding Encoding = "sign" 22 | ) 23 | 24 | func detectEncrypt(b []byte) (Encoding, bool) { 25 | if _, _, err := NewDecryptStream(bytes.NewReader(b), false, nil); err == ksaltpack.ErrNoDecryptionKey { 26 | return EncryptEncoding, false 27 | } 28 | if _, _, err := NewDecryptStream(bytes.NewReader(b), true, nil); err == ksaltpack.ErrNoDecryptionKey { 29 | return EncryptEncoding, true 30 | } 31 | if _, _, err := NewSigncryptOpenStream(bytes.NewReader(b), false, nil); err == ksaltpack.ErrNoDecryptionKey { 32 | return SigncryptEncoding, false 33 | } 34 | if _, _, err := NewSigncryptOpenStream(bytes.NewReader(b), true, nil); err == ksaltpack.ErrNoDecryptionKey { 35 | return SigncryptEncoding, true 36 | } 37 | return UnknownEncoding, false 38 | } 39 | 40 | func detectSign(b []byte) (Encoding, bool) { 41 | kr := &saltpack{} 42 | if _, _, _, err := ksaltpack.NewDearmor62VerifyStream(signVersionValidator, bytes.NewReader(b), kr); err == nil { 43 | return SignEncoding, true 44 | } 45 | if _, _, err := ksaltpack.NewVerifyStream(signVersionValidator, bytes.NewReader(b), kr); err == nil { 46 | return SignEncoding, false 47 | } 48 | return UnknownEncoding, false 49 | } 50 | 51 | func detectSignDetached(b []byte) (Encoding, bool) { 52 | kr := &saltpack{} 53 | var buf bytes.Buffer 54 | if _, _, err := ksaltpack.Dearmor62VerifyDetachedReader(signVersionValidator, &buf, string(b), kr); err == keys.ErrVerifyFailed { 55 | return SignEncoding, true 56 | } 57 | if _, err := ksaltpack.VerifyDetachedReader(signVersionValidator, &buf, b, kr); err == keys.ErrVerifyFailed { 58 | return SignEncoding, false 59 | } 60 | return UnknownEncoding, false 61 | } 62 | -------------------------------------------------------------------------------- /saltpack/detect_test.go: -------------------------------------------------------------------------------- 1 | package saltpack 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/keys-pub/keys" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestDetectEncrypt(t *testing.T) { 12 | alice := keys.NewEdX25519KeyFromSeed(testSeed(0x01)) 13 | bob := keys.NewEdX25519KeyFromSeed(testSeed(0x02)) 14 | message := []byte("hi bob") 15 | xalice := alice.X25519Key() 16 | xbob := bob.X25519Key() 17 | 18 | encrypted, err := Encrypt(message, false, xalice, xbob.ID()) 19 | require.NoError(t, err) 20 | enc, armored := detectEncrypt(encrypted) 21 | require.Equal(t, EncryptEncoding, enc) 22 | require.False(t, armored) 23 | 24 | out, err := Encrypt(message, true, xalice, xbob.ID()) 25 | require.NoError(t, err) 26 | enc, armored = detectEncrypt([]byte(out)) 27 | require.Equal(t, EncryptEncoding, enc) 28 | require.True(t, armored) 29 | 30 | signcrypted, err := Signcrypt(message, false, alice, bob.ID()) 31 | require.NoError(t, err) 32 | enc, armored = detectEncrypt([]byte(signcrypted)) 33 | require.Equal(t, SigncryptEncoding, enc) 34 | require.False(t, armored) 35 | 36 | out, err = Signcrypt(message, true, alice, bob.ID()) 37 | require.NoError(t, err) 38 | enc, armored = detectEncrypt([]byte(out)) 39 | require.Equal(t, SigncryptEncoding, enc) 40 | require.True(t, armored) 41 | 42 | enc, _ = detectEncrypt([]byte{0x01}) 43 | require.Equal(t, UnknownEncoding, enc) 44 | } 45 | 46 | func testSeed(b byte) *[32]byte { 47 | return keys.Bytes32(bytes.Repeat([]byte{b}, 32)) 48 | } 49 | -------------------------------------------------------------------------------- /saltpack/encrypt_examples_test.go: -------------------------------------------------------------------------------- 1 | package saltpack_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/keys-pub/keys" 8 | "github.com/keys-pub/keys/saltpack" 9 | ) 10 | 11 | func ExampleEncrypt() { 12 | alice := keys.NewX25519KeyFromSeed(testSeed(0x01)) 13 | bobID := keys.ID("kbx1e6xn45wvkce7c7msc9upffw8dmxs9959q5xng369hgzcwrjc04vs8u82mj") 14 | 15 | message := []byte("hi bob") 16 | 17 | fmt.Printf("alice: %s\n", alice.ID()) 18 | fmt.Printf("bob: %s\n", bobID) 19 | 20 | // Encrypt from alice to bob 21 | encrypted, err := saltpack.Encrypt(message, true, alice, bobID) 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | fmt.Printf("%s...\n", encrypted[0:30]) 26 | // Output: 27 | // alice: kbx15nsf9y4k28p83wth93tf7hafhvfajp45d2mge80ems45gz0c5gys57cytk 28 | // bob: kbx1e6xn45wvkce7c7msc9upffw8dmxs9959q5xng369hgzcwrjc04vs8u82mj 29 | // BEGIN SALTPACK ENCRYPTED MESSA... 30 | } 31 | 32 | func ExampleDecrypt() { 33 | aliceID := keys.ID("kbx15nsf9y4k28p83wth93tf7hafhvfajp45d2mge80ems45gz0c5gys57cytk") 34 | encrypted := []byte(`BEGIN SALTPACK ENCRYPTED MESSAGE. 35 | kcJn5brvybfNjz6 D5ll2Nk0YnkdsxV g8EmizCg7a8zpHt Wh3GEuw5BrCUP2u N00ZdO6tTiw5NAl 36 | M2M9M0ErPX1xAmK Cfh7IG2sQfbxIH3 OmQwZxc13hjpoG4 1NWwphYm2HR7i1Z LOdCpf8kbf5UFSC 37 | eEUlInuYgfWLJdT 7y3iBbCvlejdmJW aSRZAgrmiEqYfTL a0NzUyir4lT4h9G DUYEGWA8JD3cuCh 38 | Xfi0TNH5BlgOnBm 65o53Xaztwpv6Q4 BMM6AoTyMYk9iR3 5ybluVFI5DJq0YP N6t. 39 | END SALTPACK ENCRYPTED MESSAGE.`) 40 | 41 | bob := keys.NewX25519KeyFromSeed(testSeed(0x02)) 42 | 43 | // Bob decrypts 44 | out, sender, err := saltpack.Decrypt(encrypted, true, saltpack.NewKeyring(bob)) 45 | if err != nil { 46 | log.Fatal(err) 47 | } 48 | 49 | if sender != nil && sender.ID() == aliceID { 50 | fmt.Printf("signer is alice\n") 51 | } 52 | fmt.Printf("%s\n", string(out)) 53 | 54 | // Output: 55 | // signer is alice 56 | // hi bob 57 | } 58 | -------------------------------------------------------------------------------- /saltpack/errors.go: -------------------------------------------------------------------------------- 1 | package saltpack 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/keys-pub/keys" 7 | 8 | ksaltpack "github.com/keybase/saltpack" 9 | ) 10 | 11 | // ErrInvalidData if data was invalid. 12 | var ErrInvalidData = errors.New("invalid data") 13 | 14 | func convertSignKeyErr(err error) error { 15 | if kerr, ok := err.(ksaltpack.ErrNoSenderKey); ok { 16 | if len(kerr.Sender) == 32 { 17 | spk := keys.NewEdX25519PublicKey(keys.Bytes32(kerr.Sender)) 18 | return keys.NewErrNotFound(spk.ID().String()) 19 | } 20 | } 21 | // if err == ksaltpack.ErrNoDecryptionKey { 22 | // } 23 | return err 24 | } 25 | 26 | func convertBoxKeyErr(err error) error { 27 | if kerr, ok := err.(ksaltpack.ErrNoSenderKey); ok { 28 | if len(kerr.Sender) == 32 { 29 | bpk := keys.NewX25519PublicKey(keys.Bytes32(kerr.Sender)) 30 | return keys.NewErrNotFound(bpk.ID().String()) 31 | } 32 | } 33 | // if err == ksaltpack.ErrNoDecryptionKey { 34 | // } 35 | return err 36 | } 37 | -------------------------------------------------------------------------------- /saltpack/keybase_test.go: -------------------------------------------------------------------------------- 1 | package saltpack_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keys-pub/keys/saltpack" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestKeybaseMessageNoKey(t *testing.T) { 11 | msg := `BEGIN KEYBASE SALTPACK ENCRYPTED MESSAGE. kiS2n6W4XB3QNEC peK0YLJK3LobCmd gqRoOu9htbeNE4l cgh15YfdlKRoFob Gv3J1mr1FhUvKyU pm9W7ClSTRkJOX9 ig5OOn2RHKIpN20 ybTj8AzWbXBhmD8 y9fXvXmW3FMSnC7 Ara2CtZYt0gsE2o bTTsMhU9hBkTww9 rNTZErpLemI6vX0 ms3GBba8SVigyG9 SL4eGq8pzYJTYw7 U0eshvPZ3ikNfcV Z3wp9PRajDjkOMQ yMdj2NDXZDsBveA A0E1V3At27ZETJr OukyTS0hY4iVYXv qEbD5c80UENFJdl wvM152wLf7LwI4R NY9jkDsrXcaHrIJ I4UT6fkr2xTc0j4 DiMO7m5MNHZNc6q 99yLxq9KaRHhc8t D1k9DTKZWYIWrjc EJVErjMvjbBcoKu GOOPEdXwsHJ6q7W NkMrPVFZrQw8kvX Wop2vh1CZdMEEF7 k8Ekv8SBEosw5kQ G8iRPBp1fi491TZ R7Uf0YqtfBiZogG F3CO1tVWZAh3zVi XbnYtJIoTWCii1f tcbMPHlhlgX2NwW 7VAzUleQCWfikye 8KljVNitmKzmACy gGZMibJeKwo8x5h DuSXFDHJRGzFhEW smQz1U8GpHZ1bfC 4J7N8eQNameSAFG e95qC8eTHimQ6x7 ht5NzQC20VHH8mH 8qDd5uCbaGXPALp rerajB8P8AIuOrq hcy7WrNsIAXfQl5 Smm4EmP3JJgnurK UoYXeqbU2YsdzGZ 1kaVk4RpbOXPKps myeCMRlZhomDYDq MGimdSh41dCMEIz b2Yv4pbjSh4c7GD ESuHHoATzOWpjZu uQk4pjzr09HzbZo Vb3HlHqXyUdvd5E CjEPybUmdfuwaRV nSQSxKdvSORgKZq pOVswK9Y3J2aG9i l5Wmo7X22HMpak5 N2j3weYZYhnPqgX SFZAcuUSeDT5puW UFW3HxRRA08zMeZ . END KEYBASE SALTPACK ENCRYPTED MESSAGE.` 12 | 13 | kr := saltpack.NewKeyring() 14 | _, _, enc, err := saltpack.Open([]byte(msg), kr) 15 | require.Equal(t, saltpack.SigncryptEncoding, enc) 16 | require.EqualError(t, err, "no decryption key found for message") 17 | 18 | msg2 := `BEGIN KEYBASE SALTPACK ENCRYPTED MESSAGE. kiPI2uHhOZmDhYo ynRuBAu5CSeHWTY sqCf8KAaFomjzNw j9M5xuCaxL97VRa nXAzIBiyO2gcv7V 4xKnLdJUiH1wYqd wzpflpZpHErQ9PD wnL0HoRq8LleQmr VY4TOtb9a6vMdIW wsRebbTQjYEvEmf mG2eiY0F9vA3WkN Eti2kNud2oFNjWC WzMhxgNEnb7xdX0 RUvXMNS7RhjWwmO pxo8z3zpcuPKslh 7QlWjinDuwLFAzJ z93mv8Fpoydj0LP 1bp4FcHhbqsBroE O7KskRL7QQ1y0nD cEuBcudG13G3woJ jMntM7zdAUejuQR 7PvGVoZnCXEqfvy X6F0rYpob9REIDs XW7fXqQlg13Pukw TeukcNcyshhksxR 4TgQqYwQ2s8Gert VMqKnSzucR2Nx0u WdG4oGKKLnWrBgU 7S47v8DcQwgLlEJ iMouf6gap48ovBQ rXuYbLnpjOe7UEM GIPmtLf58ettgRX SGx9mRwQlUkv1GU z6Wviuwx3syY2Dz 7BlCXKtbC0LgRpT 7g37GjMcCzAIxou yzocJ9x2M3aUN5s 6UnTZkZy3D7bITR q84XJh0qfd2MhjI ipASuygG3z8DtPB 1foWBXfS7ctXkph lqMQ0jV4zc6NBof XlTpx73ABTDvjkF q5hGHgJ6nK0JTAg 9ay6LoKBkWzkqph 77uQozls0t1TuZI l5mdZ7cjtLp48Ya ZwcE261wHlg5AUU 4HhGYqB7pTFI0qX xb8i1FepoQuWhlD 0leM1ezREZax4Xo jrSa6BToOwRAwMy U2dHKMQdAML812D 66vkdTJIWoDT61B z78xGEGL4fN5ijZ dxVNEulmP3GfSmS qhn2DpMSgdB9jYp OjNRl0Srq6YTSL. END KEYBASE SALTPACK ENCRYPTED MESSAGE.` 19 | _, _, enc, err = saltpack.Open([]byte(msg2), kr) 20 | require.Equal(t, saltpack.EncryptEncoding, enc) 21 | require.EqualError(t, err, "no decryption key found for message") 22 | } 23 | -------------------------------------------------------------------------------- /saltpack/saltpack_test.go: -------------------------------------------------------------------------------- 1 | package saltpack_test 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/keys-pub/keys" 7 | ) 8 | 9 | func testSeed(b byte) *[32]byte { 10 | return keys.Bytes32(bytes.Repeat([]byte{b}, 32)) 11 | } 12 | -------------------------------------------------------------------------------- /saltpack/sign_examples_test.go: -------------------------------------------------------------------------------- 1 | package saltpack_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/keys-pub/keys" 8 | "github.com/keys-pub/keys/saltpack" 9 | ) 10 | 11 | func ExampleSign() { 12 | alice := keys.GenerateEdX25519Key() 13 | 14 | message := []byte("hi from alice") 15 | 16 | sig, err := saltpack.Sign(message, true, alice) 17 | if err != nil { 18 | log.Fatal(err) 19 | } 20 | fmt.Printf("%s\n", alice.ID()) 21 | fmt.Printf("%s\n", sig) 22 | } 23 | 24 | func ExampleSignDetached() { 25 | alice := keys.GenerateEdX25519Key() 26 | 27 | message := []byte("hi from alice") 28 | 29 | sig, err := saltpack.SignDetached(message, true, alice) 30 | if err != nil { 31 | log.Fatal(err) 32 | } 33 | fmt.Printf("%s", sig) 34 | } 35 | 36 | func ExampleVerify() { 37 | aliceID := keys.ID("kex1w2jep8dkr2s0g9kx5g6xe3387jslnlj08yactvn8xdtrx4cnypjq9rpnux") 38 | signed := []byte(`BEGIN SALTPACK SIGNED MESSAGE. 39 | kXR7VktZdyH7rvq v5wcIkHbs7mPCSd NhKLR9E0K47y29T QkuYinHym6EfZwL 40 | 1TwgxI3RQ52fHg5 1FzmLOMghcYLcV7 i0l0ovabGhxGrEl z7WuI4O3xMU5saq 41 | U28RqUnKNroATPO 5rn2YyQcut2SeMn lXJBlDqRv9WyxjG M0PcKvsAsvmid1m 42 | cqA4TCjz5V9VpuO zuIQ55lRQLeP5kU aWFxq5Nl8WsPqlR RdX86OuTbaKUvKI 43 | wdNd6ISacrT0I82 qZ71sc7sTxiMxoI P43uCGaAZZ3Ab62 vR8N6WQPE8. 44 | END SALTPACK SIGNED MESSAGE.`) 45 | 46 | out, signer, err := saltpack.Verify(signed) 47 | if err != nil { 48 | log.Fatal(err) 49 | } 50 | if signer == aliceID { 51 | fmt.Printf("signer is alice\n") 52 | } 53 | fmt.Printf("%s\n", string(out)) 54 | // Output: 55 | // signer is alice 56 | // hi from alice 57 | } 58 | -------------------------------------------------------------------------------- /saltpack/signcrypt_examples_test.go: -------------------------------------------------------------------------------- 1 | package saltpack_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/keys-pub/keys" 8 | "github.com/keys-pub/keys/saltpack" 9 | ) 10 | 11 | func ExampleSigncrypt() { 12 | alice := keys.NewEdX25519KeyFromSeed(testSeed(0x01)) 13 | bobID := keys.ID("kex1syuhwr4g05t4744r23nvxnr7en9cmz53knhr0gja7c84hr7fkw2quf6zcg") 14 | 15 | message := []byte("hi bob") 16 | 17 | fmt.Printf("alice: %s\n", alice.ID()) 18 | fmt.Printf("bob: %s\n", bobID) 19 | 20 | // Signcrypt from alice to bob 21 | encrypted, err := saltpack.Signcrypt(message, true, alice, bobID) 22 | if err != nil { 23 | log.Fatal(err) 24 | } 25 | fmt.Printf("%s...\n", encrypted[0:30]) 26 | // Output: 27 | // alice: kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077 28 | // bob: kex1syuhwr4g05t4744r23nvxnr7en9cmz53knhr0gja7c84hr7fkw2quf6zcg 29 | // BEGIN SALTPACK ENCRYPTED MESSA... 30 | } 31 | 32 | func ExampleSigncryptOpen() { 33 | aliceID := keys.ID("kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077") 34 | encrypted := []byte(`BEGIN SALTPACK ENCRYPTED MESSAGE. 35 | keDIDMQWYvVR58B FTfTeDQNHw4rtf4 DhnUhh7QIMs1BwB LmssBxGhQ4mlcCU qV8WjYl8IkxQJbg 36 | ONicYJ6bKt4MtL5 u1uoXQQMHpGQoxv i81G0YjJmVk3fve kTnkT7hxuNZPhL3 2gdI2jzdhgOuv2I 37 | GepiKbfYFkh9crE 1N4kuPgLFmiQoUb UxbqPeFjmNwUTf7 zGeNEy8DBW16Iyd jw64NZ1Ln4gebRP 38 | 2mFMbPdyBRdxldx ugMs9cTZ2cTcyWJ mTPQ9RkdnnfPGdd k6x2hQWAdkwBOmy 4NcS7hFls2iGX4I 39 | 4lh5nDtDzwGHFOn ehwbipT7iNVK9kE 388GznWBW4Vci88 43Z1Txd2cbm2dBJ y883ohi7SLL. 40 | END SALTPACK ENCRYPTED MESSAGE.`) 41 | 42 | bob := keys.NewEdX25519KeyFromSeed(testSeed(0x02)) 43 | 44 | // Bob decrypts 45 | out, sender, err := saltpack.SigncryptOpen(encrypted, true, saltpack.NewKeyring(bob)) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | 50 | if sender != nil && sender.ID() == aliceID { 51 | fmt.Printf("signer is alice\n") 52 | } 53 | fmt.Printf("%s\n", string(out)) 54 | 55 | // Output: 56 | // signer is alice 57 | // hi bob 58 | } 59 | -------------------------------------------------------------------------------- /saltpack/signkey.go: -------------------------------------------------------------------------------- 1 | package saltpack 2 | 3 | import ( 4 | "golang.org/x/crypto/ed25519" 5 | 6 | ksaltpack "github.com/keybase/saltpack" 7 | "github.com/keys-pub/keys" 8 | "golang.org/x/crypto/nacl/sign" 9 | ) 10 | 11 | // signKey is a wrapper for keys.SignKey. 12 | type signKey struct { 13 | ksaltpack.SigningSecretKey 14 | privateKey *[ed25519.PrivateKeySize]byte 15 | publicKey *keys.EdX25519PublicKey 16 | } 17 | 18 | // newSignKey creates SigningSecretKey from a keys.SignKey. 19 | func newSignKey(sk *keys.EdX25519Key) *signKey { 20 | return &signKey{ 21 | privateKey: sk.PrivateKey(), 22 | publicKey: sk.PublicKey(), 23 | } 24 | } 25 | 26 | func (k *signKey) Sign(message []byte) ([]byte, error) { 27 | signedMessage := sign.Sign(nil, message, k.privateKey) 28 | return signedMessage[:sign.Overhead], nil 29 | } 30 | 31 | func (k *signKey) GetPublicKey() ksaltpack.SigningPublicKey { 32 | return newSignPublicKey(k.publicKey) 33 | } 34 | 35 | // signPublicKey is a wrapper for keys.SignPublicKey. 36 | type signPublicKey struct { 37 | ksaltpack.SigningPublicKey 38 | pk *keys.EdX25519PublicKey 39 | } 40 | 41 | // newSignPublicKey creates SignPublicKey for keys.SignPublicKey. 42 | func newSignPublicKey(pk *keys.EdX25519PublicKey) *signPublicKey { 43 | return &signPublicKey{pk: pk} 44 | } 45 | 46 | func (k signPublicKey) ToKID() []byte { 47 | return k.pk.Bytes()[:] 48 | } 49 | 50 | func (k signPublicKey) Verify(message []byte, signature []byte) error { 51 | signedMessage := append(signature, message...) 52 | _, err := k.pk.Verify(signedMessage) 53 | return err 54 | } 55 | -------------------------------------------------------------------------------- /secretbox.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/pkg/errors" 7 | "golang.org/x/crypto/argon2" 8 | "golang.org/x/crypto/nacl/secretbox" 9 | ) 10 | 11 | // SecretBoxSeal encrypts using a key. 12 | // It prepends a 24 byte nonce to the the encrypted bytes. 13 | func SecretBoxSeal(b []byte, secretKey *[32]byte) []byte { 14 | nonce := Rand24() 15 | return secretBoxSeal(b, nonce, secretKey) 16 | } 17 | 18 | func secretBoxSeal(b []byte, nonce *[24]byte, secretKey *[32]byte) []byte { 19 | encrypted := secretbox.Seal(nil, b, nonce, secretKey) 20 | encrypted = append(nonce[:], encrypted...) 21 | return encrypted 22 | } 23 | 24 | // SecretBoxOpen decrypt using a key. 25 | // It assumes a 24 byte nonce before the encrypted bytes. 26 | func SecretBoxOpen(encrypted []byte, secretKey *[32]byte) ([]byte, error) { 27 | return secretBoxOpen(encrypted, secretKey) 28 | } 29 | 30 | func secretBoxOpen(encrypted []byte, secretKey *[32]byte) ([]byte, error) { 31 | if len(encrypted) < 24 { 32 | return nil, errors.Errorf("not enough bytes") 33 | } 34 | var nonce [24]byte 35 | copy(nonce[:], encrypted[:24]) 36 | encrypted = encrypted[24:] 37 | 38 | b, ok := secretbox.Open(nil, encrypted, &nonce, secretKey) 39 | if !ok { 40 | return nil, errors.Errorf("secretbox open failed") 41 | } 42 | return b, nil 43 | } 44 | 45 | // EncryptWithPassword encrypts bytes with a password. 46 | // Uses argon2.IDKey(password, salt, 1, 64*1024, 4, 32) with 16 byte salt. 47 | // The salt bytes are prepended to the encrypted bytes. 48 | // This uses nacl.secretbox, so the bytes/message should be small. 49 | // If you need to encrypt large amounts of data, use Saltpack instead 50 | // (TODO: More details here). 51 | func EncryptWithPassword(b []byte, password string) []byte { 52 | salt := Rand16() 53 | key := argon2.IDKey([]byte(password), salt[:], 1, 64*1024, 4, 32) 54 | encrypted := SecretBoxSeal(b, Bytes32(key)) 55 | return bytesJoin(salt[:], encrypted) 56 | } 57 | 58 | // DecryptWithPassword decrypts bytes using a password. 59 | // It assumes a 16 byte salt before the encrypted bytes. 60 | func DecryptWithPassword(encrypted []byte, password string) ([]byte, error) { 61 | if len(encrypted) < 16 { 62 | return nil, errors.Errorf("failed to decrypt with a password: not enough bytes") 63 | } 64 | salt := encrypted[0:16] 65 | b := encrypted[16:] 66 | key := argon2.IDKey([]byte(password), salt[:], 1, 64*1024, 4, 32) 67 | out, err := SecretBoxOpen(b, Bytes32(key)) 68 | if err != nil { 69 | return nil, errors.Wrapf(err, "failed to decrypt with a password") 70 | } 71 | return out, nil 72 | } 73 | 74 | func bytesJoin(b ...[]byte) []byte { 75 | return bytes.Join(b, []byte{}) 76 | } 77 | -------------------------------------------------------------------------------- /security.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e -u -o pipefail # Fail on error 4 | 5 | dir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 6 | cd "$dir" 7 | 8 | go get github.com/securego/gosec/v2/cmd/gosec; gosec ./... 9 | -------------------------------------------------------------------------------- /sha.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | ) 7 | 8 | // HMACSHA256 does a HMAC-SHA256 on msg with key. 9 | func HMACSHA256(key []byte, msg []byte) []byte { 10 | if len(key) == 0 { 11 | panic("empty hmac key") 12 | } 13 | if len(msg) == 0 { 14 | panic("empty hmac msg") 15 | } 16 | h := hmac.New(sha256.New, key) 17 | n, err := h.Write(msg) 18 | if err != nil { 19 | panic(err) 20 | } 21 | if n != len(msg) { 22 | panic("failed to write all bytes") 23 | } 24 | out := h.Sum(nil) 25 | if len(out) == 0 { 26 | panic("empty bytes") 27 | } 28 | return out 29 | } 30 | -------------------------------------------------------------------------------- /sha_test.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import ( 4 | "encoding/hex" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | type testVector struct { 11 | key string 12 | msg string 13 | expected string 14 | truncate int 15 | } 16 | 17 | func TestHMACSHA256(t *testing.T) { 18 | vectors := []testVector{ 19 | { 20 | key: "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", 21 | msg: "4869205468657265", 22 | expected: "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7", 23 | }, 24 | { 25 | key: "4a656665", 26 | msg: "7768617420646f2079612077616e7420666f72206e6f7468696e673f", 27 | expected: "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843", 28 | }, 29 | { 30 | key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 31 | msg: "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", 32 | expected: "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe", 33 | }, 34 | { 35 | key: "0102030405060708090a0b0c0d0e0f10111213141516171819", 36 | msg: "cdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", 37 | expected: "82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b", 38 | }, 39 | { 40 | key: "0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c", 41 | msg: "546573742057697468205472756e636174696f6e", 42 | expected: "a3b6167473100ee06e0c796c2955552b", 43 | truncate: 16, 44 | }, 45 | { 46 | key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 47 | msg: "54657374205573696e67204c6172676572205468616e20426c6f636b2d53697a65204b6579202d2048617368204b6579204669727374", 48 | expected: "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54", 49 | }, 50 | { 51 | key: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 52 | msg: "5468697320697320612074657374207573696e672061206c6172676572207468616e20626c6f636b2d73697a65206b657920616e642061206c6172676572207468616e20626c6f636b2d73697a6520646174612e20546865206b6579206e6565647320746f20626520686173686564206265666f7265206265696e6720757365642062792074686520484d414320616c676f726974686d2e", 53 | expected: "9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2", 54 | }, 55 | } 56 | 57 | for _, v := range vectors { 58 | key, err := hex.DecodeString(v.key) 59 | require.NoError(t, err) 60 | msg, err := hex.DecodeString(v.msg) 61 | require.NoError(t, err) 62 | out := HMACSHA256(key, msg) 63 | expected, err := hex.DecodeString(v.expected) 64 | require.NoError(t, err) 65 | if v.truncate > 0 { 66 | out = out[0:v.truncate] 67 | } 68 | require.Equal(t, expected, out) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sodiumbox.go: -------------------------------------------------------------------------------- 1 | package keys 2 | 3 | import ( 4 | "crypto/rand" 5 | 6 | "github.com/dchest/blake2b" 7 | "github.com/pkg/errors" 8 | "golang.org/x/crypto/nacl/box" 9 | ) 10 | 11 | func cryptoBoxSealNonce(ephemeralPk, publicKey *[32]byte) *[24]byte { 12 | nonce := new([24]byte) 13 | hashConfig := &blake2b.Config{Size: 24} 14 | hashFn, err := blake2b.New(hashConfig) 15 | if err != nil { 16 | panic("failed to create blake2b hash function") 17 | } 18 | _, _ = hashFn.Write(ephemeralPk[0:32]) 19 | _, _ = hashFn.Write(publicKey[0:32]) 20 | nonceSum := hashFn.Sum(nil) 21 | copy(nonce[:], nonceSum[0:24]) 22 | return nonce 23 | } 24 | 25 | // CryptoBoxSeal implements libsodium crypto_box_seal. 26 | func CryptoBoxSeal(b []byte, publicKey *X25519PublicKey) []byte { 27 | ephemeralPK, ephemeralSK, err := box.GenerateKey(rand.Reader) 28 | if err != nil { 29 | panic("failed to generate key") 30 | } 31 | nonce := cryptoBoxSealNonce(ephemeralPK, publicKey.Bytes32()) 32 | boxed := box.Seal(nil, b, nonce, publicKey.Bytes32(), ephemeralSK) 33 | return append(ephemeralPK[:], boxed...) 34 | } 35 | 36 | // CryptoBoxSealOpen implements libsodium crypto_box_seal_open. 37 | func CryptoBoxSealOpen(b []byte, key *X25519Key) ([]byte, error) { 38 | if len(b) < 32 { 39 | return nil, errors.Errorf("not enough data to box open") 40 | } 41 | ephemeralPK := Bytes32(b[:32]) 42 | nonce := cryptoBoxSealNonce(ephemeralPK, key.PublicKey().Bytes32()) 43 | result, ok := box.Open(nil, b[32:], nonce, ephemeralPK, key.Bytes32()) 44 | if !ok { 45 | return nil, errors.Errorf("failed to box open") 46 | } 47 | return result, nil 48 | } 49 | -------------------------------------------------------------------------------- /sodiumbox_test.go: -------------------------------------------------------------------------------- 1 | package keys_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keys-pub/keys" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestCryptoBoxSeal(t *testing.T) { 11 | alice := keys.GenerateX25519Key() 12 | charlie := keys.GenerateX25519Key() 13 | 14 | plaintext := []byte("my secret message") 15 | 16 | encrypted := keys.CryptoBoxSeal(plaintext, alice.PublicKey()) 17 | 18 | decrypted, err := keys.CryptoBoxSealOpen(encrypted, alice) 19 | require.NoError(t, err) 20 | require.Equal(t, plaintext, decrypted) 21 | 22 | _, err = keys.CryptoBoxSealOpen(encrypted, charlie) 23 | require.EqualError(t, err, "failed to box open") 24 | } 25 | -------------------------------------------------------------------------------- /statement_example_test.go: -------------------------------------------------------------------------------- 1 | package keys_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | 9 | "github.com/keys-pub/keys" 10 | ) 11 | 12 | func ExampleStatement() { 13 | sk := keys.NewEdX25519KeyFromSeed(testSeed(0x01)) 14 | 15 | st := &keys.Statement{ 16 | KID: sk.ID(), 17 | Data: bytes.Repeat([]byte{0x01}, 16), 18 | Type: "test", 19 | } 20 | if err := st.Sign(sk); err != nil { 21 | log.Fatal(err) 22 | } 23 | 24 | data := st.BytesToSign() 25 | fmt.Printf("%s\n", string(data)) 26 | 27 | b, err := st.Bytes() 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | fmt.Printf("%s\n", string(b)) 32 | 33 | b, err = json.Marshal(st) 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | fmt.Printf("%s\n", string(b)) 38 | 39 | // Output: 40 | // {".sig":"","data":"AQEBAQEBAQEBAQEBAQEBAQ==","kid":"kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077","type":"test"} 41 | // {".sig":"CFD9cK9gIB3sAEqpDwmZM0JFFO4/+RpX9uoAD25G3F1o8Af+pTk6pI4GPqAZ5FhEw1rUDfL02Qnohtx05LQxAg==","data":"AQEBAQEBAQEBAQEBAQEBAQ==","kid":"kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077","type":"test"} 42 | // {".sig":"CFD9cK9gIB3sAEqpDwmZM0JFFO4/+RpX9uoAD25G3F1o8Af+pTk6pI4GPqAZ5FhEw1rUDfL02Qnohtx05LQxAg==","data":"AQEBAQEBAQEBAQEBAQEBAQ==","kid":"kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077","type":"test"} 43 | } 44 | -------------------------------------------------------------------------------- /testdata/reddit/charlie.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "kind": "Listing", 4 | "data": { 5 | "modhash": "", 6 | "dist": 1, 7 | "children": [ 8 | { 9 | "kind": "t3", 10 | "data": { 11 | "author": "charlie", 12 | "selftext": "BEGIN MESSAGE.UxYRf6H88MO9UdD JPM9n4bqFkWDI46 fi0w93vNJDeOJtx qzl2DyLKk8eiZx0 njzYTXQiCR2PiDl hDRJRZtdhogTCKq 6Xr2MZHgg4UNRDb Zy2loGoGN3Mvxd4 r7FIwpZOJPE1JEq D2gGjkgLByR9CFG 2aCgRgZZwl5UAa4 6bmBzjEOhmsiW0K TDXulMpDFV15DyJ khxi0utY1LfCBFX QrNrg2IP.END MESSAGE.", 13 | "subreddit": "u_charlie", 14 | "subreddit_type": "user" 15 | } 16 | } 17 | ], 18 | "after": null, 19 | "before": null 20 | } 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /testdata/sc1.spew: -------------------------------------------------------------------------------- 1 | /sigchain/kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077/1 {".sig":"anM0EEq+KlSjhagIQSj7WJqfzVKFQw6YoCNh3r2J2GkxVdBbMv+kz4wMTlY6K3Ta8WEsvqU9s6S4Ma9K64foCA==","data":"dGVzdDE=","kid":"kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077","seq":1,"ts":1234567890001} 2 | /sigchain/kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077/2 {".sig":"g6ivsCKgyT36LQvZ/rzvlPgHp2NZUjmgSEO7r0URAyYW5w8gaKCDbB0yxYnPOJxpVqj8nfaqZOgO1a6ra7eOAQ==","data":"dGVzdDI=","kid":"kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077","prev":"kNmVbT6pEDgTZPOaGfFC2eV7/1qDl6xoPnxitL4OocI=","seq":2,"ts":1234567890004} 3 | /sigchain/kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077/3 {".sig":"/SxwwMeAcMIaLfYl6D4DfuQ1vbO3lXEo/iQ/4RmzNxQ3MA7A7Q5oKmf4/dxP//JLtQ7jOqlwHpQMERz6YfnwCA==","kid":"kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077","prev":"orJ1uHa9xPS9VUSVh+EfQtTN6dH5TpEihUtB0B5MH6E=","revoke":2,"seq":3,"type":"revoke"} 4 | -------------------------------------------------------------------------------- /testdata/sc2.spew: -------------------------------------------------------------------------------- 1 | /sigchain/kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077/1 {".sig":"+H4VoHKAzH8e7Fn0LTtabx1MSpmnEY7xejxzMLr13Cfu1uvj4LKDKJ8AWLP38OU+HDSqO9JYkR+MtM/o7JvzAw==","data":"AQEBAQEBAQEBAQEBAQEBAQ==","kid":"kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077","seq":1,"ts":1234567890001,"type":"test"} 2 | /sigchain/kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077/2 {".sig":"Ez8WFOCIjCM4SNRk6erV8t1+9tWT8Fz1lAbmxvEytV8CHwIQi3sfvrAd0JwB+oZEmMp3WC3VJMSEqkR07iS5Bw==","kid":"kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077","prev":"V/5ecc6cFRzsi83kcaqyahjXqWp+wTxFwpJMrk+MHXA=","revoke":1,"seq":2,"type":"revoke"} 3 | /sigchain/kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077/3 {".sig":"ur9aA2EimMiivCk7V8279oql7vfVG84b93Plw9ChyfUJ9nY90VGQJzf28B3Hkv76picnh9cdkhYgo5sOFBoqDg==","data":"AgICAgICAgICAgICAgICAg==","kid":"kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077","prev":"9oUp9biqYCYF0hI7Ns1teciXTv6K2FK9b+bblBz6UC4=","seq":3,"ts":1234567890002,"type":"test"} 4 | /sigchain/kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077/4 {".sig":"o0DsNgrluQUJT3XgPo+0wt4rPh4cBRXU+lheeL0T8znO4DLohuxzg2cs7Wfm0ed2ThOgAWDnpuBjlfC1EnCtBg==","data":"AwMDAwMDAwMDAwMDAwMDAw==","kid":"kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077","prev":"DloaUVjb4QySdgWam/iAmqSUWUHEf/yleeioT3tVH8s=","seq":4,"ts":1234567890003,"type":"test"} 5 | -------------------------------------------------------------------------------- /testdata/twitter/1205589994380783616.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "author_id": "1", 4 | "id": "1205589994380783616", 5 | "text": "BEGIN MESSAGE. FD0Lv2C2AtvqD1X EwqDo1tOTkv8LKi sQMlS6gluxz0npc 1S2MuNVOfTph934 h1xXQqj5EtueEBn tfhbDceoOBETCKq 6Xr2MZHgg4UNRDb Zy2loGoGN3Mvxd4 r7FIwpZOJPE1JEq D2gGjkgLByR9CFG 2aCgRgZZwl5UAa4 6bmBzjE5yyl9oNK SO6lAVCOrl3JBga nxnssAnkQt3vM3T dJOf. END MESSAGE." 6 | }, 7 | "includes": { 8 | "users": [ 9 | { 10 | "id": "1", 11 | "name": "Bob", 12 | "username": "bob" 13 | } 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /testdata/twitter/1222706272849391616.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "author_id": "6243152", 4 | "id": "1222706272849391616", 5 | "text": "BEGIN MESSAGE.EqcgDt8RfXvPq9b 4qCV8S3VPKIQKqa N7Rc1YruQQYuVS8 niHzUv7jdykkEPSrKGcJQCNTkNE7uF swPuwfpaZX6TCKq 6Xr2MZHgg6S0Mjg WFMJ1KHxazTuXs4icK3k8SZCR8mVLQ MSVhFeMrvz0qJOm A96zW9RAY6whsLo 5fC8i3fRJjyo9mQJZid8MwBXJl1XDL 5ZOSkLYs6sk6a2g CiGyA2IP.END MESSAGE." 6 | }, 7 | "includes": { 8 | "users": [ 9 | { 10 | "id": "6243152", 11 | "name": "Gabriel Handford", 12 | "username": "gabrlh" 13 | } 14 | ] 15 | } 16 | } -------------------------------------------------------------------------------- /testdata/twitter/statement.json: -------------------------------------------------------------------------------- 1 | {".sig":"hF2w6ccdzwwgYyzYLyOqH1jMt3ChfH7JRN8Vs/nm+b1NhXtMRe7TRENx0QGn6B0v4x1zUf4epIBnaoCvj6JUAw==","data":"eyJrIjoia2V4MWUyNnJxOXZyaGp6eXhoZXAwYzVseTZydWRxN20yY2V4amxrZ2tubDJ6NGxxZjhnYTN1YXN6M3M0OG0iLCJuIjoiZ2FicmxoIiwic3EiOjEsInNyIjoidHdpdHRlciIsInUiOiJodHRwczovL3R3aXR0ZXIuY29tL2dhYnJsaC9zdGF0dXMvMTIyMjcwNjI3Mjg0OTM5MTYxNiJ9","kid":"kex1e26rq9vrhjzyxhep0c5ly6rudq7m2cexjlkgknl2z4lqf8ga3uasz3s48m","seq":1,"ts":1580350874625,"type":"user"} -------------------------------------------------------------------------------- /tsutil/clock.go: -------------------------------------------------------------------------------- 1 | package tsutil 2 | 3 | import "time" 4 | 5 | // Clock returns time.Time. 6 | type Clock interface { 7 | // Now returns current clock time. 8 | Now() time.Time 9 | 10 | // NowMillis returns current time in milliseconds. 11 | NowMillis() int64 12 | 13 | // Add time to clock. 14 | Add(dt time.Duration) 15 | } 16 | 17 | // testClock increments a millisecond on each access. 18 | // This is for testing. 19 | type testClock struct { 20 | t time.Time 21 | tick time.Duration 22 | } 23 | 24 | // NewTestClock returns a test Clock starting at 1234567890000 millseconds since 25 | // epoch. Each access to Now() increases time by 1 millisecond. 26 | func NewTestClock() Clock { 27 | t := ParseMillis(int64(1234567890000)) 28 | return &testClock{ 29 | t: t, 30 | tick: time.Millisecond, 31 | } 32 | } 33 | 34 | // Now returns current clock time. 35 | func (c *testClock) Now() time.Time { 36 | c.t = c.t.Add(c.tick) 37 | return c.t 38 | } 39 | 40 | func (c *testClock) NowMillis() int64 { 41 | return Millis(c.Now()) 42 | } 43 | 44 | // SetTick sets tick increment for clock. 45 | func (c *testClock) SetTick(tick time.Duration) { 46 | c.tick = tick 47 | } 48 | 49 | // Add to clock. 50 | func (c *testClock) Add(dt time.Duration) { 51 | c.t = c.t.Add(dt) 52 | } 53 | 54 | // NewTestClockAt creates a Clock starting at timestamp (millis). 55 | func NewTestClockAt(ts int64) Clock { 56 | t := ParseMillis(ts) 57 | return &testClock{ 58 | t: t, 59 | tick: time.Millisecond, 60 | } 61 | } 62 | 63 | // NewClock returns current clock time. 64 | func NewClock() Clock { 65 | return &clock{ 66 | add: time.Duration(0), 67 | } 68 | } 69 | 70 | type clock struct { 71 | add time.Duration 72 | } 73 | 74 | func (c *clock) Now() time.Time { 75 | return time.Now().Add(c.add) 76 | } 77 | 78 | func (c *clock) NowMillis() int64 { 79 | return Millis(c.Now()) 80 | } 81 | 82 | func (c *clock) Add(dt time.Duration) { 83 | c.add = c.add + dt 84 | } 85 | -------------------------------------------------------------------------------- /tsutil/tsutil.go: -------------------------------------------------------------------------------- 1 | // Package tsutil provides timestamp and time utilities. 2 | package tsutil 3 | 4 | import ( 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | const ( 10 | // RFC3339Milli is RFC3339 with millisecond precision. 11 | RFC3339Milli = "2006-01-02T15:04:05.000Z07:00" 12 | ) 13 | 14 | // Millis returns milliseconds since epoch to t. 15 | // Returns 0 if t.IsZero(). 16 | func Millis(t time.Time) int64 { 17 | if t.IsZero() { 18 | return 0 19 | } 20 | return int64(t.UnixNano() / int64(time.Millisecond)) 21 | } 22 | 23 | // NowMillis returns now in milliseconds since epoch. 24 | func NowMillis() int64 { 25 | return Millis(time.Now()) 26 | } 27 | 28 | // ParseMillis returns time.Time from milliseconds since epoch. 29 | func ParseMillis(i interface{}) time.Time { 30 | switch v := i.(type) { 31 | case string: 32 | n, err := strconv.ParseInt(v, 10, 64) 33 | if err != nil { 34 | return time.Time{} 35 | } 36 | return millis(n) 37 | case int64: 38 | return millis(v) 39 | case int: 40 | return millis(int64(v)) 41 | default: 42 | return time.Time{} 43 | } 44 | } 45 | 46 | // millis returns time.Time from milliseconds since epoch. 47 | func millis(n int64) time.Time { 48 | if n == 0 { 49 | return time.Time{} 50 | } 51 | return time.Unix(0, n*int64(time.Millisecond)).UTC() 52 | } 53 | 54 | // Days returns days since epoch to t. 55 | func Days(t time.Time) int { 56 | ms := Millis(t) 57 | return int(ms / 1000 / 60 / 60 / 24) 58 | } 59 | -------------------------------------------------------------------------------- /tsutil/tsutil_test.go: -------------------------------------------------------------------------------- 1 | package tsutil_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | "time" 8 | 9 | "github.com/keys-pub/keys/tsutil" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestParseMillis(t *testing.T) { 14 | t1 := time.Now().UTC() 15 | ts1 := tsutil.Millis(t1) 16 | t2 := tsutil.ParseMillis(ts1) 17 | require.Equal(t, t1.Format(time.StampMilli), t2.Format(time.StampMilli)) 18 | 19 | require.Equal(t, int64(0), tsutil.Millis(time.Time{})) 20 | require.Equal(t, time.Time{}, tsutil.ParseMillis(0)) 21 | 22 | t3 := tsutil.ParseMillis(1234567890001) 23 | tf3 := t3.Format(http.TimeFormat) 24 | require.Equal(t, "Fri, 13 Feb 2009 23:31:30 GMT", tf3) 25 | tf3 = t3.Format(tsutil.RFC3339Milli) 26 | require.Equal(t, "2009-02-13T23:31:30.001Z", tf3) 27 | 28 | t4 := tsutil.ParseMillis("1234567890001") 29 | tf4 := t4.Format(tsutil.RFC3339Milli) 30 | require.Equal(t, "2009-02-13T23:31:30.001Z", tf4) 31 | require.Equal(t, int64(1234567890001), tsutil.Millis(t4)) 32 | 33 | t5 := tsutil.ParseMillis(1234567890001) 34 | tf5 := t5.Format(tsutil.RFC3339Milli) 35 | require.Equal(t, "2009-02-13T23:31:30.001Z", tf5) 36 | } 37 | 38 | func TestRFC3339Milli(t *testing.T) { 39 | t1 := tsutil.ParseMillis(1234567890010) 40 | s1 := t1.Format(tsutil.RFC3339Milli) 41 | require.Equal(t, "2009-02-13T23:31:30.010Z", s1) 42 | tout, err := time.Parse(tsutil.RFC3339Milli, s1) 43 | require.NoError(t, err) 44 | require.Equal(t, tsutil.Millis(t1), tsutil.Millis(tout)) 45 | } 46 | 47 | func TestDays(t *testing.T) { 48 | t1 := tsutil.ParseMillis(1234567890001) 49 | days := tsutil.Days(t1) 50 | require.Equal(t, "14288", fmt.Sprintf("%d", days)) 51 | } 52 | -------------------------------------------------------------------------------- /user/echo.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | 7 | "github.com/keys-pub/keys" 8 | ) 9 | 10 | // NewEcho creates a signed user@echo (for testing). 11 | func NewEcho(sk *keys.EdX25519Key, name string, seq int) (*User, error) { 12 | usr, err := NewForSigning(sk.ID(), "echo", name) 13 | if err != nil { 14 | return nil, err 15 | } 16 | msg, err := usr.Sign(sk) 17 | if err != nil { 18 | return nil, err 19 | } 20 | urs := "test://echo/alice/" + sk.ID().String() + "/" + url.QueryEscape(strings.ReplaceAll(msg, "\n", " ")) 21 | return New(sk.ID(), "echo", name, urs, seq) 22 | } 23 | -------------------------------------------------------------------------------- /user/result.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/keys-pub/keys/tsutil" 8 | ) 9 | 10 | // Result describes the status of a User. 11 | // TODO: Make Err/Status more explicit, it can be confusing. 12 | type Result struct { 13 | // Err if error occured. 14 | // See Status for type of error. 15 | Err string `json:"err,omitempty"` 16 | // Status for result. StatusOK if ok, otherwise an error type. 17 | Status Status `json:"status"` 18 | // Timestamp is the when the status was last updated. 19 | Timestamp int64 `json:"ts"` 20 | // User. 21 | User *User `json:"user"` 22 | // Statement we found at User.URL. 23 | Statement string `json:"statement,omitempty"` 24 | // Proxied if result was through a proxy. 25 | Proxied bool `json:"proxied,omitempty"` 26 | // VerifiedAt is when the status was last OK. 27 | VerifiedAt int64 `json:"vts,omitempty"` 28 | } 29 | 30 | func (r Result) String() string { 31 | if r.Status == StatusOK { 32 | return fmt.Sprintf("%s:%s(%d)", r.Status, r.User, r.VerifiedAt) 33 | } 34 | return fmt.Sprintf("%s:%s;err=%s", r.Status, r.User, r.Err) 35 | } 36 | 37 | // IsTimestampExpired returns true if result Timestamp is older than dt. 38 | func (r Result) IsTimestampExpired(now time.Time, dt time.Duration) bool { 39 | ts := tsutil.ParseMillis(r.Timestamp) 40 | return (ts.IsZero() || now.Sub(ts) > dt) 41 | } 42 | 43 | // IsVerifyExpired returns true if result VerifiedAt is older than dt. 44 | func (r Result) IsVerifyExpired(now time.Time, dt time.Duration) bool { 45 | ts := tsutil.ParseMillis(r.VerifiedAt) 46 | return (ts.IsZero() || now.Sub(ts) > dt) 47 | } 48 | -------------------------------------------------------------------------------- /user/services/echo.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "strings" 7 | 8 | "github.com/keys-pub/keys/http" 9 | "github.com/keys-pub/keys/user" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | type echo struct{} 14 | 15 | // Echo service. 16 | var Echo = &echo{} 17 | 18 | func (s *echo) ID() string { 19 | return "echo" 20 | } 21 | 22 | func (s *echo) Request(ctx context.Context, client http.Client, usr *user.User) (user.Status, []byte, error) { 23 | ur, err := url.Parse(usr.URL) 24 | if err != nil { 25 | return user.StatusFailure, nil, err 26 | } 27 | if ur.Scheme != "test" { 28 | return user.StatusFailure, nil, errors.Errorf("invalid scheme for echo") 29 | } 30 | if ur.Host != "echo" { 31 | return user.StatusFailure, nil, errors.Errorf("invalid host for echo") 32 | } 33 | 34 | path := ur.Path 35 | path = strings.TrimPrefix(path, "/") 36 | paths := strings.Split(path, "/") 37 | if len(paths) != 3 { 38 | return user.StatusFailure, nil, errors.Errorf("path invalid %s", path) 39 | } 40 | msg, err := url.QueryUnescape(paths[2]) 41 | if err != nil { 42 | return user.StatusFailure, nil, err 43 | } 44 | 45 | if err := usr.Verify(msg); err != nil { 46 | return user.StatusFailure, nil, err 47 | } 48 | 49 | return user.StatusOK, []byte(msg), nil 50 | } 51 | 52 | func (s *echo) Verify(ctx context.Context, b []byte, usr *user.User) (user.Status, *Verified, error) { 53 | status, statement, err := user.FindVerify(usr, b, false) 54 | if err != nil { 55 | return status, nil, err 56 | } 57 | return status, &Verified{Statement: statement}, nil 58 | } 59 | -------------------------------------------------------------------------------- /user/services/github.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "time" 7 | 8 | "github.com/keys-pub/keys/http" 9 | "github.com/keys-pub/keys/user" 10 | "github.com/keys-pub/keys/user/validate" 11 | 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | // GithubID is id for github. 16 | const GithubID = "github" 17 | 18 | type github struct{} 19 | 20 | // Github service. 21 | var Github = &github{} 22 | 23 | func (s *github) ID() string { 24 | return GithubID 25 | } 26 | 27 | func (s *github) Request(ctx context.Context, client http.Client, usr *user.User) (user.Status, []byte, error) { 28 | apiURL, err := validate.Github.APIURL(usr.Name, usr.URL) 29 | if err != nil { 30 | return user.StatusFailure, nil, err 31 | } 32 | headers := s.headers() 33 | return Request(ctx, client, apiURL, headers) 34 | } 35 | 36 | func (s *github) Verify(ctx context.Context, b []byte, usr *user.User) (user.Status, *Verified, error) { 37 | var gist gist 38 | if err := json.Unmarshal(b, &gist); err != nil { 39 | return user.StatusContentInvalid, nil, err 40 | } 41 | gistUserName := validate.Github.NormalizeName(gist.Owner.Login) 42 | if gistUserName != usr.Name { 43 | return user.StatusContentInvalid, nil, errors.Errorf("invalid gist owner login %s", gist.Owner.Login) 44 | } 45 | 46 | for _, f := range gist.Files { 47 | status, statement, err := user.FindVerify(usr, []byte(f.Content), false) 48 | if err != nil { 49 | return status, nil, err 50 | } 51 | return status, &Verified{Statement: statement}, nil 52 | } 53 | 54 | return user.StatusContentInvalid, nil, errors.Errorf("no gist files") 55 | } 56 | 57 | func (s *github) headers() []http.Header { 58 | return []http.Header{{ 59 | Name: "Accept", 60 | Value: "application/vnd.github.v3+json", 61 | }} 62 | } 63 | 64 | type file struct { 65 | Filename string `json:"filename"` 66 | Type string `json:"type"` 67 | Language string `json:"language"` 68 | RawURL string `json:"raw_url"` 69 | Size int `json:"size"` 70 | Truncated bool `json:"truncated"` 71 | Content string `json:"content"` 72 | } 73 | 74 | type gist struct { 75 | ID string `json:"id"` 76 | Files map[string]*file `json:"files"` 77 | CreatedAt time.Time `json:"created_at"` 78 | UpdatedAt time.Time `json:"updated_at"` 79 | Owner struct { 80 | Login string `json:"login"` 81 | ID int `json:"id"` 82 | AvatarURL string `json:"avatar_url"` 83 | GravatarID string `json:"gravatar_id"` 84 | URL string `json:"url"` 85 | } `json:"owner"` 86 | } 87 | -------------------------------------------------------------------------------- /user/services/github_test.go: -------------------------------------------------------------------------------- 1 | package services_test 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | "github.com/keys-pub/keys" 9 | "github.com/keys-pub/keys/http" 10 | "github.com/keys-pub/keys/user" 11 | "github.com/keys-pub/keys/user/services" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestGithub(t *testing.T) { 16 | // user.SetLogger(user.NewLogger(user.DebugLevel)) 17 | // services.SetLogger(user.NewLogger(user.DebugLevel)) 18 | // TODO: Fails on CI because of gist download? 19 | if os.Getenv("CI") != "" { 20 | t.Skip() 21 | } 22 | 23 | kid := keys.ID("kex1mnseg28xu6g3j4wur7hqwk8ag3fu3pmr2t5lync26xmgff0dtryqupf80c") 24 | urs := "https://gist.github.com/gabriel/ceea0f3b675bac03425472692273cf52" 25 | 26 | client := http.NewClient() 27 | 28 | usr, err := user.New(kid, "github", "gabriel", urs, 1) 29 | require.NoError(t, err) 30 | result := services.Verify(context.TODO(), services.Github, client, usr) 31 | require.Equal(t, user.StatusOK, result.Status) 32 | expected := `BEGIN MESSAGE. 33 | kdZaJI1U5AS7G6i VoUxdP8OtPzEoM6 pYhVl0YQZJnotVE wLg9BDb5SUO05pm 34 | abUSeCvBfdPoRpP J8wrcF5PP3wTCKq 6Xr2MZHgg6m2Qal gJCD6vMqlBQfIg6 35 | QsfB27aP5DMuXlJ AUVIAvMDHIoptmS riNMzfpwBjRShVL WH70a0GOEqD6L8b 36 | kC5EFOwCedvHFpc AQVqULHjcSpeCfZ EIOaQ2IP. 37 | END MESSAGE.` 38 | require.Equal(t, expected, result.Statement) 39 | } 40 | 41 | func TestGithubKeysPubUser(t *testing.T) { 42 | // user.SetLogger(user.NewLogger(user.DebugLevel)) 43 | // services.SetLogger(user.NewLogger(user.DebugLevel)) 44 | // TODO: Fails on CI because of gist download? 45 | if os.Getenv("CI") != "" { 46 | t.Skip() 47 | } 48 | 49 | kid := keys.ID("kex1ncfla8g5ez6vfq3trj9vpsdswqlv9fcqdks6x86nt0j7yljk3d8supvfj7") 50 | urs := "https://gist.github.com/keys-pub-user/63965d96e6586ee7e3ec3530e4331982" 51 | 52 | client := http.NewClient() 53 | 54 | usr, err := user.New(kid, "github", "keys-pub-user", urs, 1) 55 | require.NoError(t, err) 56 | result := services.Verify(context.TODO(), services.Github, client, usr) 57 | require.Equal(t, user.StatusOK, result.Status) 58 | expected := `BEGIN MESSAGE. 59 | tFOTGkqFiM1YL3P lG4V0DzFi95jz1A VaOn0e5KzZ5wzFq D2LanZPiN928o3M 60 | jPUOV3KlEcDr0iV Y6R2GYtcP2WTCKq 6Xr2MZHgg6oMjst MtVs8AxBTgCn0ed 61 | yNy78Ob23NoqDTi HLaHzAYYHCLYA3H hpW04H2qOcy9wUT TbzbvuqS4jIXVCm 62 | WgBpeqxaDOC8tGL 2rHKnD6KhZrBw8d tSPCc8sSTVh227E D. 63 | END MESSAGE.` 64 | require.Equal(t, expected, result.Statement) 65 | } 66 | -------------------------------------------------------------------------------- /user/services/https.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/keys-pub/keys/http" 7 | "github.com/keys-pub/keys/user" 8 | ) 9 | 10 | type https struct{} 11 | 12 | // HTTPS service. 13 | var HTTPS = &https{} 14 | 15 | func (s *https) ID() string { 16 | return "https" 17 | } 18 | 19 | func (s *https) Request(ctx context.Context, client http.Client, usr *user.User) (user.Status, []byte, error) { 20 | return Request(ctx, client, usr.URL, nil) 21 | } 22 | 23 | func (s *https) Verify(ctx context.Context, b []byte, usr *user.User) (user.Status, *Verified, error) { 24 | status, statement, err := user.FindVerify(usr, b, false) 25 | if err != nil { 26 | return status, nil, err 27 | } 28 | return status, &Verified{Statement: statement}, nil 29 | } 30 | -------------------------------------------------------------------------------- /user/services/https_test.go: -------------------------------------------------------------------------------- 1 | package services_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/keys-pub/keys" 8 | "github.com/keys-pub/keys/http" 9 | "github.com/keys-pub/keys/user" 10 | "github.com/keys-pub/keys/user/services" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestHTTPS(t *testing.T) { 15 | // user.SetLogger(user.NewLogger(user.DebugLevel)) 16 | // services.SetLogger(user.NewLogger(user.DebugLevel)) 17 | 18 | kid := keys.ID("kex1ydecaulsg5qty2axyy770cjdvqn3ef2qa85xw87p09ydlvs5lurq53x0p3") 19 | 20 | usr, err := user.New(kid, "https", "keys.pub", "https://keys.pub/keyspub.txt", 1) 21 | require.NoError(t, err) 22 | client := http.NewClient() 23 | result := services.Verify(context.TODO(), services.HTTPS, client, usr) 24 | require.Equal(t, user.StatusOK, result.Status) 25 | expected := `BEGIN MESSAGE. 26 | 7PPiOMcdjhvnXzM 1uVwr224ccgiOKt I5vwzYoRY3xgUdL 86O3X1DnuZwCTIP 27 | ACnuZKXBB4y39qQ f7sq7eoQs8oTCKq 6Xr2MZHgg7F8Mca NbI7en6mNzlIVvQ 28 | zIh84hprPPEByeP D9s1xc5HURCNFcv rsOvrUoV0oHQfyi 89aehuNSV2AP9hp 29 | 8dGT8SwS3TEo3FP b1X8S32XyBenWKF aJv7L2IP. 30 | END MESSAGE.` 31 | require.Equal(t, expected, result.Statement) 32 | } 33 | -------------------------------------------------------------------------------- /user/services/keyspub.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | 8 | "github.com/keys-pub/keys" 9 | "github.com/keys-pub/keys/http" 10 | "github.com/keys-pub/keys/user" 11 | "github.com/pkg/errors" 12 | ) 13 | 14 | type keyspub struct{} 15 | 16 | // KeysPub uses keys.pub user cache instead of the service directly. 17 | var KeysPub = &keyspub{} 18 | 19 | func (s *keyspub) Request(ctx context.Context, client http.Client, usr *user.User) (user.Status, []byte, error) { 20 | url := fmt.Sprintf("https://keys.pub/user/%s@%s", usr.Name, usr.Service) 21 | return Request(ctx, client, url, nil) 22 | } 23 | 24 | func (s *keyspub) Verify(ctx context.Context, b []byte, usr *user.User) (user.Status, *Verified, error) { 25 | userStatus, err := s.checkContent(usr, b) 26 | if err != nil { 27 | return user.StatusContentInvalid, nil, err 28 | } 29 | status, statement, err := user.FindVerify(usr, []byte(userStatus.Statement), false) 30 | if err != nil { 31 | return status, nil, err 32 | } 33 | 34 | verified := &Verified{Statement: statement, Timestamp: userStatus.VerifiedAt, Proxied: true} 35 | return status, verified, nil 36 | } 37 | 38 | func (s *keyspub) checkContent(usr *user.User, b []byte) (*userStatus, error) { 39 | var status struct { 40 | User userStatus `json:"user"` 41 | } 42 | if err := json.Unmarshal(b, &status); err != nil { 43 | return nil, err 44 | } 45 | us := status.User 46 | if us.Status != user.StatusOK { 47 | return nil, errors.Errorf("status not ok (%s)", us.Status) 48 | } 49 | 50 | if us.KID != usr.KID { 51 | return nil, errors.Errorf("invalid user kid") 52 | } 53 | 54 | if us.Name != usr.Name { 55 | return nil, errors.Errorf("invalid user name") 56 | } 57 | 58 | if us.Service != usr.Service { 59 | return nil, errors.Errorf("invalid user service") 60 | } 61 | 62 | if us.Seq != usr.Seq { 63 | return nil, errors.Errorf("invalid user seq") 64 | } 65 | 66 | if us.URL != usr.URL { 67 | return nil, errors.Errorf("invalid user url") 68 | } 69 | 70 | return &us, nil 71 | } 72 | 73 | type userStatus struct { 74 | ID string `json:"id,omitempty"` 75 | Name string `json:"name,omitempty"` 76 | KID keys.ID `json:"kid,omitempty"` 77 | Seq int `json:"seq,omitempty"` 78 | Service string `json:"service,omitempty"` 79 | URL string `json:"url,omitempty"` 80 | Status user.Status `json:"status,omitempty"` 81 | Statement string `json:"statement,omitempty"` 82 | VerifiedAt int64 `json:"verifiedAt,omitempty"` 83 | Timestamp int64 `json:"ts,omitempty"` 84 | MatchField string `json:"mf,omitempty"` 85 | Err string `json:"err,omitempty"` 86 | } 87 | -------------------------------------------------------------------------------- /user/services/keyspub_test.go: -------------------------------------------------------------------------------- 1 | package services_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/keys-pub/keys" 8 | "github.com/keys-pub/keys/http" 9 | "github.com/keys-pub/keys/user" 10 | "github.com/keys-pub/keys/user/services" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestKeysPub(t *testing.T) { 15 | // user.SetLogger(user.NewLogger(user.DebugLevel)) 16 | // services.SetLogger(user.NewLogger(user.DebugLevel)) 17 | 18 | kid := keys.ID("kex1mnseg28xu6g3j4wur7hqwk8ag3fu3pmr2t5lync26xmgff0dtryqupf80c") 19 | urs := "https://gist.github.com/gabriel/ceea0f3b675bac03425472692273cf52" 20 | 21 | usr, err := user.New(kid, "github", "gabriel", urs, 1) 22 | require.NoError(t, err) 23 | 24 | client := http.NewClient() 25 | 26 | result := services.Verify(context.TODO(), services.KeysPub, client, usr) 27 | require.Equal(t, user.StatusOK, result.Status) 28 | expected := `BEGIN MESSAGE. 29 | kdZaJI1U5AS7G6i VoUxdP8OtPzEoM6 pYhVl0YQZJnotVE wLg9BDb5SUO05pm 30 | abUSeCvBfdPoRpP J8wrcF5PP3wTCKq 6Xr2MZHgg6m2Qal gJCD6vMqlBQfIg6 31 | QsfB27aP5DMuXlJ AUVIAvMDHIoptmS riNMzfpwBjRShVL WH70a0GOEqD6L8b 32 | kC5EFOwCedvHFpc AQVqULHjcSpeCfZ EIOaQ2IP. 33 | END MESSAGE.` 34 | require.Equal(t, expected, result.Statement) 35 | } 36 | -------------------------------------------------------------------------------- /user/services/proxy.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/keys-pub/keys/http" 8 | "github.com/keys-pub/keys/user" 9 | "github.com/keys-pub/keys/user/validate" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | type proxy struct{} 14 | 15 | // Proxy uses keys.pub user cache instead of the service directly. 16 | var Proxy = &proxy{} 17 | 18 | func (s *proxy) Request(ctx context.Context, client http.Client, usr *user.User) (user.Status, []byte, error) { 19 | if usr.Service != "twitter" { 20 | return user.StatusFailure, nil, errors.Errorf("invalid service") 21 | } 22 | name, id, err := validate.Twitter.NameStatusForURL(usr.URL) 23 | if err != nil { 24 | return user.StatusFailure, nil, errors.Errorf("invalid url") 25 | } 26 | 27 | url := fmt.Sprintf("https://keys.pub/twitter/%s/%s/%s", usr.KID, name, id) 28 | return Request(ctx, client, url, nil) 29 | } 30 | 31 | func (s *proxy) Verify(ctx context.Context, b []byte, usr *user.User) (user.Status, *Verified, error) { 32 | status, statement, err := user.FindVerify(usr, b, false) 33 | if err != nil { 34 | return status, nil, err 35 | } 36 | return status, &Verified{Statement: statement, Proxied: true}, nil 37 | } 38 | -------------------------------------------------------------------------------- /user/services/proxy_test.go: -------------------------------------------------------------------------------- 1 | package services_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/keys-pub/keys" 8 | "github.com/keys-pub/keys/http" 9 | "github.com/keys-pub/keys/user" 10 | "github.com/keys-pub/keys/user/services" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestProxy(t *testing.T) { 15 | // user.SetLogger(user.NewLogger(user.DebugLevel)) 16 | // services.SetLogger(user.NewLogger(user.DebugLevel)) 17 | 18 | kid := keys.ID("kex1e26rq9vrhjzyxhep0c5ly6rudq7m2cexjlkgknl2z4lqf8ga3uasz3s48m") 19 | urs := "https://twitter.com/gabrlh/status/1222706272849391616" 20 | 21 | usr, err := user.New(kid, "twitter", "gabrlh", urs, 1) 22 | require.NoError(t, err) 23 | client := http.NewClient() 24 | result := services.Verify(context.TODO(), services.Proxy, client, usr) 25 | require.Equal(t, user.StatusOK, result.Status) 26 | expected := "BEGIN MESSAGE.\nEqcgDt8RfXvPq9b 4qCV8S3VPKIQKqa N7Rc1YruQQYuVS8 niHzUv7jdykkEPSrKGcJQCNTkNE7uF swPuwfpaZX6TCKq 6Xr2MZHgg6S0Mjg WFMJ1KHxazTuXs4icK3k8SZCR8mVLQ MSVhFeMrvz0qJOm A96zW9RAY6whsLo 5fC8i3fRJjyo9mQJZid8MwBXJl1XDL 5ZOSkLYs6sk6a2g CiGyA2IP.\nEND MESSAGE." 27 | require.Equal(t, expected, result.Statement) 28 | } 29 | -------------------------------------------------------------------------------- /user/services/reddit_test.go: -------------------------------------------------------------------------------- 1 | package services_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/keys-pub/keys" 8 | "github.com/keys-pub/keys/http" 9 | "github.com/keys-pub/keys/user" 10 | "github.com/keys-pub/keys/user/services" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestReddit(t *testing.T) { 15 | // user.SetLogger(user.NewLogger(user.DebugLevel)) 16 | // services.SetLogger(user.NewLogger(user.DebugLevel)) 17 | 18 | kid := keys.ID("kex164gsfjpcfcugtcv28hmv5jl8yl7nzs06l09aw2245phy06j7ygqs9u9zyd") 19 | 20 | usr, err := user.New(kid, "reddit", "gabrlh", "https://www.reddit.com/user/gabrlh/comments/ogdh94/keyspub/", 1) 21 | require.NoError(t, err) 22 | client := http.NewClient() 23 | result := services.Verify(context.TODO(), services.Reddit, client, usr) 24 | require.Equal(t, user.StatusOK, result.Status) 25 | expected := `BEGIN MESSAGE. 26 | tm8882H30GKybLj cOvOw3ezalNCV4z HIeF7ZIDa53DM5l m43v3AdpuM5xtqTZDGIhyQbA863bYk fiIRdpUYVzMTCKq 6Xr2MZHgg4bh2Wj m5fbDX2FnO9rt6TWzS6zMQo6Pf4PXS De2cdyxT0J3mPah X4cThM1A4yFIFaF lo99DSnDd3LOLwUrP9mdKCnNdvKkl1 WLZZaBlQZWXAisM CCwny21. 27 | END MESSAGE.` 28 | require.Equal(t, expected, result.Statement) 29 | } 30 | -------------------------------------------------------------------------------- /user/services/request.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/keys-pub/keys/http" 7 | "github.com/keys-pub/keys/user" 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // Request resource. 12 | func Request(ctx context.Context, client http.Client, urs string, headers []http.Header) (user.Status, []byte, error) { 13 | logger.Infof("Requesting %s", urs) 14 | req, err := http.NewRequest("GET", urs, nil) 15 | if err != nil { 16 | return user.StatusFailure, nil, err 17 | } 18 | for _, h := range headers { 19 | req.Header.Set(h.Name, h.Value) 20 | } 21 | b, err := client.Request(ctx, req) 22 | if err != nil { 23 | if errHTTP, ok := errors.Cause(err).(http.Err); ok && errHTTP.Code == 404 { 24 | return user.StatusResourceNotFound, nil, errors.Errorf("resource not found") 25 | } 26 | return user.StatusConnFailure, nil, err 27 | } 28 | return user.StatusOK, b, nil 29 | } 30 | -------------------------------------------------------------------------------- /user/services/service.go: -------------------------------------------------------------------------------- 1 | // Package services defines services capable of linking a key to a user. 2 | package services 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/keys-pub/keys/http" 8 | "github.com/keys-pub/keys/user" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // Verified results. 13 | type Verified struct { 14 | Statement string 15 | Timestamp int64 16 | Proxied bool 17 | } 18 | 19 | // Service describes a user service. 20 | type Service interface { 21 | // Request resource with client. 22 | Request(ctx context.Context, client http.Client, usr *user.User) (user.Status, []byte, error) 23 | 24 | // Verify content. 25 | Verify(ctx context.Context, b []byte, usr *user.User) (user.Status, *Verified, error) 26 | } 27 | 28 | var services = map[string]Service{ 29 | "twitter": Twitter, 30 | "github": Github, 31 | "reddit": Reddit, 32 | "https": HTTPS, 33 | "echo": Echo, 34 | } 35 | 36 | // Lookup service by name. 37 | func Lookup(service string) (Service, error) { 38 | out, ok := services[service] 39 | if out == nil || !ok { 40 | return nil, errors.Errorf("service not found: %s", service) 41 | } 42 | return out, nil 43 | } 44 | -------------------------------------------------------------------------------- /user/services/service_test.go: -------------------------------------------------------------------------------- 1 | package services_test 2 | -------------------------------------------------------------------------------- /user/services/twitter.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/keys-pub/keys/http" 10 | "github.com/keys-pub/keys/user" 11 | "github.com/keys-pub/keys/user/validate" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | // TwitterID is the id for twitter. 16 | const TwitterID = "twitter" 17 | 18 | type twitter struct { 19 | bearerToken string 20 | } 21 | 22 | // Twitter .. 23 | var Twitter = &twitter{ 24 | bearerToken: os.Getenv("TWITTER_BEARER_TOKEN"), 25 | } 26 | 27 | // SetBearerToken for auth. 28 | func (s *twitter) SetBearerToken(bearerToken string) { 29 | s.bearerToken = bearerToken 30 | } 31 | 32 | func (s *twitter) ID() string { 33 | return TwitterID 34 | } 35 | 36 | func (s *twitter) Request(ctx context.Context, client http.Client, usr *user.User) (user.Status, []byte, error) { 37 | apiURL, err := validate.Twitter.APIURL(usr.Name, usr.URL) 38 | if err != nil { 39 | return user.StatusFailure, nil, err 40 | } 41 | headers := s.headers() 42 | return Request(ctx, client, apiURL, headers) 43 | } 44 | 45 | func (s *twitter) Verify(ctx context.Context, b []byte, usr *user.User) (user.Status, *Verified, error) { 46 | var tweet tweet 47 | if err := json.Unmarshal(b, &tweet); err != nil { 48 | return user.StatusContentInvalid, nil, err 49 | } 50 | logger.Debugf("Tweet: %+v", tweet) 51 | 52 | // TODO: Double check tweet id matches 53 | 54 | found := false 55 | authorID := tweet.Data.AuthorID 56 | for _, tweetUser := range tweet.Includes.Users { 57 | if authorID == tweetUser.ID { 58 | tweetUserName := validate.Twitter.NormalizeName(tweetUser.Username) 59 | if tweetUserName != usr.Name { 60 | return user.StatusContentInvalid, nil, errors.Errorf("invalid tweet username %s", tweetUser.Username) 61 | } 62 | found = true 63 | } 64 | } 65 | if !found { 66 | return user.StatusContentInvalid, nil, errors.Errorf("tweet username not found") 67 | } 68 | 69 | msg := tweet.Data.Text 70 | status, statement, err := user.FindVerify(usr, []byte(msg), false) 71 | if err != nil { 72 | return status, nil, err 73 | } 74 | return status, &Verified{Statement: statement}, nil 75 | } 76 | 77 | func (s *twitter) headers() []http.Header { 78 | if s.bearerToken == "" { 79 | return nil 80 | } 81 | return []http.Header{ 82 | { 83 | Name: "Authorization", 84 | Value: fmt.Sprintf("Bearer %s", s.bearerToken), 85 | }, 86 | } 87 | } 88 | 89 | type tweet struct { 90 | Data struct { 91 | ID string `json:"id"` 92 | Text string `json:"text"` 93 | AuthorID string `json:"author_id"` 94 | } `json:"data"` 95 | Includes struct { 96 | Users []struct { 97 | ID string `json:"id"` 98 | Name string `json:"name"` 99 | Username string `json:"username"` 100 | } `json:"users"` 101 | } `json:"includes"` 102 | } 103 | -------------------------------------------------------------------------------- /user/services/twitter_test.go: -------------------------------------------------------------------------------- 1 | package services_test 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "testing" 7 | 8 | "github.com/keys-pub/keys" 9 | "github.com/keys-pub/keys/http" 10 | "github.com/keys-pub/keys/user" 11 | "github.com/keys-pub/keys/user/services" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | // TODO: more tests are in users package 16 | 17 | func TestTwitterNewUserForSigning(t *testing.T) { 18 | sk := keys.NewEdX25519KeyFromSeed(testSeed(0x01)) 19 | 20 | usr, err := user.NewForSigning(sk.ID(), "twitter", "123456789012345") 21 | require.NoError(t, err) 22 | msg, err := usr.Sign(sk) 23 | require.NoError(t, err) 24 | expected := `BEGIN MESSAGE. 25 | GaZybOsIjCQ9nU5 QoXI1pS28UWypBb HHSXegeFk1M6huT W5rwWMtO4Gcx4u3 26 | Gjbya7YnsVfnAVz xvTtqmINcMmTCKq 6Xr2MZHgg4UNRDb Zy2loGoGN3Mvxd4 27 | r7FIwpZOJPE1JEq D2gGjkgLByR9CFG 2aCgRgZZwl5UAa4 6bmBzjEOhmsiW0K 28 | TDXulMojfPebRMl JBdGc81U8wUvF0I 1LUOo5fLogY3MDW UqhLx. 29 | END MESSAGE.` 30 | require.Equal(t, expected, msg) 31 | require.False(t, len(msg) > 280) 32 | require.Equal(t, 274, len(msg)) 33 | 34 | err = usr.Verify(msg) 35 | require.NoError(t, err) 36 | } 37 | 38 | func TestTwitter(t *testing.T) { 39 | // Requires twitter bearer token configured 40 | if os.Getenv("TWITTER_BEARER_TOKEN") == "" { 41 | t.Skip("no bearer token") 42 | } 43 | // user.SetLogger(user.NewLogger(user.DebugLevel)) 44 | // services.SetLogger(user.NewLogger(user.DebugLevel)) 45 | 46 | kid := keys.ID("kex1e26rq9vrhjzyxhep0c5ly6rudq7m2cexjlkgknl2z4lqf8ga3uasz3s48m") 47 | urs := "https://twitter.com/gabrlh/status/1222706272849391616" 48 | 49 | usr, err := user.New(kid, "twitter", "gabrlh", urs, 1) 50 | require.NoError(t, err) 51 | client := http.NewClient() 52 | result := services.Verify(context.TODO(), services.Twitter, client, usr) 53 | require.Equal(t, user.StatusOK, result.Status) 54 | // TODO: Require msg 55 | } 56 | 57 | func TestTwitterBoboloblaws(t *testing.T) { 58 | // Requires twitter bearer token configured 59 | if os.Getenv("TWITTER_BEARER_TOKEN") == "" { 60 | t.Skip("no bearer token") 61 | } 62 | // user.SetLogger(user.NewLogger(user.DebugLevel)) 63 | // services.SetLogger(user.NewLogger(user.DebugLevel)) 64 | 65 | kid := keys.ID("kex109x2xh6tle8yls3quqpu9xuhlzffr9fakcv4ymc52cvq366qwnpqyyydgz") 66 | urs := "https://twitter.com/boboloblaws/status/1308508138652315648" 67 | 68 | usr, err := user.New(kid, "twitter", "boboloblaws", urs, 1) 69 | require.NoError(t, err) 70 | client := http.NewClient() 71 | result := services.Verify(context.TODO(), services.Twitter, client, usr) 72 | require.Equal(t, user.StatusOK, result.Status) 73 | // TODO: Require msg 74 | } 75 | -------------------------------------------------------------------------------- /user/services/verify.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/keys-pub/keys/http" 8 | "github.com/keys-pub/keys/tsutil" 9 | "github.com/keys-pub/keys/user" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | // Verify a user. 14 | // The result.Status is success (StatusOK) or type of failure. 15 | // If a failure, result.Err has the error message. 16 | func Verify(ctx context.Context, service Service, client http.Client, usr *user.User) *user.Result { 17 | result := &user.Result{User: usr} 18 | UpdateResult(ctx, service, result, client, time.Now()) 19 | return result 20 | } 21 | 22 | // UpdateResult updates a user.Result. 23 | // The result.Status is success (StatusOK) or type of failure. 24 | // If a failure, result.Err has the error message. 25 | func UpdateResult(ctx context.Context, service Service, result *user.Result, client http.Client, now time.Time) { 26 | logger.Infof("Update user %s", result.User.String()) 27 | 28 | result.Timestamp = tsutil.Millis(now) 29 | status, verified, err := requestVerify(ctx, service, client, result.User) 30 | if err != nil { 31 | result.Err = err.Error() 32 | result.Status = status 33 | result.Statement = "" 34 | return 35 | } 36 | 37 | logger.Infof("Verified %s", result.User.KID) 38 | result.Err = "" 39 | result.Status = status 40 | result.Statement = verified.Statement 41 | result.Proxied = verified.Proxied 42 | if verified.Timestamp != 0 { 43 | result.VerifiedAt = verified.Timestamp 44 | } else { 45 | result.VerifiedAt = tsutil.Millis(now) 46 | } 47 | } 48 | 49 | // requestVerify get user URL using client and verifies it. 50 | // If there is an error, it is set on the result. 51 | func requestVerify(ctx context.Context, service Service, client http.Client, usr *user.User) (user.Status, *Verified, error) { 52 | st, body, err := service.Request(ctx, client, usr) 53 | if err != nil { 54 | return st, nil, err 55 | } 56 | if st != user.StatusOK { 57 | return st, nil, err 58 | } 59 | if body == nil { 60 | return user.StatusResourceNotFound, nil, errors.Errorf("resource not found") 61 | } 62 | 63 | st, msg, err := service.Verify(ctx, body, usr) 64 | if err != nil { 65 | logger.Warningf("Failed to check content: %s", err) 66 | return st, nil, err 67 | } 68 | 69 | return st, msg, nil 70 | } 71 | -------------------------------------------------------------------------------- /user/services/verify_test.go: -------------------------------------------------------------------------------- 1 | package services_test 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/keys-pub/keys" 7 | ) 8 | 9 | func testSeed(b byte) *[32]byte { 10 | return keys.Bytes32(bytes.Repeat([]byte{b}, 32)) 11 | } 12 | -------------------------------------------------------------------------------- /user/validate/echo.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type echo struct{} 11 | 12 | // Echo service. 13 | var Echo = &echo{} 14 | 15 | func (s *echo) ID() string { 16 | return "echo" 17 | } 18 | 19 | func (s *echo) NormalizeName(name string) string { 20 | name = strings.ToLower(name) 21 | return name 22 | } 23 | 24 | func (s *echo) ValidateName(name string) error { 25 | ok := isAlphaNumericWithDashUnderscore(name) 26 | if !ok { 27 | return errors.Errorf("name has an invalid character") 28 | } 29 | 30 | if len(name) > 39 { 31 | return errors.Errorf("test name is too long, it must be less than 40 characters") 32 | } 33 | 34 | return nil 35 | } 36 | 37 | func (s *echo) NormalizeURL(name string, urs string) (string, error) { 38 | return basicURLString(urs) 39 | } 40 | 41 | func (s *echo) ValidateURL(name string, urs string) error { 42 | u, err := url.Parse(urs) 43 | if err != nil { 44 | return err 45 | } 46 | if u.Scheme != "test" { 47 | return errors.Errorf("invalid scheme for url %s", u) 48 | } 49 | if u.Host != "echo" { 50 | return errors.Errorf("invalid host for url %s", u) 51 | } 52 | path := u.Path 53 | path = strings.TrimPrefix(path, "/") 54 | paths := strings.Split(path, "/") 55 | if len(paths) == 0 { 56 | return errors.Errorf("path invalid %s for url %s", paths, u) 57 | } 58 | if paths[0] != name { 59 | return errors.Errorf("path invalid (name mismatch) %s != %s", paths[0], name) 60 | } 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /user/validate/echo_test.go: -------------------------------------------------------------------------------- 1 | package validate_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keys-pub/keys/user/validate" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestEchoNormalizeName(t *testing.T) { 11 | echo := validate.Echo 12 | name := echo.NormalizeName("Gabriel") 13 | require.Equal(t, "gabriel", name) 14 | } 15 | 16 | func TestEchoValidateName(t *testing.T) { 17 | echo := validate.Echo 18 | err := echo.ValidateName("gabriel01") 19 | require.NoError(t, err) 20 | 21 | err = echo.ValidateName("gabriel-01") 22 | require.NoError(t, err) 23 | 24 | err = echo.ValidateName("gabriel_01") 25 | require.NoError(t, err) 26 | 27 | err = echo.ValidateName("Gabriel") 28 | require.EqualError(t, err, "name has an invalid character") 29 | 30 | err = echo.ValidateName("Gabriel++") 31 | require.EqualError(t, err, "name has an invalid character") 32 | 33 | err = echo.ValidateName("reallylongnamereallylongnamereallylongnamereallylongnamereallylongnamereallylongname") 34 | require.EqualError(t, err, "test name is too long, it must be less than 40 characters") 35 | } 36 | 37 | func TestEchoNormalizeURL(t *testing.T) { 38 | echo := validate.Echo 39 | testNormalizeURL(t, echo, 40 | "gabriel", 41 | "test://echo/gabriel?", 42 | "test://echo/gabriel") 43 | } 44 | 45 | func TestEchoValidateURL(t *testing.T) { 46 | echo := validate.Echo 47 | testValidateURL(t, echo, 48 | "gabriel", 49 | "test://echo/gabriel") 50 | 51 | testValidateURLErr(t, echo, 52 | "gabriel", 53 | "test://ech/gabriel", 54 | "invalid host for url test://ech/gabriel") 55 | 56 | testValidateURLErr(t, echo, 57 | "gabriel", 58 | "test://echo/gabrie", 59 | "path invalid (name mismatch) gabrie != gabriel") 60 | } 61 | -------------------------------------------------------------------------------- /user/validate/github.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // GithubID is id for github. 11 | const GithubID = "github" 12 | 13 | type github struct{} 14 | 15 | // Github service. 16 | var Github = &github{} 17 | 18 | func (s *github) ID() string { 19 | return GithubID 20 | } 21 | 22 | func (s *github) NormalizeName(name string) string { 23 | name = strings.ToLower(name) 24 | return name 25 | } 26 | 27 | func (s *github) ValidateName(name string) error { 28 | ok := isAlphaNumericWithDash(name) 29 | if !ok { 30 | return errors.Errorf("name has an invalid character") 31 | } 32 | 33 | if len(name) > 39 { 34 | return errors.Errorf("github name is too long, it must be less than 40 characters") 35 | } 36 | 37 | return nil 38 | } 39 | 40 | func (s *github) NormalizeURL(name string, urs string) (string, error) { 41 | return basicURLString(strings.ToLower(urs)) 42 | } 43 | 44 | func (s *github) ValidateURL(name string, urs string) error { 45 | _, err := s.APIURL(name, urs) 46 | return err 47 | } 48 | 49 | func (s *github) APIURL(name string, urs string) (string, error) { 50 | u, err := url.Parse(urs) 51 | if err != nil { 52 | return "", err 53 | } 54 | if u.Scheme != "https" { 55 | return "", errors.Errorf("invalid scheme for url %s", u) 56 | } 57 | if u.Host != "gist.github.com" { 58 | return "", errors.Errorf("invalid host for url %s", u) 59 | } 60 | path := u.Path 61 | path = strings.TrimPrefix(path, "/") 62 | paths := strings.Split(path, "/") 63 | if len(paths) != 2 { 64 | return "", errors.Errorf("path invalid %s for url %s", paths, u) 65 | } 66 | if paths[0] != name { 67 | return "", errors.Errorf("path invalid (name mismatch) %s != %s", paths[0], name) 68 | } 69 | id := paths[1] 70 | api := "https://api.github.com/gists/" + id 71 | return api, nil 72 | } 73 | -------------------------------------------------------------------------------- /user/validate/github_test.go: -------------------------------------------------------------------------------- 1 | package validate_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keys-pub/keys/user/validate" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestGithubNormalizeName(t *testing.T) { 11 | github := validate.Github 12 | name := github.NormalizeName("Gabriel") 13 | require.Equal(t, "gabriel", name) 14 | } 15 | 16 | func TestGithubValidateName(t *testing.T) { 17 | github := validate.Github 18 | err := github.ValidateName("gabriel01") 19 | require.NoError(t, err) 20 | 21 | err = github.ValidateName("gabriel-01") 22 | require.NoError(t, err) 23 | 24 | err = github.ValidateName("gabriel_01") 25 | require.EqualError(t, err, "name has an invalid character") 26 | 27 | err = github.ValidateName("Gabriel") 28 | require.EqualError(t, err, "name has an invalid character") 29 | 30 | err = github.ValidateName("Gabriel++") 31 | require.EqualError(t, err, "name has an invalid character") 32 | 33 | err = github.ValidateName("reallylongnamereallylongnamereallylongnamereallylongnamereallylongnamereallylongname") 34 | require.EqualError(t, err, "github name is too long, it must be less than 40 characters") 35 | } 36 | 37 | func TestGithubNormalizeURL(t *testing.T) { 38 | github := validate.Github 39 | testNormalizeURL(t, github, 40 | "gabriel", 41 | "https://gist.github.com/gabriel/abcd?", 42 | "https://gist.github.com/gabriel/abcd") 43 | 44 | testNormalizeURL(t, github, 45 | "gabriel", 46 | "https://gist.github.com/Gabriel/abcd", 47 | "https://gist.github.com/gabriel/abcd") 48 | } 49 | 50 | func TestGithubValidateURL(t *testing.T) { 51 | github := validate.Github 52 | testValidateURL(t, github, 53 | "gabriel", 54 | "https://gist.github.com/gabriel/abcd") 55 | 56 | testValidateURLErr(t, github, 57 | "gabriel", 58 | "https://gist.github.com/gabriel", 59 | "path invalid [gabriel] for url https://gist.github.com/gabriel") 60 | 61 | testValidateURLErr(t, github, 62 | "gabriel", 63 | "https://gis.github.com/gabriel/abcd", 64 | "invalid host for url https://gis.github.com/gabriel/abcd") 65 | 66 | testValidateURLErr(t, github, 67 | "gabriel", 68 | "https://gist.github.com/gabrie/abcd", 69 | "path invalid (name mismatch) gabrie != gabriel") 70 | } 71 | -------------------------------------------------------------------------------- /user/validate/https.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/keys-pub/keys/encoding" 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | type https struct{} 13 | 14 | // HTTPS service. 15 | var HTTPS = &https{} 16 | 17 | func (s *https) ID() string { 18 | return "https" 19 | } 20 | 21 | func (s *https) NormalizeName(name string) string { 22 | name = strings.ToLower(name) 23 | return name 24 | } 25 | 26 | func (s *https) ValidateName(name string) error { 27 | isASCII := encoding.IsASCII([]byte(name)) 28 | if !isASCII { 29 | return errors.Errorf("name has non-ASCII characters") 30 | } 31 | hu := encoding.HasUpper(name) 32 | if hu { 33 | return errors.Errorf("name should be lowercase") 34 | } 35 | if len(name) > 256 { 36 | return errors.Errorf("name is too long") 37 | } 38 | 39 | if !isValidHostname(name) { 40 | return errors.Errorf("not a valid domain name") 41 | } 42 | 43 | if regexIP.MatchString(name) { 44 | return errors.Errorf("not a valid domain name") 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func (s *https) NormalizeURL(name string, urs string) (string, error) { 51 | return basicURLString(strings.ToLower(urs)) 52 | } 53 | 54 | func (s *https) ValidateURL(name string, urs string) error { 55 | if err := s.ValidateName(name); err != nil { 56 | return errors.Wrapf(err, "invalid url") 57 | } 58 | 59 | matches := []string{ 60 | fmt.Sprintf("https://%s/keyspub.txt", name), 61 | fmt.Sprintf("https://%s/.well-known/keyspub.txt", name), 62 | } 63 | 64 | for _, m := range matches { 65 | if urs == m { 66 | return nil 67 | } 68 | } 69 | 70 | return errors.Errorf("invalid url: %s", urs) 71 | } 72 | 73 | var regexIP = regexp.MustCompile(`^[0-9].*\.[0-9].*\.[0-9].*\.[0-9].*$`) 74 | 75 | // From github.com/keybase/client/libkb/util.go 76 | // Found regex here: http://stackoverflow.com/questions/106179/regular-expression-to-match-dns-hostname-or-ip-address 77 | var regexHostname = regexp.MustCompile("^(?i:[a-z0-9]|[a-z0-9][a-z0-9-]*[a-z0-9])$") 78 | 79 | func isValidHostname(s string) bool { 80 | parts := strings.Split(s, ".") 81 | if len(parts) < 2 { 82 | return false 83 | } 84 | for _, p := range parts { 85 | if !regexHostname.MatchString(p) { 86 | return false 87 | } 88 | } 89 | // TLDs must be >=2 chars 90 | return len(parts[len(parts)-1]) >= 2 91 | } 92 | -------------------------------------------------------------------------------- /user/validate/reddit.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type reddit struct{} 11 | 12 | // Reddit service. 13 | var Reddit = &reddit{} 14 | 15 | func (s *reddit) ID() string { 16 | return "reddit" 17 | } 18 | 19 | func (s *reddit) NormalizeName(name string) string { 20 | name = strings.ToLower(name) 21 | return name 22 | } 23 | 24 | func (s *reddit) ValidateName(name string) error { 25 | ok := isAlphaNumericWithDashUnderscore(name) 26 | if !ok { 27 | return errors.Errorf("name has an invalid character") 28 | } 29 | if len(name) > 20 { 30 | return errors.Errorf("reddit name is too long, it must be less than 21 characters") 31 | } 32 | return nil 33 | } 34 | 35 | func (s *reddit) NormalizeURL(name string, urs string) (string, error) { 36 | return basicURLString(strings.ToLower(urs)) 37 | } 38 | 39 | func (s *reddit) ValidateURL(name string, urs string) error { 40 | _, err := s.APIURL(name, urs) 41 | return err 42 | } 43 | 44 | func (s *reddit) APIURL(name string, urs string) (string, error) { 45 | u, err := url.Parse(urs) 46 | if err != nil { 47 | return "", err 48 | } 49 | if u.Scheme != "https" { 50 | return "", errors.Errorf("invalid scheme for url %s", u) 51 | } 52 | switch u.Host { 53 | case "reddit.com", "old.reddit.com", "www.reddit.com": 54 | // OK 55 | default: 56 | return "", errors.Errorf("invalid host for url %s", u) 57 | } 58 | path := u.Path 59 | path = strings.TrimPrefix(path, "/") 60 | paths := strings.Split(path, "/") 61 | 62 | // URL from https://reddit.com/user/{username}/comments/{id}/{post} 63 | 64 | prunedName := strings.ReplaceAll(name, "-", "") 65 | 66 | if len(paths) >= 5 && paths[0] == "user" && paths[1] == prunedName && paths[2] == "comments" { 67 | // Request json 68 | ursj, err := url.Parse("https://www.reddit.com" + strings.TrimSuffix(u.Path, "/") + ".json") 69 | if err != nil { 70 | return "", err 71 | } 72 | return ursj.String(), nil 73 | } 74 | 75 | return "", errors.Errorf("invalid path %s", u.Path) 76 | } 77 | -------------------------------------------------------------------------------- /user/validate/reddit_test.go: -------------------------------------------------------------------------------- 1 | package validate_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keys-pub/keys/user/validate" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestRedditNormalizeName(t *testing.T) { 11 | reddit := validate.Reddit 12 | name := reddit.NormalizeName("Gabriel") 13 | require.Equal(t, "gabriel", name) 14 | } 15 | 16 | func TestRedditValidateName(t *testing.T) { 17 | reddit := validate.Reddit 18 | err := reddit.ValidateName("gabriel01") 19 | require.NoError(t, err) 20 | 21 | err = reddit.ValidateName("gabriel_01-") 22 | require.NoError(t, err) 23 | 24 | err = reddit.ValidateName("Gabriel") 25 | require.EqualError(t, err, "name has an invalid character") 26 | 27 | err = reddit.ValidateName("Gabriel++") 28 | require.EqualError(t, err, "name has an invalid character") 29 | 30 | err = reddit.ValidateName("reallylongnamereallylongnamereallylongnamereallylongnamereallylongnamereallylongname") 31 | require.EqualError(t, err, "reddit name is too long, it must be less than 21 characters") 32 | } 33 | 34 | func TestRedditNormalizeURL(t *testing.T) { 35 | reddit := validate.Reddit 36 | testNormalizeURL(t, reddit, 37 | "gabrlh", 38 | "https://reddit.com/r/keyspubmsgs/comments/f8g9vd/gabrlh/?", 39 | "https://reddit.com/r/keyspubmsgs/comments/f8g9vd/gabrlh/") 40 | 41 | testNormalizeURL(t, reddit, 42 | "gabrlh", 43 | "https://reddit.com/r/keyspubmsgs/comments/f8g9vd/Gabrlh/", 44 | "https://reddit.com/r/keyspubmsgs/comments/f8g9vd/gabrlh/") 45 | } 46 | 47 | func TestRedditValidateURL(t *testing.T) { 48 | reddit := validate.Reddit 49 | testValidateURL(t, reddit, 50 | "gabrlh", 51 | "https://www.reddit.com/user/gabrlh/comments/f8g9vd/keyspub/") 52 | 53 | testValidateURL(t, reddit, 54 | "keys-pub", 55 | "https://www.reddit.com/user/keyspub/comments/f8g9vd/keyspub/") 56 | 57 | testValidateURL(t, reddit, 58 | "gabrlh", 59 | "https://old.reddit.com/user/gabrlh/comments/f8g9vd/keyspub/") 60 | 61 | testValidateURL(t, reddit, 62 | "gabrlh", 63 | "https://reddit.com/user/gabrlh/comments/f8g9vd/keyspub/") 64 | 65 | testValidateURL(t, reddit, 66 | "gabrlh", 67 | "https://reddit.com/user/gabrlh/comments/f8g9vd/keyspub?") 68 | 69 | testValidateURL(t, reddit, 70 | "gabrlh", 71 | "https://reddit.com/user/gabrlh/comments/f8g9vd/keyspub/?") 72 | 73 | testValidateURLErr(t, reddit, 74 | "gabrlh", 75 | "https://reddit.com/user/user/comments/f8g9vd/keyspub/?", 76 | "invalid path /user/user/comments/f8g9vd/keyspub/") 77 | 78 | testValidateURLErr(t, reddit, 79 | "gabrlh", 80 | "https://reddit.com/user/subreddit/comments/f8g9vd/keyspub/?", 81 | "invalid path /user/subreddit/comments/f8g9vd/keyspub/") 82 | } 83 | -------------------------------------------------------------------------------- /user/validate/twitter.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // TwitterID is the id for twitter. 11 | const TwitterID = "twitter" 12 | 13 | type twitter struct{} 14 | 15 | // Twitter .. 16 | var Twitter = &twitter{} 17 | 18 | func (s *twitter) ID() string { 19 | return TwitterID 20 | } 21 | 22 | func (s *twitter) NormalizeName(name string) string { 23 | name = strings.ToLower(name) 24 | if len(name) > 0 && name[0] == '@' { 25 | name = name[1:] 26 | } 27 | return name 28 | } 29 | 30 | func (s *twitter) ValidateName(name string) error { 31 | ok := isAlphaNumericWithUnderscore(name) 32 | if !ok { 33 | return errors.Errorf("name has an invalid character") 34 | } 35 | 36 | if len(name) > 15 { 37 | return errors.Errorf("twitter name is too long, it must be less than 16 characters") 38 | } 39 | 40 | return nil 41 | } 42 | 43 | func (s *twitter) NormalizeURL(name string, urs string) (string, error) { 44 | return basicURLString(strings.ToLower(urs)) 45 | } 46 | 47 | func (s *twitter) ValidateURL(name string, urs string) error { 48 | _, err := s.APIURL(name, urs) 49 | return err 50 | } 51 | 52 | func (s *twitter) NameStatusForURL(urs string) (string, string, error) { 53 | u, err := url.Parse(urs) 54 | if err != nil { 55 | return "", "", err 56 | } 57 | if u.Scheme != "https" { 58 | return "", "", errors.Errorf("invalid scheme for url %s", u) 59 | } 60 | switch u.Host { 61 | case "twitter.com", "mobile.twitter.com": 62 | // OK 63 | default: 64 | return "", "", errors.Errorf("invalid host for url %s", u) 65 | } 66 | 67 | path := u.Path 68 | path = strings.TrimPrefix(path, "/") 69 | paths := strings.Split(path, "/") 70 | if len(paths) != 3 { 71 | return "", "", errors.Errorf("path invalid %s for url %s", paths, u) 72 | } 73 | // if paths[0] != name { 74 | // return "", "", errors.Errorf("path invalid (name mismatch) for url %s", u) 75 | // } 76 | 77 | name := paths[0] 78 | status := paths[2] 79 | 80 | return name, status, nil 81 | } 82 | 83 | func (s *twitter) APIURL(name string, urs string) (string, error) { 84 | uname, status, err := s.NameStatusForURL(urs) 85 | if err != nil { 86 | return "", err 87 | } 88 | if uname != name { 89 | return "", errors.Errorf("path invalid (name mismatch) for url %s", urs) 90 | } 91 | return "https://api.twitter.com/2/tweets/" + status + "?expansions=author_id", nil 92 | } 93 | -------------------------------------------------------------------------------- /user/validate/twitter_test.go: -------------------------------------------------------------------------------- 1 | package validate_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keys-pub/keys/user/validate" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestTwitterNormalizeName(t *testing.T) { 11 | twitter := validate.Twitter 12 | name := twitter.NormalizeName("Gabriel") 13 | require.Equal(t, "gabriel", name) 14 | } 15 | 16 | func TestTwitterValidateName(t *testing.T) { 17 | twitter := validate.Twitter 18 | err := twitter.ValidateName("gabriel01") 19 | require.NoError(t, err) 20 | 21 | err = twitter.ValidateName("gabriel_01") 22 | require.NoError(t, err) 23 | 24 | err = twitter.ValidateName("gabriel-01") 25 | require.EqualError(t, err, "name has an invalid character") 26 | 27 | err = twitter.ValidateName("Gabriel") 28 | require.EqualError(t, err, "name has an invalid character") 29 | 30 | err = twitter.ValidateName("Gabriel++") 31 | require.EqualError(t, err, "name has an invalid character") 32 | 33 | err = twitter.ValidateName("reallylongnamereallylongnamereallylongnamereallylongnamereallylongnamereallylongname") 34 | require.EqualError(t, err, "twitter name is too long, it must be less than 16 characters") 35 | } 36 | 37 | func TestTwitterNormalizeURL(t *testing.T) { 38 | twitter := validate.Twitter 39 | testNormalizeURL(t, twitter, 40 | "boboloblaw", 41 | "https://twitter.com/Boboloblaw/status/1250914920146669568?", 42 | "https://twitter.com/boboloblaw/status/1250914920146669568") 43 | 44 | testNormalizeURL(t, twitter, 45 | "boboloblaw", 46 | "https://twitter.com/Boboloblaw/status/1250914920146669568?", 47 | "https://twitter.com/boboloblaw/status/1250914920146669568") 48 | } 49 | 50 | func TestTwitterValidateURL(t *testing.T) { 51 | twitter := validate.Twitter 52 | testValidateURL(t, twitter, 53 | "boboloblaw", 54 | "https://twitter.com/boboloblaw/status/1250914920146669568") 55 | 56 | testValidateURLErr(t, twitter, 57 | "boboloblaw", 58 | "https://twitter.com/bobolobla/status/1250914920146669568", 59 | "path invalid (name mismatch) for url https://twitter.com/bobolobla/status/1250914920146669568") 60 | 61 | testValidateURLErr(t, twitter, 62 | "boboloblaw", 63 | "https://twittter.com/boboloblaw/status/1250914920146669568", 64 | "invalid host for url https://twittter.com/boboloblaw/status/1250914920146669568") 65 | 66 | testValidateURLErr(t, twitter, 67 | "boboloblaw", 68 | "https://twitter.com/boboloblaw/status", 69 | "path invalid [boboloblaw status] for url https://twitter.com/boboloblaw/status") 70 | } 71 | -------------------------------------------------------------------------------- /user/validate/validate_test.go: -------------------------------------------------------------------------------- 1 | package validate_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/keys-pub/keys/user/validate" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func testNormalizeURL(t *testing.T, validator validate.Validator, name string, urs string, expected string) { 11 | out, err := validator.NormalizeURL(name, urs) 12 | require.NoError(t, err) 13 | require.Equal(t, expected, out) 14 | } 15 | 16 | func testValidateURL(t *testing.T, validator validate.Validator, name string, urs string) { 17 | err := validator.ValidateURL(name, urs) 18 | require.NoError(t, err) 19 | } 20 | 21 | func testValidateURLErr(t *testing.T, validator validate.Validator, name string, urs string, expected string) { 22 | err := validator.ValidateURL(name, urs) 23 | require.EqualError(t, err, expected) 24 | } 25 | -------------------------------------------------------------------------------- /user/validate/validator.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "regexp" 7 | 8 | "github.com/pkg/errors" 9 | ) 10 | 11 | // Validator describes a a validator for a user service. 12 | type Validator interface { 13 | // Normalize the service user name. 14 | // For example, on Twitter, "@username" becomes "username" or "Gabriel" 15 | // becomes "gabriel". 16 | NormalizeName(name string) string 17 | 18 | // ValidateName validates the service user name. 19 | ValidateName(name string) error 20 | 21 | // NormalizeURL normalizes an url string. 22 | NormalizeURL(name string, urs string) (string, error) 23 | 24 | // ValidateURL validates the URL string. 25 | ValidateURL(name string, urs string) error 26 | } 27 | 28 | var services = map[string]Validator{ 29 | "twitter": Twitter, 30 | "github": Github, 31 | "reddit": Reddit, 32 | "https": HTTPS, 33 | "echo": Echo, 34 | } 35 | 36 | // Lookup service by name. 37 | func Lookup(service string) (Validator, error) { 38 | out, ok := services[service] 39 | if out == nil || !ok { 40 | return nil, errors.Errorf("service not found: %s", service) 41 | } 42 | return out, nil 43 | } 44 | 45 | var regAlphaNumericWithDash = regexp.MustCompile(`^[a-z0-9-]+$`) 46 | var regAlphaNumericWithUnderscore = regexp.MustCompile(`^[a-z0-9_]+$`) 47 | var regAlphaNumericWithDashUnderscore = regexp.MustCompile(`^[a-z0-9-_]+$`) 48 | 49 | func isAlphaNumericWithDash(s string) bool { 50 | return regAlphaNumericWithDash.MatchString(s) 51 | } 52 | 53 | func isAlphaNumericWithUnderscore(s string) bool { 54 | return regAlphaNumericWithUnderscore.MatchString(s) 55 | } 56 | 57 | func isAlphaNumericWithDashUnderscore(s string) bool { 58 | return regAlphaNumericWithDashUnderscore.MatchString(s) 59 | } 60 | 61 | func basicURLString(urs string) (string, error) { 62 | ur, err := url.Parse(urs) 63 | if err != nil { 64 | return "", err 65 | } 66 | return fmt.Sprintf("%s://%s%s", ur.Scheme, ur.Host, ur.Path), nil 67 | } 68 | -------------------------------------------------------------------------------- /user/verify.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/keys-pub/keys/encoding" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | // FindVerify finds and verifies content in bytes. 11 | func FindVerify(usr *User, b []byte, isHTML bool) (Status, string, error) { 12 | msg, _ := encoding.FindSaltpack(string(b), isHTML) 13 | if msg == "" { 14 | logger.Warningf("User statement content not found") 15 | return StatusContentNotFound, "", errors.Errorf("user signed message content not found") 16 | } 17 | 18 | verifyMsg := fmt.Sprintf("BEGIN MESSAGE.\n%s\nEND MESSAGE.", msg) 19 | if err := usr.Verify(verifyMsg); err != nil { 20 | logger.Warningf("Failed to verify statement: %s", err) 21 | return StatusStatementInvalid, "", err 22 | } 23 | 24 | return StatusOK, verifyMsg, nil 25 | } 26 | -------------------------------------------------------------------------------- /users/options.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | nethttp "net/http" 5 | 6 | "github.com/keys-pub/keys/http" 7 | "github.com/keys-pub/keys/tsutil" 8 | "github.com/keys-pub/keys/user" 9 | "github.com/keys-pub/keys/user/services" 10 | ) 11 | 12 | // Options are options for Users. 13 | type Options struct { 14 | Client http.Client 15 | Clock tsutil.Clock 16 | } 17 | 18 | // Option ... 19 | type Option func(*Options) 20 | 21 | func newOptions(opts ...Option) Options { 22 | var options Options 23 | for _, o := range opts { 24 | o(&options) 25 | } 26 | if options.Client == nil { 27 | options.Client = http.NewClient() 28 | } 29 | if options.Clock == nil { 30 | options.Clock = tsutil.NewClock() 31 | } 32 | return options 33 | } 34 | 35 | // Client to use. 36 | func Client(client http.Client) Option { 37 | return func(o *Options) { 38 | o.Client = client 39 | } 40 | } 41 | 42 | // HTTPClient to use. 43 | func HTTPClient(nhc *nethttp.Client) Option { 44 | client := http.NewClient(http.WithHTTPClient(nhc)) 45 | return func(o *Options) { 46 | o.Client = client 47 | } 48 | } 49 | 50 | // Clock to use. 51 | func Clock(clock tsutil.Clock) Option { 52 | return func(o *Options) { 53 | o.Clock = clock 54 | } 55 | } 56 | 57 | // ServiceLookupFn for custom service lookup. 58 | type ServiceLookupFn func(usr *user.User) services.Service 59 | 60 | // UpdateOptions ... 61 | type UpdateOptions struct { 62 | // Specify the service to use for the check. 63 | // For twitter proxy, use services.Proxy. 64 | Service ServiceLookupFn 65 | } 66 | 67 | // UpdateOption ... 68 | type UpdateOption func(*UpdateOptions) 69 | 70 | func newUpdateOptions(opts ...UpdateOption) UpdateOptions { 71 | var options UpdateOptions 72 | for _, o := range opts { 73 | o(&options) 74 | } 75 | return options 76 | } 77 | 78 | // UseService option. 79 | func UseService(service ServiceLookupFn) UpdateOption { 80 | return func(o *UpdateOptions) { 81 | o.Service = service 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /users/reddit_test.go: -------------------------------------------------------------------------------- 1 | package users_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/keys-pub/keys" 8 | "github.com/keys-pub/keys/dstore" 9 | "github.com/keys-pub/keys/http" 10 | "github.com/keys-pub/keys/tsutil" 11 | "github.com/keys-pub/keys/user" 12 | "github.com/keys-pub/keys/users" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestResultReddit(t *testing.T) { 17 | sk := keys.NewEdX25519KeyFromSeed(testSeed(0x01)) 18 | 19 | clock := tsutil.NewTestClock() 20 | ds := dstore.NewMem() 21 | scs := keys.NewSigchains(ds) 22 | usrs := users.New(ds, scs, users.Clock(clock)) 23 | 24 | usr, err := user.NewForSigning(sk.ID(), "reddit", "charlie") 25 | require.NoError(t, err) 26 | msg, err := usr.Sign(sk) 27 | require.NoError(t, err) 28 | t.Logf(msg) 29 | 30 | sc := keys.NewSigchain(sk.ID()) 31 | stu, err := user.New(sk.ID(), "reddit", "charlie", "https://www.reddit.com/user/charlie/comments/ogdh94/keyspub.json", sc.LastSeq()+1) 32 | require.NoError(t, err) 33 | st, err := user.NewSigchainStatement(sc, stu, sk, clock.Now()) 34 | require.NoError(t, err) 35 | err = sc.Add(st) 36 | require.NoError(t, err) 37 | err = scs.Save(sc) 38 | require.NoError(t, err) 39 | 40 | _, err = user.NewSigchainStatement(sc, stu, sk, clock.Now()) 41 | require.EqualError(t, err, "user set in sigchain already") 42 | 43 | usrs.Client().SetProxy("", func(ctx context.Context, req *http.Request) http.ProxyResponse { 44 | return http.ProxyResponse{Body: testdata(t, "testdata/reddit/charlie.json")} 45 | }) 46 | 47 | result, err := usrs.Update(context.TODO(), sk.ID()) 48 | require.NoError(t, err) 49 | require.NotNil(t, result) 50 | require.NotNil(t, result.User) 51 | require.Equal(t, user.StatusOK, result.Status) 52 | require.Equal(t, "reddit", result.User.Service) 53 | require.Equal(t, "charlie", result.User.Name) 54 | require.Equal(t, int64(1234567890003), result.VerifiedAt) 55 | require.Equal(t, int64(1234567890003), result.Timestamp) 56 | 57 | result, err = usrs.Get(context.TODO(), sk.ID()) 58 | require.NoError(t, err) 59 | require.NotNil(t, result) 60 | require.Equal(t, "reddit", result.User.Service) 61 | require.Equal(t, "charlie", result.User.Name) 62 | 63 | result, err = usrs.User(context.TODO(), "charlie@reddit") 64 | require.NoError(t, err) 65 | require.NotNil(t, result) 66 | require.Equal(t, "reddit", result.User.Service) 67 | require.Equal(t, "charlie", result.User.Name) 68 | 69 | kids, err := usrs.KIDs(context.TODO()) 70 | require.NoError(t, err) 71 | require.Equal(t, 1, len(kids)) 72 | require.Equal(t, keys.ID("kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077"), kids[0]) 73 | 74 | res, err := usrs.Search(context.TODO(), &users.SearchRequest{Query: "charlie"}) 75 | require.NoError(t, err) 76 | require.Equal(t, 1, len(res)) 77 | require.Equal(t, keys.ID("kex132yw8ht5p8cetl2jmvknewjawt9xwzdlrk2pyxlnwjyqrdq0dawqqph077"), res[0].KID) 78 | } 79 | -------------------------------------------------------------------------------- /users/search.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/keys-pub/keys" 8 | "github.com/keys-pub/keys/dstore" 9 | "github.com/keys-pub/keys/user" 10 | ) 11 | 12 | // SearchRequest ... 13 | type SearchRequest struct { 14 | // Query to search for. 15 | Query string 16 | // Limit number of results. 17 | Limit int 18 | } 19 | 20 | // SearchResult ... 21 | type SearchResult struct { 22 | KID keys.ID 23 | Result *user.Result 24 | // Field we matched on (if not the user). 25 | Field string 26 | } 27 | 28 | func (u *Users) searchUsers(ctx context.Context, query string, limit int) ([]*SearchResult, error) { 29 | logger.Infof("Searching users %q", query) 30 | iter, err := u.ds.DocumentIterator(ctx, indexSearch, dstore.Prefix(query)) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | results := make([]*SearchResult, 0, limit) 36 | for { 37 | doc, err := iter.Next() 38 | if err != nil { 39 | return nil, err 40 | } 41 | if doc == nil { 42 | break 43 | } 44 | if len(results) >= limit { 45 | break 46 | } 47 | var keyDoc keyDocument 48 | if err := json.Unmarshal(doc.Data(), &keyDoc); err != nil { 49 | return nil, err 50 | } 51 | 52 | results = append(results, &SearchResult{ 53 | KID: keyDoc.KID, 54 | Result: keyDoc.Result, 55 | }) 56 | } 57 | iter.Release() 58 | logger.Infof("Found %d user results", len(results)) 59 | return results, nil 60 | } 61 | 62 | // Search for users. 63 | func (u *Users) Search(ctx context.Context, req *SearchRequest) ([]*SearchResult, error) { 64 | logger.Infof("Search users, query=%q, limit=%d", req.Query, req.Limit) 65 | limit := req.Limit 66 | if limit == 0 { 67 | limit = 100 68 | } 69 | 70 | // Check if query is for a key identifier. 71 | kid, err := keys.ParseID(req.Query) 72 | if err == nil { 73 | res, err := u.Find(ctx, kid) 74 | if err != nil { 75 | return nil, err 76 | } 77 | if res != nil { 78 | return []*SearchResult{&SearchResult{ 79 | KID: kid, 80 | Result: res, 81 | Field: "kid", 82 | }}, nil 83 | } 84 | } 85 | 86 | res, err := u.searchUsers(ctx, req.Query, limit) 87 | if err != nil { 88 | return nil, err 89 | } 90 | return res, nil 91 | } 92 | -------------------------------------------------------------------------------- /users/services.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "github.com/keys-pub/keys/user" 5 | "github.com/keys-pub/keys/user/services" 6 | ) 7 | 8 | // LookupService finds service. 9 | func LookupService(usr *user.User, opt ...UpdateOption) (services.Service, error) { 10 | opts := newUpdateOptions(opt...) 11 | 12 | if opts.Service != nil { 13 | service := opts.Service(usr) 14 | if service != nil { 15 | return service, nil 16 | } 17 | } 18 | 19 | return services.Lookup(usr.Service) 20 | } 21 | -------------------------------------------------------------------------------- /users/testdata/kex109x2xh6tle8yls3quqpu9xuhlzffr9fakcv4ymc52cvq366qwnpqyyydgz.json: -------------------------------------------------------------------------------- 1 | [{".sig":"edAc8MLm40yFWVQgZorLMeD5qhRiYt2VLjJko2IbdHZDawurgHjZf7qQLoJav9v9rRqHnln518DX01L2xtcPBw==","data":"eyJrIjoia2V4MTA5eDJ4aDZ0bGU4eWxzM3F1cXB1OXh1aGx6ZmZyOWZha2N2NHltYzUyY3ZxMzY2cXducHF5eXlkZ3oiLCJuIjoiYm9ib2xvYmxhd3MiLCJzcSI6MSwic3IiOiJ0d2l0dGVyIiwidSI6Imh0dHBzOi8vdHdpdHRlci5jb20vYm9ib2xvYmxhd3Mvc3RhdHVzLzEyNzY5NDgyMzM5MTUyMDc2ODAifQ==","kid":"kex109x2xh6tle8yls3quqpu9xuhlzffr9fakcv4ymc52cvq366qwnpqyyydgz","seq":1,"ts":1593286209553,"type":"user"},{".sig":"5IWYdZJzW9dqLTiLIX23Ut6LVW6TeO5COS37k9CH8UVVFQdKt9G4mQgCb7YyE2cQ5RRmYCWyukYfhhB0mcyLAw==","kid":"kex109x2xh6tle8yls3quqpu9xuhlzffr9fakcv4ymc52cvq366qwnpqyyydgz","prev":"hMRmiiS5xq1GY7OS4ZHpaGsF7h5laLdKh+69yoRj/p0=","revoke":1,"seq":2,"type":"revoke"},{".sig":"oiAWrWGtqGNU0lqSEGtViO/1+aEyO2Ye8jamPwPJIZEyxIs4zaq4bHK8tIQ6IuRPU0i9l50DSoSd1eDUdrGODQ==","data":"eyJrIjoia2V4MTA5eDJ4aDZ0bGU4eWxzM3F1cXB1OXh1aGx6ZmZyOWZha2N2NHltYzUyY3ZxMzY2cXducHF5eXlkZ3oiLCJuIjoiYm9ib2xvYmxhd3MiLCJzcSI6Mywic3IiOiJ0d2l0dGVyIiwidSI6Imh0dHBzOi8vdHdpdHRlci5jb20vYm9ib2xvYmxhd3Mvc3RhdHVzLzEyNzY5NDgyMzM5MTUyMDc2ODAifQ==","kid":"kex109x2xh6tle8yls3quqpu9xuhlzffr9fakcv4ymc52cvq366qwnpqyyydgz","prev":"lfF+HD1EsuieHr/neAtzR196MWG7myNcdYN5yVpW8dw=","seq":3,"ts":1593287469835,"type":"user"},{".sig":"nm8593dEPc0GnpkvXetmnylyD6m4o3foMIIg5/5SEq5AGR1VdeGDNx0N6uPfloAwYEWIEn6YorjhKZyaXrwWBQ==","kid":"kex109x2xh6tle8yls3quqpu9xuhlzffr9fakcv4ymc52cvq366qwnpqyyydgz","prev":"xLc3tDxkehkfl1p2Fl+04BqJw3Zudw5tyQE+7vXICYo=","revoke":3,"seq":4,"type":"revoke"},{".sig":"JilMRwctFykjO7P585xBEFGlI30HL4+7TcqIY0zl717d0LuLIDSKv2rpNVoCmoE4RSgfbjMS387ZRx8FW9S9Cg==","data":"eyJrIjoia2V4MTA5eDJ4aDZ0bGU4eWxzM3F1cXB1OXh1aGx6ZmZyOWZha2N2NHltYzUyY3ZxMzY2cXducHF5eXlkZ3oiLCJuIjoiYm9ib2xvYmxhd3MiLCJzcSI6NSwic3IiOiJ0d2l0dGVyIiwidSI6Imh0dHBzOi8vdHdpdHRlci5jb20vYm9ib2xvYmxhd3Mvc3RhdHVzLzEzMDg1MDgxMzg2NTIzMTU2NDgifQ==","kid":"kex109x2xh6tle8yls3quqpu9xuhlzffr9fakcv4ymc52cvq366qwnpqyyydgz","prev":"+kfL0FRhQtdZhyw9n9mDJqXMiVpPJamV9H2PEgyfcXQ=","seq":5,"ts":1600807631380,"type":"user"}] -------------------------------------------------------------------------------- /users/testdata/kex1s08uz8zqqrmzcek0pms0sjknv4wpz33f8p5t57y0d6xsf2sgmd2swgm7er.json: -------------------------------------------------------------------------------- 1 | [{".sig":"0e2LW7K6hQTeucogNTeUumH2Kds4/a5iwOee1cNY6+OUI8dxBFuDJrZzQ4XRgljmhHE7rraul3N+P+oYuGftDQ==","data":"eyJrIjoia2V4MXMwOHV6OHpxcXJtemNlazBwbXMwc2prbnY0d3B6MzNmOHA1dDU3eTBkNnhzZjJzZ21kMnN3Z203ZXIiLCJuIjoiYm9ib2xvYmxhd3MiLCJzcSI6MSwic3IiOiJ0d2l0dGVyIiwidSI6Imh0dHBzOi8vdHdpdHRlci5jb20vYm9ib2xvYmxhd3Mvc3RhdHVzLzEzMDY2MDg1NzQyNTcxOTcwNTgifQ==","kid":"kex1s08uz8zqqrmzcek0pms0sjknv4wpz33f8p5t57y0d6xsf2sgmd2swgm7er","seq":1,"ts":1600358115195,"type":"user"}] -------------------------------------------------------------------------------- /x25519_test.go: -------------------------------------------------------------------------------- 1 | package keys_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/keys-pub/keys" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestNewX25519KeyFromPrivateKey(t *testing.T) { 12 | // Test new X25519Key and X25519Key from private key are the same 13 | X25519Key := keys.GenerateX25519Key() 14 | X25519KeyOut := keys.NewX25519KeyFromPrivateKey(X25519Key.PrivateKey()) 15 | 16 | require.Equal(t, X25519Key.PrivateKey(), X25519KeyOut.PrivateKey()) 17 | require.Equal(t, X25519Key.PublicKey().Bytes(), X25519KeyOut.PublicKey().Bytes()) 18 | } 19 | 20 | func TestX25519KeyConversion(t *testing.T) { 21 | sk := keys.GenerateEdX25519Key() 22 | bk := sk.X25519Key() 23 | 24 | bpk := sk.PublicKey().X25519PublicKey() 25 | 26 | require.Equal(t, bk.PublicKey().Bytes()[:], bpk.Bytes()[:]) 27 | } 28 | 29 | func ExampleGenerateX25519Key() { 30 | alice := keys.GenerateX25519Key() 31 | fmt.Printf("Alice: %s\n", alice.ID()) 32 | } 33 | --------------------------------------------------------------------------------