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}