Start work on implementing kitty @ as a static binary using Go

This commit is contained in:
Kovid Goyal 2022-08-14 18:36:03 +05:30
parent 126468a5dd
commit bbf7504303
No known key found for this signature in database
GPG Key ID: 06BC317B515ACE7C
8 changed files with 796 additions and 0 deletions

19
go.mod Normal file
View File

@ -0,0 +1,19 @@
module kitty
go 1.19
require (
github.com/fatih/color v1.13.0
github.com/mattn/go-runewidth v0.0.13
github.com/spf13/cobra v1.5.0
github.com/spf13/pflag v1.0.5
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa
)
require (
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/mattn/go-colorable v0.1.9 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect
)

28
go.sum Normal file
View File

@ -0,0 +1,28 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w=
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

3
tools/README.rst Normal file
View File

@ -0,0 +1,3 @@
This folder contains various small command line utilities compiled statically
for doing things like kitty remote control or icat on machines where the full
kitty is not available.

328
tools/base85/base85.go Normal file
View File

@ -0,0 +1,328 @@
// package base85
// This package provides a RFC1924 implementation of base85 encoding.
//
// See http://www.ietf.org/rfc/rfc1924.txt
package base85
import (
"bufio"
"errors"
"io"
"strconv"
)
// 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!#$%&()*+-;<=>?@^_`{|}~
var b2c_table = [85]byte{
// 0 1 2 3 4 5 6 7 8 9 A B C D E F
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, //0
0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, //1
0x57, 0x58, 0x59, 0x5A, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, //2
0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A, 0x21, 0x23, //3
0x24, 0x25, 0x26, 0x28, 0x29, 0x2A, 0x2B, 0x2D, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x5E, 0x5F, //4
0x60, 0x7B, 0x7C, 0x7D, 0x7E,
}
var encode [85]byte
var decode [256]byte
func init() {
copy(encode[:], b2c_table[:])
for i := 0; i < len(decode); i++ {
decode[i] = 0xFF
}
for i := 0; i < len(encode); i++ {
decode[encode[i]] = byte(i)
}
}
// encodeChunk encodes 4 byte-chunk to 5 byte
// if chunk size is less then 4, then it is padded before convertion.
// return written bytes or error
func encodeChunk(dst, src []byte) int {
if len(src) == 0 {
return 0
}
//read 4 byte as big-endian uint32 into small endian uint32
var val uint32
switch len(src) {
default:
val |= uint32(src[3])
fallthrough
case 3:
val |= uint32(src[2]) << 8
fallthrough
case 2:
val |= uint32(src[1]) << 16
fallthrough
case 1:
val |= uint32(src[0]) << 24
}
buf := [5]byte{0, 0, 0, 0, 0}
for i := 4; i >= 0; i-- {
r := val % 85
val /= 85
buf[i] = encode[r]
}
m := EncodedLen(len(src))
copy(dst[:], buf[:m])
return m
}
var decode_base = [5]uint32{85 * 85 * 85 * 85, 85 * 85 * 85, 85 * 85, 85, 1}
// decodeChunk decodes 5 byte-chunk to 4 byte
// if chunk size is less then 5, then it is padded before convertion.
// return written bytes and error input index
func decodeChunk(dst, src []byte) (int, int) {
if len(src) == 0 {
return 0, 0
}
var val uint32
m := DecodedLen(len(src))
buf := [5]byte{84, 84, 84, 84, 84}
for i := 0; i < len(src); i++ {
e := decode[src[i]]
if e == 0xFF {
return 0, i + 1
}
buf[i] = e
}
for i := 0; i < 5; i++ {
r := buf[i]
val += uint32(r) * decode_base[i]
}
//small endian uint32 to big endian uint32 in bytes
switch m {
default:
dst[3] = byte(val & 0xff)
fallthrough
case 3:
dst[2] = byte((val >> 8) & 0xff)
fallthrough
case 2:
dst[1] = byte((val >> 16) & 0xff)
fallthrough
case 1:
dst[0] = byte((val >> 24) & 0xff)
}
return m, 0
}
// Encode encodes src into dst, return the bytes written
// The dst must have size of EncodedLen(len(src))
func Encode(dst, src []byte) int {
n := 0
for len(src) > 0 {
if len(src) < 4 {
n += encodeChunk(dst, src)
return n
}
n += encodeChunk(dst[:5], src[:4])
src = src[4:]
dst = dst[5:]
}
return n
}
// EncodeToString returns the base85 encoding of src.
func EncodeToString(src []byte) string {
buf := make([]byte, EncodedLen(len(src)))
Encode(buf, src)
return string(buf)
}
// DecodeString returns the bytes represented by the base85 string s.
func DecodeString(src string) ([]byte, error) {
buf := make([]byte, DecodedLen(len(src)))
_, err := Decode(buf, []byte(src))
return buf, err
}
// Decode decodes src into dst, return the bytes written
// The dst must have size of DecodedLen(len(src))
// An CorruptInputError is returned when invalid character is found in src.
func Decode(dst, src []byte) (int, error) {
f := 0
t := 0
for len(src) > 0 {
if len(src) < 5 {
w, err := decodeChunk(dst, src)
if err > 0 {
return t, CorruptInputError(f + err)
}
return t + w, nil
}
_, err := decodeChunk(dst[:4], src[:5])
if err > 0 {
return t, CorruptInputError(f + err)
} else {
t += 4
f += 5
src = src[5:]
dst = dst[4:]
}
}
return t, nil
}
// EncodedLen returns the length in bytes of the base64 encoding of an input
// buffer of length n.
func EncodedLen(n int) int {
s := n / 4
r := n % 4
if r > 0 {
return s*5 + 5 - (4 - r)
} else {
return s * 5
}
}
// DecodedLen returns the maximum length in bytes of the decoded data
// corresponding to n bytes of base85-encoded data.
func DecodedLen(n int) int {
s := n / 5
r := n % 5
if r > 0 {
return s*4 + 4 - (5 - r)
} else {
return s * 4
}
}
type encoder struct {
w io.Writer
bufin [4]byte
encoded [5]byte
fill int
err error
}
func (e *encoder) Write(p []byte) (n int, err error) {
if e.err != nil {
return 0, e.err
}
for len(p) >= len(e.bufin)-e.fill {
//copy len(e.buf) - fill bytes into e.buf to make it full
to_copy := len(e.bufin) - e.fill
copy(e.bufin[e.fill:], p[:to_copy])
p = p[to_copy:]
//write the encoded whole buffer
encodeChunk(e.encoded[:], e.bufin[:])
_, e.err = e.w.Write(e.encoded[:])
if e.err != nil {
return n, e.err
}
n += 4
e.fill = 0
}
for i := 0; i < len(p); i++ {
e.bufin[e.fill] = p[i]
e.fill += 1
}
return n, e.w.(*bufio.Writer).Flush()
}
func (e *encoder) Close() error {
if e.err == nil && e.fill > 0 {
m := EncodedLen(e.fill)
encodeChunk(e.encoded[:m], e.bufin[:e.fill])
_, e.err = e.w.Write(e.encoded[:m])
if e.err != nil {
return e.err
}
e.err = e.w.(*bufio.Writer).Flush()
}
err := e.err
e.err = errors.New("encoder closed")
return err
}
// NewEncoder returns a stream encoder of w.
// All write to the encoder is encoded into base85 and write to w.
// The writer should call Close() to indicate the end of stream
func NewEncoder(w io.Writer) io.WriteCloser {
encoder := new(encoder)
encoder.w = bufio.NewWriterSize(w, 1000)
return encoder
}
type decoder struct {
r io.Reader
bufin [1000]byte
decoded []byte
fill int
err error
}
// NewDecoder returns a stream decoder of r.
// All read from the reader will read the base85 encoded string from r and decode it.
func NewDecoder(r io.Reader) io.Reader {
decoder := new(decoder)
decoder.r = bufio.NewReaderSize(r, 1000)
return decoder
}
func (d *decoder) Read(p []byte) (n int, err error) {
if d.err != nil {
return 0, d.err
}
for len(p) > 0 {
// try filling the buffer
m, err := d.r.Read(d.bufin[d.fill:])
d.fill += m
if err != nil {
// no further input, decode and copy into p
d.decoded = make([]byte, DecodedLen(d.fill))
if d.err == io.EOF {
k, err := Decode(d.decoded, d.bufin[:d.fill])
copy(p, d.decoded[:k])
n += k
d.fill -= EncodedLen(k)
if err != nil {
d.err = err
return n, err
}
} else {
k, err := Decode(d.decoded, d.bufin[:d.fill-d.fill%5])
copy(p, d.decoded[:k])
n += k
d.fill -= EncodedLen(k)
if err != nil {
d.err = err
return n, err
}
}
d.err = err
return n, d.err
}
//decode d.fill - d.fill % 5 byte of d.bufin
chunked_max := d.fill
d.fill = d.fill % 5
chunked_max -= d.fill
d.decoded = make([]byte, DecodedLen(chunked_max))
k, err := Decode(d.decoded, d.bufin[:chunked_max])
copy(p, d.decoded[:k])
p = p[k:]
n += k
if err != nil {
d.err = err
return n, d.err
}
}
return n, d.err
}
type CorruptInputError int64
func (e CorruptInputError) Error() string {
return "illegal base85 data at input byte " + strconv.FormatInt(int64(e), 10)
}

