From 33e16df5864bbf97bf4c37d2737894d5caa27b58 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 17 Aug 2022 08:40:57 +0530 Subject: [PATCH] Add some basic RC serialization tests --- gen-rc-go.py | 1 + tools/cli/infrastructure_test.go | 2 +- tools/cmd/at/main.go | 84 +++++++++++++++++++++++++++----- tools/cmd/at/main_test.go | 51 +++++++++++++++++++ tools/cmd/at/template.go | 11 +++++ tools/crypto/crypto.go | 48 ++++++++++-------- tools/utils/types.go | 9 ++-- 7 files changed, 169 insertions(+), 37 deletions(-) create mode 100644 tools/cmd/at/main_test.go diff --git a/gen-rc-go.py b/gen-rc-go.py index 13012f13a..05a67c1c4 100755 --- a/gen-rc-go.py +++ b/gen-rc-go.py @@ -42,6 +42,7 @@ var WebsiteBaseURL string = "{kc.website_base_url}" var Version VersionType = VersionType{{Major: {kc.version.major}, Minor: {kc.version.minor}, Patch: {kc.version.patch},}} var DefaultPager []string = []string{{ {dp} }} var VCSRevision string = "" +var RC_ENCRYPTION_PROTOCOL_VERSION string = "{kc.RC_ENCRYPTION_PROTOCOL_VERSION}" ''') with open('tools/cmd/at/template.go') as f: template = f.read() diff --git a/tools/cli/infrastructure_test.go b/tools/cli/infrastructure_test.go index a905a2fe9..656f15598 100644 --- a/tools/cli/infrastructure_test.go +++ b/tools/cli/infrastructure_test.go @@ -13,6 +13,6 @@ func TestFormatLineWithIndent(t *testing.T) { format_line_with_indent(&output, "testing \x1b[31mstyled\x1b[m", indent, 11) expected := indent + "testing \n" + indent + "\x1b[31mstyled\x1b[m\n" if output.String() != expected { - t.Errorf("%#v != %#v", expected, output.String()) + t.Fatalf("%#v != %#v", expected, output.String()) } } diff --git a/tools/cmd/at/main.go b/tools/cmd/at/main.go index f3a81c553..95cbb9e75 100644 --- a/tools/cmd/at/main.go +++ b/tools/cmd/at/main.go @@ -1,6 +1,7 @@ package at import ( + "encoding/json" "fmt" "io/ioutil" "os" @@ -11,24 +12,86 @@ import ( "golang.org/x/sys/unix" "golang.org/x/term" + "kitty" + "kitty/tools/base85" "kitty/tools/cli" "kitty/tools/crypto" + "kitty/tools/utils" ) -var encrypt_cmd = crypto.Encrypt_cmd - type GlobalOptions struct { - to, password, use_password string - to_from_env bool + to_address, password string + to_address_is_from_env_var bool } var global_options GlobalOptions -func get_password(password string, password_file string, password_env string, use_password string) (string, error) { - if use_password == "never" { - return "", nil +func get_pubkey(encoded_key string) (encryption_version string, pubkey []byte, err error) { + if encoded_key == "" { + encoded_key = os.Getenv("KITTY_PUBLIC_KEY") + if encoded_key == "" { + err = fmt.Errorf("Password usage requested but KITTY_PUBLIC_KEY environment variable is not available") + return + } + } + encryption_version, encoded_key, found := strings.Cut(encoded_key, ":") + if !found { + err = fmt.Errorf("KITTY_PUBLIC_KEY environment variable does not have a : in it") + return + } + if encryption_version != kitty.RC_ENCRYPTION_PROTOCOL_VERSION { + err = fmt.Errorf("KITTY_PUBLIC_KEY has unknown version, if you are running on a remote system, update kitty on this system") + return + } + pubkey = make([]byte, base85.DecodedLen(len(encoded_key))) + n, err := base85.Decode(pubkey, []byte(encoded_key)) + if err == nil { + pubkey = pubkey[:n] + } + return +} + +func simple_serializer(rc *utils.RemoteControlCmd) (ans []byte, err error) { + ans, err = json.Marshal(rc) + return +} + +type serializer_func func(rc *utils.RemoteControlCmd) ([]byte, error) + +var serializer serializer_func = simple_serializer + +func create_serializer(password string, encoded_pubkey string) (ans serializer_func, err error) { + if password != "" { + encryption_version, pubkey, err := get_pubkey(encoded_pubkey) + if err != nil { + return nil, err + } + ans = func(rc *utils.RemoteControlCmd) (ans []byte, err error) { + ec, err := crypto.Encrypt_cmd(rc, global_options.password, pubkey, encryption_version) + ans, err = json.Marshal(ec) + return + } + } + return simple_serializer, nil +} + +func send_rc_command(rc *utils.RemoteControlCmd) (err error) { + serializer, err = create_serializer(global_options.password, "") + if err != nil { + return + } + d, err := serializer(rc) + if err != nil { + return + } + println(string(d)) + return +} + +func get_password(password string, password_file string, password_env string, use_password string) (ans string, err error) { + if use_password == "never" { + return } - ans := "" if password != "" { ans = password } @@ -82,10 +145,9 @@ func EntryPoint(tool_root *cobra.Command) *cobra.Command { PersistentPreRunE: func(cmd *cobra.Command, args []string) error { if *to == "" { *to = os.Getenv("KITTY_LISTEN_ON") - global_options.to_from_env = true + global_options.to_address_is_from_env_var = true } - global_options.to = *to - global_options.use_password = *use_password + global_options.to_address = *to q, err := get_password(*password, *password_file, *password_env, *use_password) global_options.password = q return err diff --git a/tools/cmd/at/main_test.go b/tools/cmd/at/main_test.go new file mode 100644 index 000000000..857a60ceb --- /dev/null +++ b/tools/cmd/at/main_test.go @@ -0,0 +1,51 @@ +package at + +import ( + "encoding/json" + "kitty/tools/crypto" + "kitty/tools/utils" + "testing" +) + +func TestRCSerialization(t *testing.T) { + serializer, err := create_serializer("", "") + if err != nil { + t.Fatal(err) + } + var ver = [3]int{1, 2, 3} + rc := utils.RemoteControlCmd{ + Cmd: "test", Version: ver, + } + simple := func(expected string) { + actual, err := serializer(&rc) + if err != nil { + t.Fatal(err) + } + as := string(actual) + if as != expected { + t.Fatalf("Incorrect serialization: %s != %s", expected, as) + } + } + simple(`{"cmd":"test","version":[1,2,3]}`) + pubkey_b, _, err := crypto.KeyPair("1") + if err != nil { + t.Fatal(err) + } + pubkey, err := crypto.EncodePublicKey(pubkey_b, "1") + if err != nil { + t.Fatal(err) + } + serializer, err = create_serializer("tpw", pubkey) + if err != nil { + t.Fatal(err) + } + raw, err := serializer(&rc) + var ec utils.EncryptedRemoteControlCmd + err = json.Unmarshal([]byte(raw), &ec) + if err != nil { + t.Fatal(err) + } + if ec.Version != ver { + t.Fatal("Incorrect version in encrypted command: ", ec.Version) + } +} diff --git a/tools/cmd/at/template.go b/tools/cmd/at/template.go index 3aa3ffdea..e38b43d1f 100644 --- a/tools/cmd/at/template.go +++ b/tools/cmd/at/template.go @@ -8,13 +8,24 @@ import ( "github.com/spf13/cobra" "kitty/tools/cli" + "kitty/tools/utils" ) +func run_CMD_NAME(cmd *cobra.Command, args []string) (err error) { + rc := utils.RemoteControlCmd{ + Cmd: "CLI_NAME", + Version: [3]int{0, 20, 0}, + } + err = send_rc_command(&rc) + return +} + func setup_CMD_NAME(root *cobra.Command) *cobra.Command { ans := cli.CreateCommand(&cobra.Command{ Use: "CLI_NAME [options]", Short: "SHORT_DESC", Long: "LONG_DESC", + RunE: run_CMD_NAME, }) return ans diff --git a/tools/crypto/crypto.go b/tools/crypto/crypto.go index 175f58aee..b44f8bf13 100644 --- a/tools/crypto/crypto.go +++ b/tools/crypto/crypto.go @@ -6,13 +6,10 @@ import ( "crypto/rand" "crypto/sha256" "encoding/json" - "errors" "fmt" "golang.org/x/crypto/curve25519" "kitty/tools/base85" "kitty/tools/utils" - "os" - "strings" "time" ) @@ -40,8 +37,8 @@ func b85_decode(data string) (decoded []byte, err error) { return } -func encrypt(plaintext []byte, alice_public_key []byte) (iv []byte, tag []byte, ciphertext []byte, bob_public_key []byte, err error) { - bob_private_key, bob_public_key, err := curve25519_key_pair() +func encrypt(plaintext []byte, alice_public_key []byte, encryption_protocol string) (iv []byte, tag []byte, ciphertext []byte, bob_public_key []byte, err error) { + bob_private_key, bob_public_key, err := KeyPair(encryption_protocol) if err != nil { return } @@ -70,31 +67,40 @@ func encrypt(plaintext []byte, alice_public_key []byte) (iv []byte, tag []byte, return } -func Encrypt_cmd(cmd *utils.RemoteControlCmd, password string, other_pubkey []byte) (encrypted_cmd utils.EncryptedRemoteControlCmd, err error) { - if len(other_pubkey) == 0 { - raw := os.Getenv("KITTY_PUBLIC_KEY") - if len(raw) == 0 { - err = errors.New("No KITTY_PUBLIC_KEY environment variable set cannot use passwords") - return - } - if !strings.HasPrefix(raw, "1:") { - err = fmt.Errorf("KITTY_PUBLIC_KEY has unknown protocol: %s", raw[:2]) - return - } - other_pubkey, err = b85_decode(raw[2:]) - if err != nil { - return - } +func KeyPair(encryption_protocol string) (private_key []byte, public_key []byte, err error) { + switch encryption_protocol { + case "1": + return curve25519_key_pair() + default: + err = fmt.Errorf("Unknown encryption protocol: %s", encryption_protocol) + return } +} + +func EncodePublicKey(pubkey []byte, encryption_protocol string) (ans string, err error) { + switch encryption_protocol { + case "1": + ans = fmt.Sprintf("1:%s", b85_encode(pubkey)) + default: + err = fmt.Errorf("Unknown encryption protocol: %s", encryption_protocol) + return + } + return +} + +func Encrypt_cmd(cmd *utils.RemoteControlCmd, password string, other_pubkey []byte, encryption_protocol string) (encrypted_cmd utils.EncryptedRemoteControlCmd, err error) { cmd.Password = password cmd.Timestamp = time.Now().UnixNano() plaintext, err := json.Marshal(cmd) if err != nil { return } - iv, tag, ciphertext, pubkey, err := encrypt(plaintext, other_pubkey) + iv, tag, ciphertext, pubkey, err := encrypt(plaintext, other_pubkey, encryption_protocol) encrypted_cmd = utils.EncryptedRemoteControlCmd{ Version: cmd.Version, IV: b85_encode(iv), Tag: b85_encode(tag), Pubkey: b85_encode(pubkey), Encrypted: b85_encode(ciphertext)} + if encryption_protocol != "1" { + encrypted_cmd.EncProto = encryption_protocol + } return } diff --git a/tools/utils/types.go b/tools/utils/types.go index 7df61f57c..09d6e49c8 100644 --- a/tools/utils/types.go +++ b/tools/utils/types.go @@ -3,10 +3,10 @@ package utils type RemoteControlCmd struct { Cmd string `json:"cmd"` Version [3]int `json:"version"` - NoResponse bool `json:"no_response,omitifempty"` - Payload map[string]interface{} `json:"payload,omitifempty"` - Timestamp int64 `json:"timestamp,omitifempty"` - Password string `json:"password,omitifempty"` + NoResponse bool `json:"no_response,omitempty"` + Payload map[string]interface{} `json:"payload,omitempty"` + Timestamp int64 `json:"timestamp,omitempty"` + Password string `json:"password,omitempty"` } type EncryptedRemoteControlCmd struct { @@ -15,4 +15,5 @@ type EncryptedRemoteControlCmd struct { Tag string `json:"tag"` Pubkey string `json:"pubkey"` Encrypted string `json:"encrypted"` + EncProto string `json:"enc_proto,omitempty"` }