package main

import "strings"

// ---------------------------------------------------------------------------
// The ssh_config keyword table
// ---------------------------------------------------------------------------
//
// This is the documented subset SSHDesk recognises. A keyword outside this
// table is reported as unknown with a nearest-match suggestion; it is still
// parsed and still resolved, because ssh itself would only warn.

// knownKeywords maps the lowercased keyword to its canonical spelling.
var knownKeywords = map[string]string{}

// canonicalKeywords is the list used to build knownKeywords and to power the
// nearest-match suggestion for unknown keywords.
var canonicalKeywords = []string{
	"AddKeysToAgent", "AddressFamily", "BatchMode", "BindAddress",
	"BindInterface", "CanonicalDomains", "CanonicalizeFallbackLocal",
	"CanonicalizeHostname", "CanonicalizeMaxDots", "CanonicalizePermittedCNAMEs",
	"CASignatureAlgorithms", "CertificateFile", "ChannelTimeout", "CheckHostIP",
	"Ciphers", "ClearAllForwardings", "Compression", "ConnectionAttempts",
	"ConnectTimeout", "ControlMaster", "ControlPath", "ControlPersist",
	"DynamicForward", "EnableEscapeCommandline", "EnableSSHKeysign",
	"EscapeChar", "ExitOnForwardFailure", "FingerprintHash",
	"ForkAfterAuthentication", "ForwardAgent", "ForwardX11",
	"ForwardX11Timeout", "ForwardX11Trusted", "GatewayPorts",
	"GlobalKnownHostsFile", "GSSAPIAuthentication",
	"GSSAPIDelegateCredentials", "HashKnownHosts", "Host",
	"HostbasedAcceptedAlgorithms", "HostbasedAuthentication",
	"HostKeyAlgorithms", "HostKeyAlias", "HostName", "IdentitiesOnly",
	"IdentityAgent", "IdentityFile", "IgnoreUnknown", "Include", "IPQoS",
	"KbdInteractiveAuthentication", "KbdInteractiveDevices", "KexAlgorithms",
	"KnownHostsCommand", "LocalCommand", "LocalForward", "LogLevel",
	"LogVerbose", "MACs", "Match", "NoHostAuthenticationForLocalhost",
	"NumberOfPasswordPrompts", "ObscureKeystrokeTiming",
	"PasswordAuthentication", "PermitLocalCommand", "PermitRemoteOpen",
	"PKCS11Provider", "Port", "PreferredAuthentications", "ProxyCommand",
	"ProxyJump", "ProxyUseFdpass", "PubkeyAcceptedAlgorithms",
	"PubkeyAuthentication", "RekeyLimit", "RemoteCommand", "RemoteForward",
	"RequestTTY", "RequiredRSASize", "RevokedHostKeys", "SecurityKeyProvider",
	"SendEnv", "ServerAliveCountMax", "ServerAliveInterval", "SessionType",
	"SetEnv", "StdinNull", "StreamLocalBindMask", "StreamLocalBindUnlink",
	"StrictHostKeyChecking", "SyslogFacility", "TCPKeepAlive", "Tag", "Tunnel",
	"TunnelDevice", "UpdateHostKeys", "User", "UserKnownHostsFile",
	"VerifyHostKeyDNS", "VisualHostKey", "XAuthLocation",
}

// deprecatedKeywords are accepted by some ssh versions but should not appear
// in a modern config. The value is the advice printed by `check`.
var deprecatedKeywords = map[string]string{
	"cipher":                  "removed in OpenSSH 7.6; use Ciphers",
	"compressionlevel":        "removed in OpenSSH 7.4; it only ever applied to SSHv1",
	"protocol":                "removed in OpenSSH 7.6; SSHv1 no longer exists",
	"rhostsrsaauthentication": "removed in OpenSSH 7.4 (SSHv1 only)",
	"rsaauthentication":       "removed in OpenSSH 7.4 (SSHv1 only)",
	"useprivilegedport":       "removed in OpenSSH 7.5",
	"useroaming":              "removed in OpenSSH 7.1p2 (CVE-2016-0777)",
	"gssapitrustdns":          "not present in portable OpenSSH by default",
	"challengeresponseauthentication": "renamed to KbdInteractiveAuthentication " +
		"in OpenSSH 8.7",
}

// listKeywords accumulate: every matching block contributes a value instead of
// the first one winning outright. This is real OpenSSH behaviour and it is the
// only exception to first-obtained-value-wins.
var listKeywords = map[string]bool{
	"identityfile":                true,
	"certificatefile":             true,
	"localforward":                true,
	"remoteforward":               true,
	"dynamicforward":              true,
	"sendenv":                     true,
	"setenv":                      true,
	"canonicalizepermittedcnames": true,
}

func init() {
	for _, k := range canonicalKeywords {
		knownKeywords[strings.ToLower(k)] = k
	}
	// OpenSSH accepts "Hostname" and "HostName" identically; the lowercase
	// map handles that, but the alias below keeps the canonical spelling.
	knownKeywords["hostname"] = "HostName"
	knownKeywords["macs"] = "MACs"
}

func isKnownKeyword(lower string) bool {
	_, ok := knownKeywords[lower]
	if ok {
		return true
	}
	_, ok = deprecatedKeywords[lower]
	return ok
}

func canonicalKeyword(lower string) string {
	if c, ok := knownKeywords[lower]; ok {
		return c
	}
	return lower
}

// ---------------------------------------------------------------------------
// Nearest-match suggestion
// ---------------------------------------------------------------------------