225
tools/cli/infrastructure.go Normal file
View File

@ -0,0 +1,225 @@
package cli
import (
"fmt"
"os"
"strings"
"syscall"
"unicode"
"unsafe"
"github.com/fatih/color"
runewidth "github.com/mattn/go-runewidth"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"kitty"
)
type Winsize struct {
Rows uint16
Cols uint16
Xpixels uint16
Ypixels uint16
}
func GetTTYSize() (Winsize, error) {
var ws Winsize
f, err := os.OpenFile("/dev/tty", os.O_RDONLY, 0755)
if err != nil {
return ws, err
}
fd := f.Fd()
retCode, _, errno := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&ws)))
f.Close()
if int(retCode) == -1 {
return ws, errno
}
return ws, nil
}
func add_choices(cmd *cobra.Command, flags *pflag.FlagSet, choices []string, name string, usage string) {
flags.String(name, choices[0], usage)
cmd.Annotations["choices-"+name] = strings.Join(choices, "\000")
}
func Choices(cmd *cobra.Command, name string, usage string, choices ...string) {
add_choices(cmd, cmd.Flags(), choices, name, usage)
}
func PersistentChoices(cmd *cobra.Command, name string, usage string, choices ...string) {
add_choices(cmd, cmd.PersistentFlags(), choices, name, usage)
}
func key_in_slice(vals []string, key string) bool {
for _, q := range vals {
if q == key {
return true
}
}
return false
}
func ValidateChoices(cmd *cobra.Command, args []string) error {
for key, val := range cmd.Annotations {
if strings.HasPrefix(key, "choices-") {
allowed := strings.Split(val, "\000")
name := key[len("choices-"):]
if cval, err := cmd.Flags().GetString(name); err == nil && !key_in_slice(allowed, cval) {
return fmt.Errorf("%s: Invalid value: %s. Allowed values are: %s", color.YellowString("--"+name), color.RedString(cval), strings.Join(allowed, ", "))
}
}
}
return nil
}
var title_fmt = color.New(color.FgBlue, color.Bold).SprintFunc()
var exe_fmt = color.New(color.FgYellow, color.Bold).SprintFunc()
var opt_fmt = color.New(color.FgGreen).SprintFunc()
var italic_fmt = color.New(color.Italic).SprintFunc()
func cmd_name(cmd *cobra.Command) string {
if cmd.Annotations != nil {
parts := strings.Split(cmd.Annotations["exe"], " ")
return parts[len(parts)-1]
}
return cmd.Name()
}
func print_created_by(root *cobra.Command) {
fmt.Println(italic_fmt(root.Annotations["exe"]), opt_fmt(root.Version), "created by", title_fmt("Kovid Goyal"))
}
func print_with_indent(text string, indent string, screen_width int) {
x := len(indent)
fmt.Print(indent)
in_sgr := false
current_word := ""
print_word := func(r rune) {
w := runewidth.StringWidth(current_word)
if x+w > screen_width {
fmt.Println()
fmt.Print(indent)
x = len(indent)
current_word = strings.TrimSpace(current_word)
}
fmt.Print(current_word)
current_word = string(r)
x += w
}
for _, r := range text {
if in_sgr {
if r == 'm' {
in_sgr = false
}
continue
}
if r == 0x1b {
in_sgr = true
continue
}
if current_word != "" && unicode.IsSpace(r) && r != 0xa0 {
print_word(r)
} else {
current_word += string(r)
}
}
if current_word != "" {
print_word(' ')
fmt.Print(current_word)
}
if len(text) > 0 {
fmt.Println()
}
}
func show_usage(cmd *cobra.Command) error {
ws, tty_size_err := GetTTYSize()
screen_width := 80
if tty_size_err == nil && ws.Cols < 80 {
screen_width = int(ws.Cols)
}
fmt.Println(title_fmt("Usage")+":", exe_fmt(cmd.Annotations["exe"]), cmd.Use)
fmt.Println()
if len(cmd.Long) > 0 {
print_with_indent(cmd.Long, "", screen_width)
} else if len(cmd.Short) > 0 {
print_with_indent(cmd.Short, "", screen_width)
}
if cmd.HasAvailableSubCommands() {
fmt.Println()
fmt.Println(title_fmt("Commands") + ":")
for _, child := range cmd.Commands() {
fmt.Println(" ", opt_fmt(cmd_name(child)))
print_with_indent(child.Short, " ", screen_width)
}
fmt.Println()
print_with_indent("Get help for an individual command by running:", "", screen_width)
fmt.Println(" ", cmd.Annotations["exe"], italic_fmt("command"), "-h")
}
if cmd.HasAvailableFlags() {
options_title := cmd.Annotations["options_title"]
if len(options_title) == 0 {
options_title = "Options"
}
fmt.Println()
fmt.Println(title_fmt(options_title) + ":")
flag_set := cmd.LocalFlags()
flag_set.VisitAll(func(flag *pflag.Flag) {
fmt.Print(opt_fmt(" --" + flag.Name))
if flag.Shorthand != "" {
fmt.Print(", ", opt_fmt("-"+flag.Shorthand))
}
defval := ""
switch flag.Value.Type() {
default:
defval = fmt.Sprintf("[=%s]", italic_fmt(flag.DefValue))
case "bool":
case "count":
}
if defval != "" {
fmt.Print(" ", defval)
}
fmt.Println()
if flag.Name == "help" {
fmt.Println(" ", "Print this help message")
fmt.Println()
return
}
for _, line := range strings.Split(flag.Usage, "\n") {
print_with_indent(line, " ", screen_width)
}
if cmd.Annotations["choices-"+flag.Name] != "" {
fmt.Println(" Choices:", strings.Join(strings.Split(cmd.Annotations["choices-"+flag.Name], "\000"), ", "))
}
fmt.Println()
})
}
if cmd.HasParent() {
cmd.VisitParents(func(cmd *cobra.Command) {
if !cmd.HasParent() {
print_created_by(cmd)
}
})
} else {
print_created_by(cmd)
}
return nil
}
func CreateCommand(cmd *cobra.Command, exe string) *cobra.Command {
cmd.Annotations = make(map[string]string)
cmd.Annotations["exe"] = exe
return cmd
}
func Init(root *cobra.Command) {
root.Version = kitty.VersionString
root.PersistentPreRunE = ValidateChoices
root.SetUsageFunc(show_usage)
root.SetHelpTemplate("{{.UsageString}}")
}

