main
44ceea7 · 1 month ago 7 commits
  1// Package crypto implements the exact primitives the TP-Link router web UI
  2// uses (see libs/encrypt.js and libs/tpEncrypt.js):
  3//
  4//   - AES-128-CBC / PKCS7, where the key and IV are the UTF-8 bytes of
  5//     16-character ASCII strings (CryptoJS.enc.Utf8.parse semantics).
  6//   - RSA PKCS#1 v1.5 "type 2" encryption padding, with long inputs split
  7//     into fixed-size chunks and the hex blocks concatenated (the JS
  8//     getSignature() chunking).
  9//   - MD5 / SHA-256 credential hashing.
 10package main 
 11
 12import (
 13	"crypto/aes"
 14	"crypto/cipher"
 15	"crypto/md5"
 16	"crypto/rand"
 17	"crypto/rsa"
 18	"crypto/sha256"
 19	"encoding/base64"
 20	"encoding/hex"
 21	"fmt"
 22	"math/big"
 23	"strings"
 24)
 25
 26// PublicKey is an RSA public key parsed from the router's (n_hex, e_hex) pair.
 27type PublicKey struct {
 28	key      *rsa.PublicKey
 29	sizeHex  int // modulus size in hex chars (== bytes*2)
 30	maxChunk int // max plaintext bytes per block (k - 11 for PKCS#1 v1.5)
 31}
 32
 33// ParsePublicKey builds an RSA public key from hex modulus and exponent,
 34// exactly as encrypt.js setPublic(n, e) does.
 35func ParsePublicKey(nHex, eHex string) (*PublicKey, error) {
 36	n, ok := new(big.Int).SetString(nHex, 16)
 37	if !ok {
 38		return nil, fmt.Errorf("invalid RSA modulus hex")
 39	}
 40	e, ok := new(big.Int).SetString(eHex, 16)
 41	if !ok {
 42		return nil, fmt.Errorf("invalid RSA exponent hex")
 43	}
 44	k := (n.BitLen() + 7) / 8
 45	return &PublicKey{
 46		key:      &rsa.PublicKey{N: n, E: int(e.Int64())},
 47		sizeHex:  k * 2,
 48		maxChunk: k - 11,
 49	}, nil
 50}
 51
 52// EncryptChunked RSA-encrypts msg in maxChunk-sized pieces, concatenating the
 53// hex of each block (left-padded to the modulus byte length). This matches
 54// both su.encrypt (single password block) and getSignature (53-byte chunks).
 55func (p *PublicKey) EncryptChunked(msg string) (string, error) {
 56	raw := []byte(msg)
 57	var b strings.Builder
 58	for i := 0; i < len(raw); i += p.maxChunk {
 59		end := min(i + p.maxChunk, len(raw))
 60		ct, err := rsa.EncryptPKCS1v15(rand.Reader, p.key, raw[i:end])
 61		if err != nil {
 62			return "", err
 63		}
 64		h := hex.EncodeToString(ct)
 65		// left-pad to fixed block width (JS zero-pads short blocks)
 66		if len(h) < p.sizeHex {
 67			h = strings.Repeat("0", p.sizeHex-len(h)) + h
 68		}
 69		b.WriteString(h)
 70	}
 71	return b.String(), nil
 72}
 73
 74// AESEncryptB64 performs AES-128-CBC + PKCS7 and returns base64, matching
 75// CryptoJS.AES.encrypt(plaintext, Utf8(key), {iv: Utf8(iv)}).toString().
 76// key and iv must each be 16 ASCII characters.
 77func AESEncryptB64(plaintext, key, iv string) (string, error) {
 78	block, err := aes.NewCipher([]byte(key))
 79	if err != nil {
 80		return "", err
 81	}
 82	if len(iv) != aes.BlockSize {
 83		return "", fmt.Errorf("iv must be 16 bytes, got %d", len(iv))
 84	}
 85	data := pkcs7Pad([]byte(plaintext), aes.BlockSize)
 86	out := make([]byte, len(data))
 87	cipher.NewCBCEncrypter(block, []byte(iv)).CryptBlocks(out, data)
 88	return base64.StdEncoding.EncodeToString(out), nil
 89}
 90
 91// AESDecryptB64 reverses AESEncryptB64 — used to decrypt router responses.
 92func AESDecryptB64(ciphertextB64, key, iv string) (string, error) {
 93	block, err := aes.NewCipher([]byte(key))
 94	if err != nil {
 95		return "", err
 96	}
 97	data, err := base64.StdEncoding.DecodeString(ciphertextB64)
 98	if err != nil {
 99		return "", err
100	}
101	if len(data) == 0 || len(data)%aes.BlockSize != 0 {
102		return "", fmt.Errorf("ciphertext not a multiple of block size")
103	}
104	out := make([]byte, len(data))
105	cipher.NewCBCDecrypter(block, []byte(iv)).CryptBlocks(out, data)
106	unpadded, err := pkcs7Unpad(out)
107	if err != nil {
108		return "", err
109	}
110	return string(unpadded), nil
111}
112
113// HashCredential computes MD5(username+password), or SHA-256 if isRGSec
114// (the IS_RG_SEC branch in tpEncrypt.js setHash).
115func HashCredential(username, password string, isRGSec bool) string {
116	data := []byte(username + password)
117	if isRGSec {
118		sum := sha256.Sum256(data)
119		return hex.EncodeToString(sum[:])
120	}
121	sum := md5.Sum(data)
122	return hex.EncodeToString(sum[:])
123}
124
125// RandDigits returns n random ASCII digits, matching
126// tpEncrypt.js generateRandomIntString(n).
127func RandDigits(n int) (string, error) {
128	buf := make([]byte, n)
129	if _, err := rand.Read(buf); err != nil {
130		return "", err
131	}
132	out := make([]byte, n)
133	for i, b := range buf {
134		out[i] = '0' + (b % 10)
135	}
136	return string(out), nil
137}
138
139func pkcs7Pad(data []byte, blockSize int) []byte {
140	pad := blockSize - len(data)%blockSize
141	out := make([]byte, len(data)+pad)
142	copy(out, data)
143	for i := len(data); i < len(out); i++ {
144		out[i] = byte(pad)
145	}
146	return out
147}
148
149func pkcs7Unpad(data []byte) ([]byte, error) {
150	if len(data) == 0 {
151		return nil, fmt.Errorf("empty plaintext")
152	}
153	pad := int(data[len(data)-1])
154	if pad == 0 || pad > len(data) {
155		return nil, fmt.Errorf("invalid PKCS7 padding")
156	}
157	return data[:len(data)-pad], nil
158}