// editDistance is the standard Levenshtein distance over bytes.
func editDistance(a, b string) int {
	if a == b {
		return 0
	}
	if len(a) == 0 {
		return len(b)
	}
	if len(b) == 0 {
		return len(a)
	}
	prev := make([]int, len(b)+1)
	cur := make([]int, len(b)+1)
	for j := 0; j <= len(b); j++ {
		prev[j] = j
	}
	for i := 1; i <= len(a); i++ {
		cur[0] = i
		for j := 1; j <= len(b); j++ {
			cost := 1
			if a[i-1] == b[j-1] {
				cost = 0
			}
			del := prev[j] + 1
			ins := cur[j-1] + 1
			sub := prev[j-1] + cost
			m := del
			if ins < m {
				m = ins
			}
			if sub < m {
				m = sub
			}
			cur[j] = m
		}
		prev, cur = cur, prev
	}
	return prev[len(b)]
}

// suggestKeyword returns the closest known keyword to the given unknown one,
// or "" when nothing is close enough to be worth printing.
func suggestKeyword(unknown string) string {
	u := strings.ToLower(unknown)
	best, bestDist := "", 1<<30
	for _, k := range canonicalKeywords {
		d := editDistance(u, strings.ToLower(k))
		if d < bestDist || (d == bestDist && k < best) {
			best, bestDist = k, d
		}
	}
	// Allow roughly one edit per three characters, capped at 4.
	limit := len(u)/3 + 1
	if limit > 4 {
		limit = 4
	}
	if bestDist > limit {
		return ""
	}
	return best
}

// ---------------------------------------------------------------------------
// Settings that quietly weaken security
// ---------------------------------------------------------------------------

type weakRule struct {
	Key      string // lowercased keyword
	Value    string // lowercased value that triggers it, "" for any value
	Severity string
	Message  string
}

var weakRules = []weakRule{
	{"stricthostkeychecking", "no", "error",
		"host key verification is disabled: ssh will silently accept any key, " +
			"which is exactly what a man-in-the-middle needs"},
	{"stricthostkeychecking", "off", "error",
		"host key verification is disabled ('off' is a synonym for 'no')"},
	{"stricthostkeychecking", "accept-new", "info",
		"new host keys are accepted without asking; changed keys are still " +
			"refused. Weaker than 'yes' but far better than 'no'"},
	{"userknownhostsfile", "/dev/null", "error",
		"host keys are thrown away, so every connection is a first connection " +
			"and key changes can never be detected"},
	{"forwardagent", "yes", "warning",
		"agent forwarding lets anyone with root on the remote host use your " +
			"keys for as long as you are connected"},
	{"forwardx11", "yes", "warning",
		"X11 forwarding exposes your local X server to the remote host"},
	{"forwardx11trusted", "yes", "warning",
		"trusted X11 forwarding disables the X11 security extension entirely"},
	{"gssapidelegatecredentials", "yes", "warning",
		"delegating GSSAPI credentials hands your Kerberos ticket to the server"},
	{"checkhostip", "no", "info",
		"the host key is no longer checked against the server's IP address"},
	{"nohostauthenticationforlocalhost", "yes", "warning",
		"host keys are not verified for localhost, which matters when local " +
			"ports are forwarded to somewhere else"},
	{"permitlocalcommand", "yes", "info",
		"the remote side of an escape sequence can run local commands"},
	{"batchmode", "yes", "info",
		"password and passphrase prompts are disabled; connections fail " +
			"instead of prompting"},
	{"identitiesonly", "no", "info",
		"every key in your agent is offered to the server, which leaks the " +
			"list of hosts you have keys for"},
	{"hashknownhosts", "no", "info",
		"known_hosts is stored in clear text, so a reader of the file learns " +
			"every host you connect to"},
}

// weakAlgorithms flags obsolete primitives inside an algorithm list.
var weakAlgorithmKeywords = map[string]bool{
	"ciphers":                     true,
	"macs":                        true,
	"kexalgorithms":               true,
	"hostkeyalgorithms":           true,
	"pubkeyacceptedalgorithms":    true,
	"hostbasedacceptedalgorithms": true,
	"casignaturealgorithms":       true,
}

var weakAlgorithms = map[string]string{
	"3des-cbc":                           "56-bit effective 3DES",
	"blowfish-cbc":                       "64-bit block cipher, birthday-bound attacks",
	"cast128-cbc":                        "64-bit block cipher",
	"arcfour":                            "RC4 is broken",
	"arcfour128":                         "RC4 is broken",
	"arcfour256":                         "RC4 is broken",
	"hmac-md5":                           "MD5 is broken",
	"hmac-md5-96":                        "MD5 is broken",
	"hmac-sha1":                          "SHA-1 is deprecated",
	"hmac-sha1-96":                       "SHA-1 is deprecated",
	"umac-64@openssh.com":                "64-bit tag is too short",
	"diffie-hellman-group1-sha1":         "1024-bit group, SHA-1",
	"diffie-hellman-group14-sha1":        "SHA-1",
	"diffie-hellman-group-exchange-sha1": "SHA-1",
	"ssh-dss":                            "DSA is limited to 1024 bits",
	"ssh-dss-cert-v01@openssh.com":       "DSA is limited to 1024 bits",
	"ssh-rsa":                            "RSA with SHA-1 signatures",
	"ssh-rsa-cert-v01@openssh.com":       "RSA with SHA-1 signatures",
	"rijndael-cbc@lysator.liu.se":        "non-standard alias for AES-CBC",
	"hmac-ripemd160":                     "RIPEMD-160 is deprecated",
}
