github.com/google/osv-scalibr@v0.4.1/veles/secrets/vapid/detector.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 vapid
    16  
    17  import (
    18  	"crypto/ecdh"
    19  	"encoding/base64"
    20  	"fmt"
    21  	"regexp"
    22  
    23  	"github.com/google/osv-scalibr/veles"
    24  	"github.com/google/osv-scalibr/veles/secrets/common/pair"
    25  )
    26  
    27  const (
    28  	publicKeyLen  = 87
    29  	privateKeyLen = 43
    30  	maxKeyLen     = max(publicKeyLen, privateKeyLen)
    31  
    32  	// maxDistance is the maximum window length to pair env-style credentials.
    33  	maxDistance = 10 * 1 << 10 // 10 KiB
    34  )
    35  
    36  var (
    37  	// match a base64 blob of exactly 87 characters
    38  	publicKeyPattern = regexp.MustCompile(`\b[A-Za-z0-9_-]{87}\b`)
    39  	// match a base64 blob of exactly 43 characters
    40  	privateKeyPattern = regexp.MustCompile(`\b[A-Za-z0-9_-]{43}\b`)
    41  )
    42  
    43  // NewDetector returns a VAPID private key detector
    44  //
    45  // a key is detected if:
    46  //
    47  // - it has some context, (ex: `VAPID_KEY:base64blob`)
    48  // - it is validated against a nearby public key
    49  func NewDetector() veles.Detector {
    50  	return &pair.Detector{
    51  		MaxElementLen: maxKeyLen, MaxDistance: maxDistance,
    52  		FindA: pair.FindAllMatches(publicKeyPattern),
    53  		FindB: pair.FindAllMatches(privateKeyPattern),
    54  		FromPair: func(p pair.Pair) (veles.Secret, bool) {
    55  			pubB64, privB64 := string(p.A.Value), string(p.B.Value)
    56  			if ok, _ := validateVAPIDKeys(pubB64, privB64); !ok {
    57  				return nil, false
    58  			}
    59  			return Key{PublicB64: pubB64, PrivateB64: privB64}, true
    60  		},
    61  	}
    62  }
    63  
    64  // validateVAPIDKeys checks if a VAPID public key matches a private key (P-256)
    65  func validateVAPIDKeys(pubB64, privB64 string) (bool, error) {
    66  	// Decode base64url keys
    67  	pubBytes, err := base64.RawURLEncoding.DecodeString(pubB64)
    68  	if err != nil {
    69  		return false, fmt.Errorf("invalid public key: %w", err)
    70  	}
    71  	privBytes, err := base64.RawURLEncoding.DecodeString(privB64)
    72  	if err != nil {
    73  		return false, fmt.Errorf("invalid private key: %w", err)
    74  	}
    75  
    76  	// Load curve
    77  	curve := ecdh.P256()
    78  
    79  	// Parse keys
    80  	pubKey, err := curve.NewPublicKey(pubBytes)
    81  	if err != nil {
    82  		return false, fmt.Errorf("failed to parse public key: %w", err)
    83  	}
    84  
    85  	privKey, err := curve.NewPrivateKey(privBytes)
    86  	if err != nil {
    87  		return false, fmt.Errorf("failed to parse private key: %w", err)
    88  	}
    89  
    90  	// Compare public keys
    91  	expectedPub := privKey.PublicKey()
    92  	if !expectedPub.Equal(pubKey) {
    93  		return false, nil
    94  	}
    95  
    96  	return true, nil
    97  }