|
| 1 | +package auth |
| 2 | + |
| 3 | +import ( |
| 4 | + "crypto/ecdsa" |
| 5 | + "crypto/elliptic" |
| 6 | + "crypto/rand" |
| 7 | + "encoding/base64" |
| 8 | + "fmt" |
| 9 | + "math/big" |
| 10 | +) |
| 11 | + |
| 12 | +// VapidKeys represents a VAPID public/private key pair |
| 13 | +type VapidKeys struct { |
| 14 | + PublicKey string |
| 15 | + PrivateKey string |
| 16 | +} |
| 17 | + |
| 18 | +// GenerateVAPIDKeys generates a new VAPID key pair for Web Push notifications |
| 19 | +// This should be run once during initial setup and the keys stored securely |
| 20 | +func GenerateVAPIDKeys() (*VapidKeys, error) { |
| 21 | + // Generate P-256 elliptic curve private key |
| 22 | + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) |
| 23 | + if err != nil { |
| 24 | + return nil, fmt.Errorf("failed to generate private key: %w", err) |
| 25 | + } |
| 26 | + |
| 27 | + // Encode private key to base64 URL-safe format |
| 28 | + privateKeyBytes := privateKey.D.Bytes() |
| 29 | + // Ensure the key is 32 bytes (pad with zeros if needed) |
| 30 | + if len(privateKeyBytes) < 32 { |
| 31 | + paddedKey := make([]byte, 32) |
| 32 | + copy(paddedKey[32-len(privateKeyBytes):], privateKeyBytes) |
| 33 | + privateKeyBytes = paddedKey |
| 34 | + } |
| 35 | + privateKeyEncoded := base64.RawURLEncoding.EncodeToString(privateKeyBytes) |
| 36 | + |
| 37 | + // Encode public key to base64 URL-safe format (uncompressed format) |
| 38 | + // First byte is 0x04 (indicating uncompressed point), followed by X and Y coordinates |
| 39 | + publicKeyBytes := make([]byte, 65) |
| 40 | + publicKeyBytes[0] = 0x04 |
| 41 | + |
| 42 | + xBytes := privateKey.PublicKey.X.Bytes() |
| 43 | + yBytes := privateKey.PublicKey.Y.Bytes() |
| 44 | + |
| 45 | + // Ensure X and Y are 32 bytes each (pad with zeros if needed) |
| 46 | + copy(publicKeyBytes[1+32-len(xBytes):33], xBytes) |
| 47 | + copy(publicKeyBytes[33+32-len(yBytes):65], yBytes) |
| 48 | + |
| 49 | + publicKeyEncoded := base64.RawURLEncoding.EncodeToString(publicKeyBytes) |
| 50 | + |
| 51 | + return &VapidKeys{ |
| 52 | + PublicKey: publicKeyEncoded, |
| 53 | + PrivateKey: privateKeyEncoded, |
| 54 | + }, nil |
| 55 | +} |
| 56 | + |
| 57 | +// ParseVAPIDPrivateKey parses a base64-encoded VAPID private key into an ECDSA private key |
| 58 | +func ParseVAPIDPrivateKey(encodedKey string) (*ecdsa.PrivateKey, error) { |
| 59 | + // Decode the base64 URL-safe encoded private key |
| 60 | + privateKeyBytes, err := base64.RawURLEncoding.DecodeString(encodedKey) |
| 61 | + if err != nil { |
| 62 | + return nil, fmt.Errorf("failed to decode private key: %w", err) |
| 63 | + } |
| 64 | + |
| 65 | + // Convert bytes to big.Int |
| 66 | + d := new(big.Int).SetBytes(privateKeyBytes) |
| 67 | + |
| 68 | + // Create the private key |
| 69 | + privateKey := new(ecdsa.PrivateKey) |
| 70 | + privateKey.PublicKey.Curve = elliptic.P256() |
| 71 | + privateKey.D = d |
| 72 | + |
| 73 | + // Calculate the public key from the private key |
| 74 | + privateKey.PublicKey.X, privateKey.PublicKey.Y = privateKey.PublicKey.Curve.ScalarBaseMult(d.Bytes()) |
| 75 | + |
| 76 | + return privateKey, nil |
| 77 | +} |
0 commit comments