diff --git a/cmd_onion.go b/cmd_onion.go index fd12ed2..2c62417 100644 --- a/cmd_onion.go +++ b/cmd_onion.go @@ -8,55 +8,142 @@ package bulb import ( + "crypto" + "crypto/rsa" + "encoding/base64" "fmt" "strings" + + "github.com/yawning/bulb/utils/pkcs1" ) // OnionInfo is the result of the AddOnion command. type OnionInfo struct { - OnionID string - KeyType string - Key string + OnionID string + PrivateKey crypto.PrivateKey RawResponse *Response } -// AddOnion issues an ADD_ONION command and returns the parsed response. -func (c *Conn) AddOnion(virtPort int, target, keyType, keyContent string, new bool) (*OnionInfo, error) { - var fields []string - request := "ADD_ONION " - onionInfo := OnionInfo{} +// OnionPrivateKey is a unknown Onion private key (crypto.PublicKey). +type OnionPrivateKey struct { + KeyType string + Key string +} - if new { - request += "NEW:BEST" - } else { - request += fmt.Sprintf("%s:%s", keyType, keyContent) +// OnionPortSpec is a Onion VirtPort/Target pair. +type OnionPortSpec struct { + VirtPort uint16 + Target string +} + +// AddOnion issues an ADD_ONION command and returns the parsed response. +func (c *Conn) AddOnion(ports []OnionPortSpec, key crypto.PrivateKey, oneshot bool) (*OnionInfo, error) { + const keyTypeRSA = "RSA1024" + var err error + + var portStr string + if ports == nil { + return nil, newProtocolError("invalid port specification") + } + for _, v := range ports { + portStr += fmt.Sprintf(" Port=%d", v.VirtPort) + if v.Target != "" { + portStr += "," + v.Target + } + } + + var hsKeyType, hsKeyStr string + if key != nil { + switch t := key.(type) { + case *rsa.PrivateKey: + rsaPK, _ := key.(*rsa.PrivateKey) + if rsaPK.N.BitLen() != 1024 { + return nil, newProtocolError("invalid RSA key size") + } + pkDER, err := pkcs1.EncodePrivateKeyDER(rsaPK) + if err != nil { + return nil, newProtocolError("failed to serialize RSA key: %v", err) + } + hsKeyType = keyTypeRSA + hsKeyStr = base64.StdEncoding.EncodeToString(pkDER) + case *OnionPrivateKey: + genericPK, _ := key.(*OnionPrivateKey) + hsKeyType = genericPK.KeyType + hsKeyStr = genericPK.Key + default: + return nil, newProtocolError("unsupported private key type: %v", t) + } + } + + var resp *Response + if hsKeyStr == "" { + flags := " Flags=DiscardPK" + if !oneshot { + flags = "" + } + resp, err = c.Request("ADD_ONION NEW:BEST%s%s", portStr, flags) + } else { + resp, err = c.Request("ADD_ONION %s:%s%s", hsKeyType, hsKeyStr, portStr) } - request += fmt.Sprintf(" Port=%d,%s\n", virtPort, target) - response, err := c.Request(request) if err != nil { return nil, err } - onionInfo.RawResponse = response - fields = strings.Split(fmt.Sprintf("%s", response.Data), "ServiceID=") - fields = strings.Split(fields[1], " ") - onionInfo.OnionID = fields[0] + // Parse out the response. + var serviceID string + var hsPrivateKey crypto.PrivateKey + for _, l := range resp.Data { + const ( + serviceIDPrefix = "ServiceID=" + privateKeyPrefix = "PrivateKey=" + ) - if new { - fields = strings.Split(fmt.Sprintf("%s", response.Data), "PrivateKey=") - fields = strings.Split(fields[1], ":") - onionInfo.KeyType = fields[0] - fields = strings.Split(fields[1], "\n") - onionInfo.Key = fields[0] + if strings.HasPrefix(l, serviceIDPrefix) { + serviceID = strings.TrimPrefix(l, serviceIDPrefix) + } else if strings.HasPrefix(l, privateKeyPrefix) { + if oneshot || hsKeyStr != "" { + return nil, newProtocolError("received an unexpected private key") + } + hsKeyStr = strings.TrimPrefix(l, privateKeyPrefix) + splitKey := strings.SplitN(hsKeyStr, ":", 2) + if len(splitKey) != 2 { + return nil, newProtocolError("failed to parse private key type") + } + + switch splitKey[0] { + case keyTypeRSA: + keyBlob, err := base64.StdEncoding.DecodeString(splitKey[1]) + if err != nil { + return nil, newProtocolError("failed to base64 decode RSA key: %v", err) + } + hsPrivateKey, _, err = pkcs1.DecodePrivateKeyDER(keyBlob) + if err != nil { + return nil, newProtocolError("failed to deserialize RSA key: %v", err) + } + default: + hsPrivateKey := new(OnionPrivateKey) + hsPrivateKey.KeyType = splitKey[0] + hsPrivateKey.Key = splitKey[1] + } + } + } + if serviceID == "" { + // This should *NEVER* happen, since the command succeded, and the spec + // guarantees that this will always be present. + return nil, newProtocolError("failed to determine service ID") } - return &onionInfo, nil + oi := new(OnionInfo) + oi.RawResponse = resp + oi.OnionID = serviceID + oi.PrivateKey = hsPrivateKey + + return oi, nil } // DeleteOnion issues a DEL_ONION command and returns the parsed response. func (c *Conn) DeleteOnion(serviceID string) error { - var deleteCmd string = fmt.Sprintf("DEL_ONION %s\n", serviceID) - _, err := c.Request(deleteCmd) + _, err := c.Request("DEL_ONION %s", serviceID) return err }