32
tools/cmd/at/main.go Normal file
View File

@ -0,0 +1,32 @@
package main
import (
"fmt"
"os"
"github.com/spf13/cobra"
"kitty/tools/cli"
"kitty/tools/crypto"
)
var encrypt_cmd = crypto.Encrypt_cmd
func main() {
var root = cli.CreateCommand(&cobra.Command{
Use: "[global options] command [command options] [command args]",
Short: "Control kitty remotely",
Long: "Control kitty by sending it commands. Set the allow_remote_control option in kitty.conf or use a password, for this to work.",
Run: func(cmd *cobra.Command, args []string) {
use_password, _ := cmd.Flags().GetString("use-password")
fmt.Println("In global run, use-password:", use_password)
},
}, "kitty-at")
cli.PersistentChoices(root, "use-password", "If no password is available, kitty will usually just send the remote control command without a password. This option can be used to force it to always or never use the supplied password.", "if-available", "always", "never")
root.Annotations["options_title"] = "Global options"
cli.Init(root)
if err := root.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

117
tools/crypto/crypto.go Normal file
View File

@ -0,0 +1,117 @@
package crypto
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"golang.org/x/crypto/curve25519"
"kitty/tools/base85"
"os"
"strings"
"time"
)
type Cmd 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"`
}
func curve25519_key_pair() (private_key []byte, public_key []byte, err error) {
private_key = make([]byte, 32)
_, err = rand.Read(private_key)
if err == nil {
public_key, err = curve25519.X25519(private_key[:], curve25519.Basepoint)
}
return
}
func curve25519_derive_shared_secret(private_key []byte, public_key []byte) (secret []byte, err error) {
secret, err = curve25519.X25519(private_key[:], public_key[:])
return
}
func b85_encode(data []byte) (encoded string) {
encoded = base85.EncodeToString(data)
return
}
func b85_decode(data string) (decoded []byte, err error) {
decoded, err = base85.DecodeString(data)
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()
if err != nil {
return
}
shared_secret_raw, err := curve25519_derive_shared_secret(bob_private_key, alice_public_key)
if err != nil {
return
}
shared_secret_hashed := sha256.Sum256(shared_secret_raw)
shared_secret := shared_secret_hashed[:]
block, err := aes.NewCipher(shared_secret)
if err != nil {
return
}
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return
}
iv = make([]byte, aesgcm.NonceSize())
_, err = rand.Read(iv)
if err != nil {
return
}
output := aesgcm.Seal(nil, iv, plaintext, nil)
ciphertext = output[0 : len(output)-16]
tag = output[len(output)-16:]
return
}
type EncryptedCmd struct {
Version [3]int `json:"version"`
IV string `json:"iv"`
Tag string `json:"tag"`
Pubkey string `json:"pubkey"`
Encrypted string `json:"encrypted"`
}
func Encrypt_cmd(cmd *Cmd, password string, other_pubkey []byte) (encrypted_cmd EncryptedCmd, 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
}
}
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)
encrypted_cmd = EncryptedCmd{
Version: cmd.Version, IV: b85_encode(iv), Tag: b85_encode(tag), Pubkey: b85_encode(pubkey), Encrypted: b85_encode(ciphertext)}
return
}
// }}}

44
version.go Normal file
View File

@ -0,0 +1,44 @@
package kitty
import (
_ "embed"
"fmt"
"regexp"
"runtime/debug"
"strconv"
)
//go:embed kitty/constants.py
var raw string
type VersionType struct {
major, minor, patch int
}
var VersionString string
var Version VersionType
var VCSRevision string
func init() {
var verpat = regexp.MustCompile(`Version\((\d+),\s*(\d+),\s*(\d+)\)`)
matches := verpat.FindStringSubmatch(raw)
major, err := strconv.Atoi(matches[1])
minor, err := strconv.Atoi(matches[2])
patch, err := strconv.Atoi(matches[3])
if err != nil {
panic(err)
}
Version.major = major
Version.minor = minor
Version.patch = patch
VersionString = fmt.Sprint(major, ".", minor, ".", patch)
bi, ok := debug.ReadBuildInfo()
if ok {
for _, bs := range bi.Settings {
if bs.Key == "vcs.revision" {
VCSRevision = bs.Value
}
}
}
}