File size: 4,570 Bytes
c7052c4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
/* cspell:disable */

// Crypto utilities that work in both Node.js and Cloudflare Workers
export class CryptoUtils {
  /**
   * Normalize a PEM key that might be missing newlines
   */
  private static normalizePemKey(pemKey: string): string {
    // Remove all whitespace first
    let normalized = pemKey.trim().replace(/\s+/g, '');

    // Check for BEGIN/END markers
    const beginMarkers = [
      '-----BEGINPRIVATEKEY-----',
      '-----BEGINRSAPRIVATEKEY-----',
      '-----BEGINENCRYPTEDPRIVATEKEY-----',
    ];

    const endMarkers = [
      '-----ENDPRIVATEKEY-----',
      '-----ENDRSAPRIVATEKEY-----',
      '-----ENDENCRYPTEDPRIVATEKEY-----',
    ];

    let beginMarker = '';
    let endMarker = '';
    let keyContent = normalized;

    // Find which markers are present
    for (let i = 0; i < beginMarkers.length; i++) {
      if (normalized.includes(beginMarkers[i])) {
        beginMarker = beginMarkers[i];
        endMarker = endMarkers[i];
        // Extract content between markers
        const startIdx = normalized.indexOf(beginMarker) + beginMarker.length;
        const endIdx = normalized.indexOf(endMarker);
        keyContent = normalized.substring(startIdx, endIdx);
        break;
      }
    }

    // If no markers found, assume the whole thing is the key content
    if (!beginMarker) {
      beginMarker = '-----BEGINPRIVATEKEY-----';
      endMarker = '-----ENDPRIVATEKEY-----';
    }

    // Reformat with proper newlines (64 chars per line is PEM standard)
    const formattedContent = keyContent.match(/.{1,64}/g)?.join('\n') || keyContent;

    // Reconstruct with proper spacing
    const properBegin = beginMarker
      .replace('-----BEGIN', '-----BEGIN ')
      .replace('KEY-----', ' KEY-----');
    const properEnd = endMarker.replace('-----END', '-----END ').replace('KEY-----', ' KEY-----');

    return `${properBegin}\n${formattedContent}\n${properEnd}`;
  }

  private static async importPrivateKey(pemKey: string, _passphrase?: string): Promise<CryptoKey> {
    // Normalize the key first
    const normalizedKey = this.normalizePemKey(pemKey);

    // Remove PEM headers and decode base64
    const pemHeader = '-----BEGIN';
    const pemFooter = '-----END';
    const pemContents = normalizedKey
      .split('\n')
      .filter((line) => !line.includes(pemHeader) && !line.includes(pemFooter))
      .join('');

    const binaryDer = this.base64ToArrayBuffer(pemContents);

    // Import the key using Web Crypto API
    try {
      return await crypto.subtle.importKey(
        'pkcs8',
        binaryDer,
        {
          name: 'RSASSA-PKCS1-v1_5',
          hash: 'SHA-256',
        },
        false,
        ['sign'],
      );
    } catch (error) {
      throw new Error(
        `Failed to import private key. Ensure it's in PKCS8 format. Use: openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in key.pem -out key_pkcs8.pem`,
        { cause: error },
      );
    }
  }

  private static base64ToArrayBuffer(base64: string): ArrayBuffer {
    const binaryString =
      typeof atob !== 'undefined' ? atob(base64) : Buffer.from(base64, 'base64').toString('binary');

    const bytes = new Uint8Array(binaryString.length);
    for (let i = 0; i < binaryString.length; i++) {
      bytes[i] = binaryString.charCodeAt(i);
    }
    return bytes.buffer;
  }

  private static arrayBufferToBase64(buffer: ArrayBuffer): string {
    const bytes = new Uint8Array(buffer);
    let binary = '';
    for (let i = 0; i < bytes.byteLength; i++) {
      binary += String.fromCharCode(bytes[i]);
    }
    return typeof btoa !== 'undefined'
      ? btoa(binary)
      : Buffer.from(binary, 'binary').toString('base64');
  }

  static async sign(privateKey: CryptoKey, data: string): Promise<string> {
    const encoder = new TextEncoder();
    const dataBuffer = encoder.encode(data);

    const signature = await crypto.subtle.sign('RSASSA-PKCS1-v1_5', privateKey, dataBuffer);

    return this.arrayBufferToBase64(signature);
  }

  static async sha256(data: string): Promise<string> {
    const encoder = new TextEncoder();
    const dataBuffer = encoder.encode(data);
    const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer);
    return this.arrayBufferToBase64(hashBuffer);
  }

  static async loadPrivateKey(pemKey: string, passphrase?: string): Promise<CryptoKey> {
    if (passphrase) {
      console.warn(
        'Key passphrase provided but not supported in Web Crypto API. Please use an unencrypted key.',
      );
    }
    return this.importPrivateKey(pemKey, passphrase);
  }
}