github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/secrets/mysqlmylogin/decrypt.go (about) 1 // Copyright 2025 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package mysqlmylogin 16 17 import ( 18 "crypto/aes" 19 "crypto/cipher" 20 "encoding/binary" 21 "errors" 22 "io" 23 ) 24 25 const ( 26 loginKeyLen = 20 27 maxCipher = 4096 28 ) 29 30 // decryptMyLoginCNF decrypts the content of .mylogin.cnf from an io.Reader 31 // 32 // .mylogin.cnf file format (MySQL 5.6+): 33 // - First 4 bytes: unused/reserved (probably for version number) 34 // - Next 20 bytes: key used to derive the AES key via XOR 35 // - Rest of the file: repeated for each chunk: 36 // - 4 bytes: length of encrypted chunk (little-endian) 37 // - n bytes: encrypted chunk 38 // 39 // References: 40 // - MySQL Documentation: https://dev.mysql.com/doc/refman/8.0/en/mysql-config-editor.html 41 // - Reference implementation: https://ocelot.ca/blog/blog/2015/05/21/decrypt-mylogin-cnf/ 42 // - Reference implementation: https://github.com/PyMySQL/myloginpath 43 // - MySQL source code: https://github.com/ocelot-inc/ocelotgui/blob/master/readmylogin.c 44 func decryptMyLoginCNF(reader io.Reader) ([]byte, error) { 45 // Read the first 4 bytes (unused/reserved for future version) 46 // Reference: "First four bytes are unused, probably reserved for version number" 47 var unused [4]byte 48 if err := binary.Read(reader, binary.LittleEndian, &unused); err != nil { 49 return nil, errors.New("error reading header") 50 } 51 52 // Read the key (20 bytes) 53 // From the Reference: 54 // "Next twenty bytes are the basis of the key, to be XORed in a loop 55 // until a sixteen-byte key is produced" 56 key := make([]byte, loginKeyLen) 57 if _, err := io.ReadFull(reader, key); err != nil { 58 return nil, errors.New("error reading key") 59 } 60 61 // Derive the 16-byte AES key via cyclic XOR 62 // Reference: XOR of the 20 bytes into a 16-byte buffer 63 aesKey := make([]byte, aes.BlockSize) 64 for i := range loginKeyLen { 65 aesKey[i%aes.BlockSize] ^= key[i] 66 } 67 68 // Initialize the AES cipher 69 block, err := aes.NewCipher(aesKey) 70 if err != nil { 71 return nil, errors.New("error initializing AES") 72 } 73 74 var plaintext []byte 75 76 // Read and decrypt chunks 77 // Reference: 78 // "The rest of the file is, repeated as necessary: 79 // four bytes = length of following cipher chunk, little-endian 80 // n bytes = cipher chunk" 81 for { 82 // Read chunk length (4 bytes, little-endian) 83 var chunkLen uint32 84 if err := binary.Read(reader, binary.LittleEndian, &chunkLen); err != nil { 85 if errors.Is(err, io.EOF) { 86 break 87 } 88 return nil, errors.New("error reading chunk length") 89 } 90 91 if chunkLen > maxCipher { 92 return nil, errors.New("chunk too large") 93 } 94 95 // Read the encrypted chunk 96 cipherChunk := make([]byte, chunkLen) 97 if _, err := io.ReadFull(reader, cipherChunk); err != nil { 98 return nil, errors.New("error reading encrypted chunk") 99 } 100 101 // Decrypt the chunk using AES-128-ECB 102 // Reference: "Encryption is AES 128-bit ecb" 103 // Reference: MySQL default block_encryption_mode is aes-128-ecb 104 decryptedChunk, err := decryptAES128ECB(cipherChunk, block) 105 if err != nil { 106 return nil, errors.New("error decrypting chunk") 107 } 108 109 // Remove padding from the chunk 110 // Reference: 111 // "Chunk lengths are always a multiple of 16 bytes (128 bits). 112 // Therefore there may be padding. We assume that any trailing 113 // byte containing a value less than '\n' is a padding byte." 114 decryptedChunk = removePaddingBytes(decryptedChunk) 115 116 plaintext = append(plaintext, decryptedChunk...) 117 } 118 119 return plaintext, nil 120 } 121 122 // removePaddingBytes removes padding bytes from the plaintext 123 // 124 // See the comment before the for loop 125 // All the fields are separated by \n. 126 // Padding is made until 16 bytes (maximum). It can be from 0x1 to 0x10. 127 func removePaddingBytes(data []byte) []byte { 128 if len(data) == 0 { 129 return data 130 } 131 132 i := len(data) 133 // Check if the last byte of the chunk is a padding byte 134 for i > 0 && data[i-1] <= 0x10 { 135 if data[i-1] == 0x0A { 136 // Handle of multiple \n case 137 if i > 1 && data[i-2] != 0x0A { 138 // This is the first (legitimate) newline, keep it 139 break 140 } 141 // Otherwise it's padding, remove it 142 } 143 i-- 144 } 145 146 return data[:i] 147 } 148 149 // decryptAES128ECB decrypts using AES-128 in ECB mode 150 // 151 // ECB (Electronic Codebook) mode decrypts each block independently. 152 // Note: ECB is not secure for real sensitive data, but MySQL uses .mylogin.cnf 153 // only for obfuscation, not for true security. 154 // 155 // From the Reference: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#ECB 156 func decryptAES128ECB(ciphertext []byte, block cipher.Block) ([]byte, error) { 157 if len(ciphertext)%aes.BlockSize != 0 { 158 return nil, errors.New("ciphertext is not a multiple of block size") 159 } 160 161 plaintext := make([]byte, len(ciphertext)) 162 163 // ECB mode: decrypt each 16-byte block independently 164 for i := 0; i < len(ciphertext); i += aes.BlockSize { 165 block.Decrypt(plaintext[i:i+aes.BlockSize], ciphertext[i:i+aes.BlockSize]) 166 } 167 168 return plaintext, nil 169 }