mirror of
https://github.com/AlexxIT/go2rtc.git
synced 2025-12-24 12:57:56 +08:00
397 lines
9.0 KiB
Go
397 lines
9.0 KiB
Go
package hap
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/sha512"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305"
|
|
"github.com/AlexxIT/go2rtc/pkg/hap/curve25519"
|
|
"github.com/AlexxIT/go2rtc/pkg/hap/ed25519"
|
|
"github.com/AlexxIT/go2rtc/pkg/hap/hkdf"
|
|
"github.com/AlexxIT/go2rtc/pkg/hap/tlv8"
|
|
"github.com/tadglines/go-pkgs/crypto/srp"
|
|
)
|
|
|
|
type Server struct {
|
|
Pin string
|
|
DeviceID string
|
|
DevicePrivate []byte
|
|
|
|
// GetClientPublic may be nil, so client validation will be disabled
|
|
GetClientPublic func(id string) []byte
|
|
}
|
|
|
|
func (s *Server) ServerPublic() []byte {
|
|
return s.DevicePrivate[32:]
|
|
}
|
|
|
|
//func (s *Server) Status() string {
|
|
// if len(s.Pairings) == 0 {
|
|
// return StatusNotPaired
|
|
// }
|
|
// return StatusPaired
|
|
//}
|
|
|
|
func (s *Server) PairSetup(req *http.Request, rw *bufio.ReadWriter) (id string, publicKey []byte, err error) {
|
|
// STEP 1. Request from iPhone
|
|
var plainM1 struct {
|
|
State byte `tlv8:"6"`
|
|
Method byte `tlv8:"0"`
|
|
Flags uint32 `tlv8:"19"`
|
|
}
|
|
if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil {
|
|
return
|
|
}
|
|
if plainM1.State != StateM1 {
|
|
err = newRequestError(plainM1)
|
|
return
|
|
}
|
|
|
|
username := []byte("Pair-Setup")
|
|
|
|
// Stanford Secure Remote Password (SRP) / Password Authenticated Key Exchange (PAKE)
|
|
pake, err := srp.NewSRP("rfc5054.3072", sha512.New, keyDerivativeFuncRFC2945(username))
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
pake.SaltLength = 16
|
|
|
|
salt, verifier, err := pake.ComputeVerifier([]byte(s.Pin))
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
session := pake.NewServerSession(username, salt, verifier)
|
|
|
|
// STEP 2. Response to iPhone
|
|
plainM2 := struct {
|
|
State byte `tlv8:"6"`
|
|
PublicKey string `tlv8:"3"`
|
|
Salt string `tlv8:"2"`
|
|
}{
|
|
State: StateM2,
|
|
PublicKey: string(session.GetB()),
|
|
Salt: string(salt),
|
|
}
|
|
body, err := tlv8.Marshal(plainM2)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {
|
|
return
|
|
}
|
|
|
|
// STEP 3. Request from iPhone
|
|
if req, err = http.ReadRequest(rw.Reader); err != nil {
|
|
return
|
|
}
|
|
|
|
var plainM3 struct {
|
|
State byte `tlv8:"6"`
|
|
PublicKey string `tlv8:"3"`
|
|
Proof string `tlv8:"4"`
|
|
}
|
|
if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM3); err != nil {
|
|
return
|
|
}
|
|
if plainM3.State != StateM3 {
|
|
err = newRequestError(plainM3)
|
|
return
|
|
}
|
|
|
|
// important to compute key before verify client
|
|
sessionShared, err := session.ComputeKey([]byte(plainM3.PublicKey))
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if !session.VerifyClientAuthenticator([]byte(plainM3.Proof)) {
|
|
err = errors.New("hap: VerifyClientAuthenticator")
|
|
return
|
|
}
|
|
|
|
proof := session.ComputeAuthenticator([]byte(plainM3.Proof)) // server proof
|
|
|
|
// STEP 4. Response to iPhone
|
|
payloadM4 := struct {
|
|
State byte `tlv8:"6"`
|
|
Proof string `tlv8:"4"`
|
|
}{
|
|
State: StateM4,
|
|
Proof: string(proof),
|
|
}
|
|
if body, err = tlv8.Marshal(payloadM4); err != nil {
|
|
return
|
|
}
|
|
if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {
|
|
return
|
|
}
|
|
|
|
// STEP 5. Request from iPhone
|
|
if req, err = http.ReadRequest(rw.Reader); err != nil {
|
|
return
|
|
}
|
|
var cipherM5 struct {
|
|
State byte `tlv8:"6"`
|
|
EncryptedData string `tlv8:"5"`
|
|
}
|
|
if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &cipherM5); err != nil {
|
|
return
|
|
}
|
|
if cipherM5.State != StateM5 {
|
|
err = newRequestError(cipherM5)
|
|
return
|
|
}
|
|
|
|
// decrypt message using session shared
|
|
encryptKey, err := hkdf.Sha512(sessionShared, "Pair-Setup-Encrypt-Salt", "Pair-Setup-Encrypt-Info")
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
b, err := chacha20poly1305.Decrypt(encryptKey, "PS-Msg05", []byte(cipherM5.EncryptedData))
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// unpack message from TLV8
|
|
var plainM5 struct {
|
|
Identifier string `tlv8:"1"`
|
|
PublicKey string `tlv8:"3"`
|
|
Signature string `tlv8:"10"`
|
|
}
|
|
if err = tlv8.Unmarshal(b, &plainM5); err != nil {
|
|
return
|
|
}
|
|
|
|
// 3. verify client ID and Public
|
|
remoteSign, err := hkdf.Sha512(
|
|
sessionShared, "Pair-Setup-Controller-Sign-Salt", "Pair-Setup-Controller-Sign-Info",
|
|
)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
b = Append(remoteSign, plainM5.Identifier, plainM5.PublicKey)
|
|
if !ed25519.ValidateSignature([]byte(plainM5.PublicKey), b, []byte(plainM5.Signature)) {
|
|
err = errors.New("hap: ValidateSignature")
|
|
return
|
|
}
|
|
|
|
// 4. generate signature to our ID and Public
|
|
localSign, err := hkdf.Sha512(
|
|
sessionShared, "Pair-Setup-Accessory-Sign-Salt", "Pair-Setup-Accessory-Sign-Info",
|
|
)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
b = Append(localSign, s.DeviceID, s.ServerPublic()) // ServerPublic
|
|
signature, err := ed25519.Signature(s.DevicePrivate, b)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// 5. pack our ID and Public
|
|
plainM6 := struct {
|
|
Identifier string `tlv8:"1"`
|
|
PublicKey string `tlv8:"3"`
|
|
Signature string `tlv8:"10"`
|
|
}{
|
|
Identifier: s.DeviceID,
|
|
PublicKey: string(s.ServerPublic()),
|
|
Signature: string(signature),
|
|
}
|
|
if b, err = tlv8.Marshal(plainM6); err != nil {
|
|
return
|
|
}
|
|
|
|
// 6. encrypt message
|
|
b, err = chacha20poly1305.Encrypt(encryptKey, "PS-Msg06", b)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// STEP 6. Response to iPhone
|
|
cipherM6 := struct {
|
|
State byte `tlv8:"6"`
|
|
EncryptedData string `tlv8:"5"`
|
|
}{
|
|
State: StateM6,
|
|
EncryptedData: string(b),
|
|
}
|
|
if body, err = tlv8.Marshal(cipherM6); err != nil {
|
|
return
|
|
}
|
|
if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {
|
|
return
|
|
}
|
|
|
|
id = plainM5.Identifier
|
|
publicKey = []byte(plainM5.PublicKey)
|
|
|
|
return
|
|
}
|
|
|
|
func (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter) (id string, sessionKey []byte, err error) {
|
|
// Request from iPhone
|
|
var plainM1 struct {
|
|
State byte `tlv8:"6"`
|
|
PublicKey string `tlv8:"3"`
|
|
}
|
|
if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil {
|
|
return
|
|
}
|
|
if plainM1.State != StateM1 {
|
|
err = newRequestError(plainM1)
|
|
return
|
|
}
|
|
|
|
// Generate the key pair
|
|
sessionPublic, sessionPrivate := curve25519.GenerateKeyPair()
|
|
sessionShared, err := curve25519.SharedSecret(sessionPrivate, []byte(plainM1.PublicKey))
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
encryptKey, err := hkdf.Sha512(
|
|
sessionShared, "Pair-Verify-Encrypt-Salt", "Pair-Verify-Encrypt-Info",
|
|
)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
b := Append(sessionPublic, s.DeviceID, plainM1.PublicKey)
|
|
signature, err := ed25519.Signature(s.DevicePrivate, b)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// STEP M2. Response to iPhone
|
|
plainM2 := struct {
|
|
Identifier string `tlv8:"1"`
|
|
Signature string `tlv8:"10"`
|
|
}{
|
|
Identifier: s.DeviceID,
|
|
Signature: string(signature),
|
|
}
|
|
if b, err = tlv8.Marshal(plainM2); err != nil {
|
|
return
|
|
}
|
|
|
|
b, err = chacha20poly1305.Encrypt(encryptKey, "PV-Msg02", b)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
cipherM2 := struct {
|
|
State byte `tlv8:"6"`
|
|
PublicKey string `tlv8:"3"`
|
|
EncryptedData string `tlv8:"5"`
|
|
}{
|
|
State: StateM2,
|
|
PublicKey: string(sessionPublic),
|
|
EncryptedData: string(b),
|
|
}
|
|
body, err := tlv8.Marshal(cipherM2)
|
|
if err != nil {
|
|
return
|
|
}
|
|
if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {
|
|
return
|
|
}
|
|
|
|
// STEP M3. Request from iPhone
|
|
if req, err = http.ReadRequest(rw.Reader); err != nil {
|
|
return
|
|
}
|
|
|
|
var cipherM3 struct {
|
|
State byte `tlv8:"6"`
|
|
EncryptedData string `tlv8:"5"`
|
|
}
|
|
if err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &cipherM3); err != nil {
|
|
return
|
|
}
|
|
if cipherM3.State != StateM3 {
|
|
err = newRequestError(cipherM3)
|
|
return
|
|
}
|
|
|
|
b, err = chacha20poly1305.Decrypt(encryptKey, "PV-Msg03", []byte(cipherM3.EncryptedData))
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
var plainM3 struct {
|
|
Identifier string `tlv8:"1"`
|
|
Signature string `tlv8:"10"`
|
|
}
|
|
if err = tlv8.Unmarshal(b, &plainM3); err != nil {
|
|
return
|
|
}
|
|
|
|
if s.GetClientPublic != nil {
|
|
clientPublic := s.GetClientPublic(plainM3.Identifier)
|
|
if clientPublic == nil {
|
|
err = errors.New("hap: PairVerify with unknown client_id: " + plainM3.Identifier)
|
|
return
|
|
}
|
|
|
|
b = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic)
|
|
if !ed25519.ValidateSignature(clientPublic, b, []byte(plainM3.Signature)) {
|
|
err = errors.New("hap: ValidateSignature")
|
|
return
|
|
}
|
|
}
|
|
|
|
// STEP M4. Response to iPhone
|
|
payloadM4 := struct {
|
|
State byte `tlv8:"6"`
|
|
}{
|
|
State: StateM4,
|
|
}
|
|
if body, err = tlv8.Marshal(payloadM4); err != nil {
|
|
return
|
|
}
|
|
if err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {
|
|
return
|
|
}
|
|
|
|
id = plainM3.Identifier
|
|
sessionKey = sessionShared
|
|
|
|
return
|
|
}
|
|
|
|
func WriteResponse(w *bufio.Writer, statusCode int, contentType string, body []byte) error {
|
|
header := fmt.Sprintf(
|
|
"HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %d\r\n\r\n",
|
|
statusCode, http.StatusText(statusCode), contentType, len(body),
|
|
)
|
|
body = append([]byte(header), body...)
|
|
if _, err := w.Write(body); err != nil {
|
|
return err
|
|
}
|
|
return w.Flush()
|
|
}
|
|
|
|
//func WriteBackoff(rw *bufio.ReadWriter) error {
|
|
// plainM2 := struct {
|
|
// State byte `tlv8:"6"`
|
|
// Error byte `tlv8:"7"`
|
|
// }{
|
|
// State: StateM2,
|
|
// Error: 3, // BackoffError
|
|
// }
|
|
// body, err := tlv8.Marshal(plainM2)
|
|
// if err != nil {
|
|
// return err
|
|
// }
|
|
// return WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body)
|
|
//}
|