diff --git a/irc/cloaking/cloak.go b/irc/cloaking/cloak.go new file mode 100644 index 00000000..9de8e967 --- /dev/null +++ b/irc/cloaking/cloak.go @@ -0,0 +1,177 @@ +// Copyright (c) 2017 Daniel Oaks +// released under the MIT license + +// Package cloak implements IP address cloaking for IRC. +package cloak + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "net" + "strings" + + "encoding/base32" +) + +const ( + // MinKeyLength determines how many bytes our cloak keys should be, minimum. + // This MUST NOT be higher in future releases, or else it will break existing, + // working cloaking for everyone using key lengths this long. + MinKeyLength = 32 + // partLength is how long each octet is after being base32'd. + partLength = 10 +) + +var ( + errNetName = errors.New("NetName is not the right size (must be 1-10 characters long)") + errNotIPv4 = errors.New("The given address is not an IPv4 address") + errConfigDisabled = errors.New("Config has disabled IP cloaking") + errKeysTooShort = errors.New("Cloaking keys too short") + errKeysNotRandomEnough = errors.New("Cloaking keys aren't random enough") +) + +// Config controls whether we cloak, and if we do what values are used to do so. +type Config struct { + // Enabled controls whether cloaking is performed. + Enabled bool + // NetName is the name used for the network in cloaked addresses. + NetName string + // IPv4KeyString is used to cloak the `a`, `b`, `c` and `d` parts of the IP address. + // it is split up into the separate A/B/C/D keys below. + IPv4KeysString []string `yaml:"ipv4-keys"` + IPv4KeyA []byte + IPv4KeyB []byte + IPv4KeyC []byte + IPv4KeyD []byte +} + +// CheckConfig checks whether we're configured correctly. +func (config *Config) CheckConfig() error { + if config.Enabled { + // IPv4 cloak keys + if len(config.IPv4KeysString) < 4 { + return errKeysTooShort + } + + keyA, errA := base64.StdEncoding.DecodeString(config.IPv4KeysString[0]) + keyB, errB := base64.StdEncoding.DecodeString(config.IPv4KeysString[1]) + keyC, errC := base64.StdEncoding.DecodeString(config.IPv4KeysString[2]) + keyD, errD := base64.StdEncoding.DecodeString(config.IPv4KeysString[3]) + + if errA != nil || errB != nil || errC != nil || errD != nil { + return fmt.Errorf("Could not decode IPv4 cloak keys") + } + if len(keyA) < MinKeyLength || len(keyB) < MinKeyLength || len(keyC) < MinKeyLength || len(keyD) < MinKeyLength { + return errKeysTooShort + } + + config.IPv4KeyA = keyA + config.IPv4KeyB = keyB + config.IPv4KeyC = keyC + config.IPv4KeyD = keyD + + // try cloaking IPs to confirm everything works properly + _, err := IPv4(net.ParseIP("8.8.8.8"), config) + if err != nil { + return err + } + } + return nil +} + +// GenerateCloakKey generates one cloak key. +func GenerateCloakKey() (string, error) { + keyBytes := make([]byte, MinKeyLength) + _, err := rand.Read(keyBytes) + if err != nil { + return "", fmt.Errorf("Could not generate random bytes for cloak key: %s", err.Error()) + } + + return base64.StdEncoding.EncodeToString(keyBytes), nil +} + +// IsRandomEnough makes sure people are using keys that are random enough. +func IsRandomEnough(key []byte) bool { + //TODO(dan): actually find out how to calc this + return true +} + +// toByteSlice is used for converting sha512 output from [64]byte to []byte. +func toByteSlice(orig [64]byte) []byte { + var new []byte + for _, val := range orig { + new = append(new, val) + } + return new +} + +// hashOctet does the heavy lifting in terms of hashing and returned an appropriate hashed octet +func hashOctet(key []byte, data string) string { + sig := hmac.New(sha256.New, key) + sig.Write([]byte(data)) + raw := sig.Sum(nil) + return strings.ToLower(base32.StdEncoding.EncodeToString(raw)) +} + +// IPv4 returns a cloaked IPv4 address +// +// IPv4 addresses can be represented as a.b.c.d, where `a` is the least unique +// part of the address and `d` is the most unique part. +// +// `a` is unique for a given a.*.*.*, and `d` is unique for a given, specific +// a.b.c.d address. That is, if you have 1.2.3.4 and 2.3.4.4, the `d` part of +// both addresses should differ to prevent discoverability. In the same way, +// if you have 4.5.6.7 and 4.3.2.1 then the `a` part of those addresses will +// be the same value. This ensures chanops can properly ban dodgy people as +// they need to do so. +func IPv4(address net.IP, config *Config) (string, error) { + if !config.Enabled { + return "", errConfigDisabled + } + if len(config.NetName) < 1 || 10 < len(config.NetName) { + return "", errNetName + } + + // make sure the IP address is an IPv4 address. + // from this point on we can assume `address` is a 4-byte slice + if address.To4() == nil { + return "", errNotIPv4 + } + + // check randomness of cloak keys + if len(config.IPv4KeyA) < MinKeyLength || len(config.IPv4KeyB) < MinKeyLength || len(config.IPv4KeyC) < MinKeyLength || len(config.IPv4KeyD) < MinKeyLength { + return "", errKeysTooShort + } + if !IsRandomEnough(config.IPv4KeyA) || !IsRandomEnough(config.IPv4KeyB) || !IsRandomEnough(config.IPv4KeyC) || !IsRandomEnough(config.IPv4KeyD) { + return "", errKeysNotRandomEnough + } + + // get IP parts + address = address.To4() + partA := address[0] + partB := address[1] + partC := address[2] + partD := address[3] + + // cloak `a` part of IP address. + data := fmt.Sprintf("%d", partA) + partAHashed := hashOctet(config.IPv4KeyA, data)[:partLength] + + // cloak `b` part of IP address. + data = fmt.Sprintf("%d%d", partB, partA) + partBHashed := hashOctet(config.IPv4KeyB, data)[:partLength] + + // cloak `c` part of IP address. + data = fmt.Sprintf("%d%d%d", partC, partB, partA) + partCHashed := hashOctet(config.IPv4KeyC, data)[:partLength] + + // cloak `d` part of IP address. + data = fmt.Sprintf("%d%d%d%d", partD, partC, partB, partA) + partDHashed := hashOctet(config.IPv4KeyD, data)[:partLength] + + return fmt.Sprintf("%s.%s.%s.%s.%s-cloaked", partAHashed, partBHashed, partCHashed, partDHashed, strings.ToLower(config.NetName)), nil +} diff --git a/irc/config.go b/irc/config.go index ac840f7e..8f297606 100644 --- a/irc/config.go +++ b/irc/config.go @@ -14,6 +14,7 @@ import ( "strings" "time" + cloak "github.com/oragono/oragono/irc/cloaking" "github.com/oragono/oragono/irc/custime" "github.com/oragono/oragono/irc/logger" @@ -187,7 +188,8 @@ type StackImpactConfig struct { // Config defines the overall configuration. type Config struct { Network struct { - Name string + Name string + IPCloaking cloak.Config `yaml:"ip-cloaking"` } Server struct { @@ -418,6 +420,10 @@ func LoadConfig(filename string) (config *Config, err error) { return nil, fmt.Errorf("STS port is incorrect, should be 0 if disabled: %d", config.Server.STS.Port) } } + err = config.Network.IPCloaking.CheckConfig() + if err != nil { + return nil, fmt.Errorf("Could not parse IP cloaking: %s", err.Error()) + } if config.Server.ConnectionThrottle.Enabled { config.Server.ConnectionThrottle.Duration, err = time.ParseDuration(config.Server.ConnectionThrottle.DurationString) if err != nil { diff --git a/oragono.go b/oragono.go index 40ceb7c7..4c0839af 100644 --- a/oragono.go +++ b/oragono.go @@ -15,6 +15,7 @@ import ( "github.com/docopt/docopt-go" "github.com/oragono/oragono/irc" + cloak "github.com/oragono/oragono/irc/cloaking" "github.com/oragono/oragono/irc/logger" "github.com/oragono/oragono/mkcerts" stackimpact "github.com/stackimpact/stackimpact-go" @@ -28,6 +29,7 @@ Usage: oragono initdb [--conf ] [--quiet] oragono upgradedb [--conf ] [--quiet] oragono genpasswd [--conf ] [--quiet] + oragono genkeys oragono mkcerts [--conf ] [--quiet] oragono run [--conf ] [--quiet] oragono -h | --help @@ -40,29 +42,35 @@ Options: arguments, _ := docopt.Parse(usage, nil, true, version, false) + // load config and logger for everything but genkeys + var err error configfile := arguments["--conf"].(string) - config, err := irc.LoadConfig(configfile) - if err != nil { - log.Fatal("Config file did not load successfully:", err.Error()) - } + var config *irc.Config + var logman *logger.Manager + if !arguments["genkeys"].(bool) { + config, err = irc.LoadConfig(configfile) + if err != nil { + log.Fatal("Config file did not load successfully:", err.Error()) + } - // assemble separate log configs - var logConfigs []logger.Config - for _, lConfig := range config.Logging { - logConfigs = append(logConfigs, logger.Config{ - MethodStdout: lConfig.MethodStdout, - MethodStderr: lConfig.MethodStderr, - MethodFile: lConfig.MethodFile, - Filename: lConfig.Filename, - Level: lConfig.Level, - Types: lConfig.Types, - ExcludedTypes: lConfig.ExcludedTypes, - }) - } + // assemble separate log configs + var logConfigs []logger.Config + for _, lConfig := range config.Logging { + logConfigs = append(logConfigs, logger.Config{ + MethodStdout: lConfig.MethodStdout, + MethodStderr: lConfig.MethodStderr, + MethodFile: lConfig.MethodFile, + Filename: lConfig.Filename, + Level: lConfig.Level, + Types: lConfig.Types, + ExcludedTypes: lConfig.ExcludedTypes, + }) + } - logger, err := logger.NewManager(logConfigs...) - if err != nil { - log.Fatal("Logger did not load successfully:", err.Error()) + logman, err = logger.NewManager(logConfigs...) + if err != nil { + log.Fatal("Logger did not load successfully:", err.Error()) + } } if arguments["genpasswd"].(bool) { @@ -78,6 +86,65 @@ Options: } fmt.Print("\n") fmt.Println(encoded) + } else if arguments["genkeys"].(bool) { + fmt.Println("Here are your cloak keys:") + + // generate IPv4 keys + keyA, err := cloak.GenerateCloakKey() + if err != nil { + log.Fatal("Error generating cloak keys:", err) + } + keyB, err := cloak.GenerateCloakKey() + if err != nil { + log.Fatal("Error generating cloak keys:", err) + } + keyC, err := cloak.GenerateCloakKey() + if err != nil { + log.Fatal("Error generating cloak keys:", err) + } + keyD, err := cloak.GenerateCloakKey() + if err != nil { + log.Fatal("Error generating cloak keys:", err) + } + + fmt.Println(fmt.Sprintf(`ipv4-keys: ["%s", "%s", "%s", "%s"]`, keyA, keyB, keyC, keyD)) + + // generate IPv6 keys + keyA, err = cloak.GenerateCloakKey() + if err != nil { + log.Fatal("Error generating cloak keys:", err) + } + keyB, err = cloak.GenerateCloakKey() + if err != nil { + log.Fatal("Error generating cloak keys:", err) + } + keyC, err = cloak.GenerateCloakKey() + if err != nil { + log.Fatal("Error generating cloak keys:", err) + } + keyD, err = cloak.GenerateCloakKey() + if err != nil { + log.Fatal("Error generating cloak keys:", err) + } + keyE, err := cloak.GenerateCloakKey() + if err != nil { + log.Fatal("Error generating cloak keys:", err) + } + keyF, err := cloak.GenerateCloakKey() + if err != nil { + log.Fatal("Error generating cloak keys:", err) + } + keyG, err := cloak.GenerateCloakKey() + if err != nil { + log.Fatal("Error generating cloak keys:", err) + } + keyH, err := cloak.GenerateCloakKey() + if err != nil { + log.Fatal("Error generating cloak keys:", err) + } + + fmt.Println(fmt.Sprintf(`ipv6-keys: ["%s", "%s", "%s", "%s", "%s", "%s", "%s", "%s"]`, keyA, keyB, keyC, keyD, keyE, keyF, keyG, keyH)) + } else if arguments["initdb"].(bool) { irc.InitDB(config.Datastore.Path) if !arguments["--quiet"].(bool) { @@ -108,13 +175,13 @@ Options: } else if arguments["run"].(bool) { rand.Seed(time.Now().UTC().UnixNano()) if !arguments["--quiet"].(bool) { - logger.Info("startup", fmt.Sprintf("Oragono v%s starting", irc.SemVer)) + logman.Info("startup", fmt.Sprintf("Oragono v%s starting", irc.SemVer)) } // profiling if config.Debug.StackImpact.Enabled { if config.Debug.StackImpact.AgentKey == "" || config.Debug.StackImpact.AppName == "" { - logger.Error("startup", "Could not start StackImpact - agent-key or app-name are undefined") + logman.Error("startup", "Could not start StackImpact - agent-key or app-name are undefined") return } @@ -122,22 +189,22 @@ Options: agent.Start(stackimpact.Options{AgentKey: config.Debug.StackImpact.AgentKey, AppName: config.Debug.StackImpact.AppName}) defer agent.RecordPanic() - logger.Info("startup", fmt.Sprintf("StackImpact profiling started as %s", config.Debug.StackImpact.AppName)) + logman.Info("startup", fmt.Sprintf("StackImpact profiling started as %s", config.Debug.StackImpact.AppName)) } // warning if running a non-final version if strings.Contains(irc.SemVer, "unreleased") { - logger.Warning("startup", "You are currently running an unreleased beta version of Oragono that may be unstable and could corrupt your database.\nIf you are running a production network, please download the latest build from https://oragono.io/downloads.html and run that instead.") + logman.Warning("startup", "You are currently running an unreleased beta version of Oragono that may be unstable and could corrupt your database.\nIf you are running a production network, please download the latest build from https://oragono.io/downloads.html and run that instead.") } - server, err := irc.NewServer(configfile, config, logger) + server, err := irc.NewServer(configfile, config, logman) if err != nil { - logger.Error("startup", fmt.Sprintf("Could not load server: %s", err.Error())) + logman.Error("startup", fmt.Sprintf("Could not load server: %s", err.Error())) return } if !arguments["--quiet"].(bool) { - logger.Info("startup", "Server running") - defer logger.Info("shutdown", fmt.Sprintf("Oragono v%s exiting", irc.SemVer)) + logman.Info("startup", "Server running") + defer logman.Info("shutdown", fmt.Sprintf("Oragono v%s exiting", irc.SemVer)) } server.Run() } diff --git a/oragono.yaml b/oragono.yaml index bfb961a8..af026ae4 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -5,6 +5,26 @@ network: # name of the network name: OragonoTest + # cloaking IP addresses and hostnames + ip-cloaking: + # enable the cloaking + enabled: false + + # short name to use in cloaked hostnames + netname: "testnet" + + # ipv4 cloak keys + # to generate these keys, run "oragono genkeys" + ipv4-keys: ["keyhere", "keyhere", "keyhere", "keyhere"] + + # ipv6 cloak keys + # to generate these keys, run "oragono genkeys" + ipv6-keys: ["keyhere", "keyhere", "keyhere", "keyhere", "keyhere", "keyhere", "keyhere", "keyhere"] + + # hostname cloaking keys + # to generate these keys, run "oragono genkeys" + hostname-keys: ["keyhere", "keyhere", "keyhere", "keyhere"] + # server configuration server: # server name