Use encryption for bypass

This commit is contained in:
Kovid Goyal
2023-06-30 18:27:27 +05:30
parent 6d1dd50546
commit aa86b98eee
5 changed files with 86 additions and 11 deletions

View File

@@ -458,6 +458,12 @@ The value of ``bypass`` is of the form ``hash_function_name : hash_value``
they will introduce a lot of latency to starting a session and in any case they will introduce a lot of latency to starting a session and in any case
there is no mathematical proof that **any** hash function is not brute-forceable. there is no mathematical proof that **any** hash function is not brute-forceable.
Terminal implementations are free to use their own more advanced hashing
schemes, with prefixes other than those starting with ``sha``, which are
reserved. For instance, kitty uses a scheme based on public key encryption
via :envvar:`KITTY_PUBLIC_KEY`. For details of this scheme, see the
``check_bypass()`` function in the kitty source code.
Encoding of transfer commands as escape codes Encoding of transfer commands as escape codes
------------------------------------------------ ------------------------------------------------

View File

@@ -361,7 +361,13 @@ func (self *SendManager) start_transfer() string {
func (self *SendManager) initialize() { func (self *SendManager) initialize() {
if self.bypass != "" { if self.bypass != "" {
self.bypass = encode_bypass(self.request_id, self.bypass) q, err := encode_bypass(self.request_id, self.bypass)
if err == nil {
self.bypass = q
} else {
fmt.Fprintln(os.Stderr, "Ignoring password because of error:", err)
}
} }
self.fid_map = make(map[string]*File, len(self.files)) self.fid_map = make(map[string]*File, len(self.files))
for _, f := range self.files { for _, f := range self.files {

View File

@@ -4,13 +4,13 @@ package transfer
import ( import (
"crypto/rand" "crypto/rand"
"crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"kitty/tools/crypto"
"kitty/tools/utils" "kitty/tools/utils"
) )
@@ -33,10 +33,20 @@ func home_path() string {
return global_home return global_home
} }
func encode_bypass(request_id string, bypass string) string { func encode_bypass(request_id string, bypass string) (string, error) {
q := request_id + ";" + bypass q := request_id + ";" + bypass
sum := sha256.Sum256(utils.UnsafeStringToBytes(q)) if pkey_encoded := os.Getenv("KITTY_PUBLIC_KEY"); pkey_encoded != "" {
return fmt.Sprintf("sha256:%x", sum) encryption_protocol, pubkey, err := crypto.DecodePublicKey(pkey_encoded)
if err != nil {
return "", err
}
encrypted, err := crypto.Encrypt_data(utils.UnsafeStringToBytes(q), pubkey, encryption_protocol)
if err != nil {
return "", err
}
return fmt.Sprintf("kitty-1:%s", utils.UnsafeBytesToString(encrypted)), nil
}
return "", fmt.Errorf("KITTY_PUBLIC_KEY env var not set, cannot transmit password securely")
} }
func abspath(path string, use_home ...bool) string { func abspath(path string, use_home ...bool) string {

View File

@@ -1,12 +1,13 @@
#!/usr/bin/env python #!/usr/bin/env python
# License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net> # License: GPLv3 Copyright: 2021, Kovid Goyal <kovid at kovidgoyal.net>
import base64
import errno import errno
import json
import os import os
import re import re
import stat import stat
import tempfile import tempfile
from base64 import standard_b64decode
from collections import defaultdict, deque from collections import defaultdict, deque
from contextlib import suppress from contextlib import suppress
from dataclasses import Field, dataclass, field, fields from dataclasses import Field, dataclass, field, fields
@@ -14,12 +15,12 @@ from enum import Enum, auto
from functools import partial from functools import partial
from gettext import gettext as _ from gettext import gettext as _
from itertools import count from itertools import count
from time import monotonic from time import monotonic, time_ns
from typing import IO, Any, Callable, DefaultDict, Deque, Dict, Iterable, Iterator, List, Optional, Tuple, Union, cast from typing import IO, Any, Callable, DefaultDict, Deque, Dict, Iterable, Iterator, List, Optional, Tuple, Union, cast
from kittens.transfer.librsync import LoadSignature, PatchFile, delta_for_file, signature_of_file from kittens.transfer.librsync import LoadSignature, PatchFile, delta_for_file, signature_of_file
from kittens.transfer.utils import IdentityCompressor, ZlibCompressor, abspath, expand_home, home_path from kittens.transfer.utils import IdentityCompressor, ZlibCompressor, abspath, expand_home, home_path
from kitty.fast_data_types import FILE_TRANSFER_CODE, OSC, add_timer, base64_encode, get_boss, get_options from kitty.fast_data_types import FILE_TRANSFER_CODE, OSC, AES256GCMDecrypt, add_timer, base64_encode, get_boss, get_options
from kitty.types import run_once from kitty.types import run_once
from .utils import log_error from .utils import log_error
@@ -252,7 +253,7 @@ def b64decode(val: memoryview) -> bytes:
if extra != 0: if extra != 0:
padding = b'=' * (4 - extra) padding = b'=' * (4 - extra)
val = memoryview(bytes(val) + padding) val = memoryview(bytes(val) + padding)
return standard_b64decode(val) return base64.standard_b64decode(val)
@dataclass @dataclass
@@ -494,6 +495,29 @@ class DestFile:
self.apply_metadata() self.apply_metadata()
def check_bypass(password: str, request_id: str, bypass_data: str) -> bool:
protocol, sep, bypass_data = bypass_data.partition(':')
if protocol == 'kitty-1':
try:
pcmd = json.loads(bypass_data)
pubkey = pcmd.get('pubkey', '')
if not pubkey:
return False
ekey = get_boss().encryption_key
d = AES256GCMDecrypt(ekey.derive_secret(base64.b85decode(pubkey)), base64.b85decode(pcmd['iv']), base64.b85decode(pcmd['tag']))
data = d.add_data_to_be_decrypted(base64.b85decode(pcmd['encrypted']), True)
timestamp, sep, payload = data.decode('utf-8').partition(':')
delta = time_ns() - int(timestamp)
if abs(delta) > 5 * 60 * 1e9:
return False
return payload == f'{request_id};{password}'
except Exception:
return False
elif protocol == 'sha256':
return (encode_bypass(request_id, password) == bypass_data) if password else False
return False
class ActiveReceive: class ActiveReceive:
id: str id: str
files: Dict[str, DestFile] files: Dict[str, DestFile]
@@ -504,7 +528,7 @@ class ActiveReceive:
self.bypass_ok: Optional[bool] = None self.bypass_ok: Optional[bool] = None
if bypass: if bypass:
byp = get_options().file_transfer_confirmation_bypass byp = get_options().file_transfer_confirmation_bypass
self.bypass_ok = (encode_bypass(request_id, byp) == bypass) if byp else False self.bypass_ok = check_bypass(byp, request_id, bypass)
self.files = {} self.files = {}
self.last_activity_at = monotonic() self.last_activity_at = monotonic()
self.send_acknowledgements = quiet < 1 self.send_acknowledgements = quiet < 1

View File

@@ -11,9 +11,12 @@ import (
"crypto/sha256" "crypto/sha256"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/jamesruan/go-rfc1924/base85"
"kitty/tools/utils" "kitty/tools/utils"
"strconv"
"strings"
"time" "time"
"github.com/jamesruan/go-rfc1924/base85"
) )
func curve25519_key_pair() (private_key []byte, public_key []byte, err error) { func curve25519_key_pair() (private_key []byte, public_key []byte, err error) {
@@ -103,6 +106,15 @@ func EncodePublicKey(pubkey []byte, encryption_protocol string) (ans string, err
return return
} }
func DecodePublicKey(raw string) (encryption_protocol string, pubkey []byte, err error) {
encryption_protocol, encoded_pubkey, found := strings.Cut(raw, ":")
if !found {
return "", nil, fmt.Errorf("Invalid encoded pubkey, no : in string")
}
pubkey, err = b85_decode(encoded_pubkey)
return
}
func Encrypt_cmd(cmd *utils.RemoteControlCmd, password string, other_pubkey []byte, encryption_protocol string) (encrypted_cmd utils.EncryptedRemoteControlCmd, err error) { func Encrypt_cmd(cmd *utils.RemoteControlCmd, password string, other_pubkey []byte, encryption_protocol string) (encrypted_cmd utils.EncryptedRemoteControlCmd, err error) {
cmd.Password = password cmd.Password = password
cmd.Timestamp = time.Now().UnixNano() cmd.Timestamp = time.Now().UnixNano()
@@ -119,4 +131,21 @@ func Encrypt_cmd(cmd *utils.RemoteControlCmd, password string, other_pubkey []by
return return
} }
func Encrypt_data(data []byte, other_pubkey []byte, encryption_protocol string) (ans []byte, err error) {
d := make([]byte, 0, len(data)+32)
d = append(d, []byte(fmt.Sprintf("%s:", strconv.FormatInt(time.Now().UnixNano(), 10)))...)
d = append(d, data...)
iv, tag, ciphertext, pubkey, err := encrypt(d, other_pubkey, encryption_protocol)
if err != nil {
return
}
ec := utils.EncryptedRemoteControlCmd{
IV: b85_encode(iv), Tag: b85_encode(tag), Pubkey: b85_encode(pubkey), Encrypted: b85_encode(ciphertext)}
if encryption_protocol != "1" {
ec.EncProto = encryption_protocol
}
ans, err = json.Marshal(ec)
return
}
// }}} // }}}