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  }