happ_crypto.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. package service
  2. import (
  3. "bytes"
  4. "crypto/rand"
  5. "crypto/rsa"
  6. "crypto/sha256"
  7. "crypto/x509"
  8. "encoding/base64"
  9. "encoding/hex"
  10. "encoding/pem"
  11. "fmt"
  12. "math/big"
  13. "net/url"
  14. "strconv"
  15. "strings"
  16. "unicode"
  17. "unicode/utf8"
  18. "golang.org/x/crypto/chacha20poly1305"
  19. )
  20. // vdfzfoff public key from Omegaplexx/hpwnr 3745cb96e2551e003cb217ab7705b4d67f8ac006, src/keys.rs.
  21. // Salted Crypt5 with separator V passed Android 4.3.0 and Windows 4.1.2 import/update probes.
  22. const happPublicKeyPEM = `-----BEGIN PUBLIC KEY-----
  23. MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9+umWSxp8coKnMONnI4u
  24. NvtPErJZt8VNgNb2XS+RrCMc9AFWZQH01ILr3Py/mviuqFgNLMEcPs3k6+ZPh6Sa
  25. OCXHmjQicGPJAw6Co6GQwO/b4vspHgOM4HSvX5r6SY1EKIHUSLIyRV28DfwJKdFv
  26. x2EKqypewlrAo4AV76uI/9U+1t40yHcVCj/OtFxsq+mMM6qySieTsA1q6C5raBrJ
  27. u3l/RWMxFYvDInYDs1IaTFGDFwSdFDqhNU19gPGloT/GApy+U32R6AGSxJymS2nh
  28. e6pm/M9bvsH0o0Oc1kyXsBpVN04n/a9gVVUoqODzrUyXDx7/jAzNJD43PWtblcz0
  29. ZNBKN50wvpSD5UuAQydwMT7xWJIpPaZqTUj/sg8hIm57XGlUxRCge17nB0Ff7sKO
  30. JAgaXVdbfqDdzx+PhSaZY9xfcAh/sHfE6hKaCQ9kIn5cjbx9bcYqZWnpuSOzSFg+
  31. CgMSqvG6rV6d+96dNMHuE0tRIUJ83xrLcm9hZJmJ6WDm6hteZbnb1k3eQF9c+XCF
  32. wSEvsWiXyduQmkVNJaCRXwy8tSaZp9JftALhRHMvd7Eq6ctAkvn7w0upynsAtLeL
  33. N8xZ5q1gcRgboydr588D3m8KF7mVuX/XRp2AG7hzyYdkQov9bfEfXIaBVlwHMKhy
  34. uPTxeM4Les6fvaHMSWJ+8EUCAwEAAQ==
  35. -----END PUBLIC KEY-----`
  36. const (
  37. happCrypt5Marker = "vdfzfoff"
  38. happPublicKeyFingerprint = "22319c7b13647897bf5fd4f827ba92bf3946d738007a0054ccd931c31f221768"
  39. // Bound application work before encoding; this is not a promised Happ client limit.
  40. happMaxSourceBytes = 8192
  41. )
  42. func encryptHappLink(source string) (string, error) {
  43. key, err := checkedHappPublicKey([]byte(happPublicKeyPEM))
  44. if err != nil {
  45. return "", err
  46. }
  47. return encryptHappSource(source, key)
  48. }
  49. func encryptHappSource(source string, key *rsa.PublicKey) (string, error) {
  50. if !validHappRSAKey(key) {
  51. return "", ErrHappLinkUnavailable
  52. }
  53. if len(source) > happMaxSourceBytes {
  54. return "", ErrHappSourceTooLong
  55. }
  56. if len(source) == 0 || !utf8.ValidString(source) || strings.IndexFunc(source, unicode.IsControl) >= 0 {
  57. return "", ErrHappLinkUnavailable
  58. }
  59. parsed, err := url.Parse(source)
  60. if err != nil || !parsed.IsAbs() || parsed.Opaque != "" || parsed.Hostname() == "" ||
  61. parsed.User != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") {
  62. return "", ErrHappLinkUnavailable
  63. }
  64. const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
  65. const alnum = letters + "0123456789"
  66. sessionKey := make([]byte, 32)
  67. if _, err := rand.Read(sessionKey); err != nil {
  68. return "", fmt.Errorf("happ key randomness: %w", err)
  69. }
  70. nonce, err := randomHappCharacters(12, alnum)
  71. if err != nil {
  72. return "", err
  73. }
  74. tag, err := randomHappCharacters(2, letters)
  75. if err != nil {
  76. return "", err
  77. }
  78. salt, err := randomHappCharacters(8, alnum)
  79. if err != nil {
  80. return "", err
  81. }
  82. wrappedKey := make([]byte, 32)
  83. for i := range wrappedKey {
  84. wrappedKey[i] = sessionKey[i] ^ salt[i%8]
  85. }
  86. rsaPlain := swapHappPairs([]byte(base64.StdEncoding.EncodeToString(wrappedKey)))
  87. //nolint:staticcheck // Happ Crypt5 requires PKCS#1 v1.5 key wrapping; OAEP changes the wire format.
  88. rsaCipher, err := rsa.EncryptPKCS1v15(rand.Reader, key, rsaPlain)
  89. if err != nil {
  90. return "", fmt.Errorf("happ RSA wrapping: %w", err)
  91. }
  92. aead, err := chacha20poly1305.New(sessionKey)
  93. if err != nil {
  94. return "", fmt.Errorf("happ AEAD initialization: %w", err)
  95. }
  96. // Parsing validates the URL but must not normalize its UTF-8, escapes, or query bytes.
  97. plain := swapHappPairs([]byte(base64.StdEncoding.EncodeToString([]byte(source))))
  98. cipherB64 := base64.StdEncoding.EncodeToString(aead.Seal(nil, nonce, plain, nil))
  99. body := string(nonce) + string(tag) + string(salt) + strconv.Itoa(len(cipherB64)) +
  100. "V" + cipherB64 + base64.StdEncoding.EncodeToString(rsaCipher)
  101. frame := []byte(happCrypt5Marker[:4] + body + happCrypt5Marker[4:])
  102. for i := 0; i+3 < len(frame); i += 4 {
  103. frame[i], frame[i+2] = frame[i+2], frame[i]
  104. frame[i+1], frame[i+3] = frame[i+3], frame[i+1]
  105. }
  106. return "happ://crypt5/" + string(frame), nil
  107. }
  108. func checkedHappPublicKey(pemData []byte) (*rsa.PublicKey, error) {
  109. trimmed := bytes.TrimSpace(pemData)
  110. block, rest := pem.Decode(trimmed)
  111. if !bytes.HasPrefix(trimmed, []byte("-----BEGIN PUBLIC KEY-----")) || block == nil ||
  112. block.Type != "PUBLIC KEY" || len(block.Headers) != 0 || len(bytes.TrimSpace(rest)) != 0 {
  113. return nil, ErrHappLinkUnavailable
  114. }
  115. parsed, err := x509.ParsePKIXPublicKey(block.Bytes)
  116. if err != nil {
  117. return nil, ErrHappLinkUnavailable
  118. }
  119. key, ok := parsed.(*rsa.PublicKey)
  120. if !ok || !validHappRSAKey(key) {
  121. return nil, ErrHappLinkUnavailable
  122. }
  123. spki, err := x509.MarshalPKIXPublicKey(key)
  124. if err != nil {
  125. return nil, ErrHappLinkUnavailable
  126. }
  127. fingerprint := sha256.Sum256(spki)
  128. // The marker selects the client's private key, so accepting any RSA public key would be incorrect.
  129. if hex.EncodeToString(fingerprint[:]) != happPublicKeyFingerprint {
  130. return nil, ErrHappLinkUnavailable
  131. }
  132. return key, nil
  133. }
  134. func validHappRSAKey(key *rsa.PublicKey) bool {
  135. return key != nil && key.N != nil && key.N.Sign() > 0 && key.N.BitLen() == 4096 && key.N.Bit(0) == 1 && key.E == 65537
  136. }
  137. func randomHappCharacters(length int, alphabet string) ([]byte, error) {
  138. result := make([]byte, length)
  139. limit := big.NewInt(int64(len(alphabet)))
  140. for i := range result {
  141. index, err := rand.Int(rand.Reader, limit)
  142. if err != nil {
  143. return nil, fmt.Errorf("happ character randomness: %w", err)
  144. }
  145. result[i] = alphabet[index.Int64()]
  146. }
  147. return result, nil
  148. }
  149. func swapHappPairs(data []byte) []byte {
  150. for i := 0; i+1 < len(data); i += 2 {
  151. data[i], data[i+1] = data[i+1], data[i]
  152. }
  153. return data
  154. }