Skip to content

Commit 666a5f7

Browse files
committed
feat: generatePublicKey from base64 string
`Wireguard:generatePublicKey` now accepts the privateKey as a base64 encoded string, which gets decoded internally to produce the raw bytes.
1 parent a50adb2 commit 666a5f7

File tree

5 files changed

+97
-116
lines changed

5 files changed

+97
-116
lines changed

src/base64.ts

Lines changed: 52 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,13 @@
1-
const { slice } = require<{
2-
slice: <T extends defined>(arr: T[], start: number, stop?: number) => T[];
1+
const { toBinary, getCharAt } = require<{
2+
toBinary: (int: number) => string;
3+
getCharAt: (str: string, pos: number) => string;
34
}>("./util.lua");
45

5-
function stringToBytes(str: string) {
6-
const result = [];
7-
8-
for (let i = 0; i < str.size(); i++) {
9-
result.push(string.byte(str, i + 1)[0]);
10-
}
11-
12-
return result;
13-
}
14-
15-
// Adapted from https://github.com/un-ts/ab64/blob/main/src/ponyfill.ts#L24
16-
const _atob = (asc: string) => {
17-
const b64CharList = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
18-
19-
const b64Chars = string.split(b64CharList, "");
20-
21-
const b64Table = b64Chars.reduce<Record<string, number>>((acc, char, index) => {
22-
acc[char] = index;
23-
return acc;
24-
}, {});
25-
26-
const fromCharCode = string.char;
27-
28-
asc = string.gsub(asc, "%s+", "")[0];
29-
asc += string.char(...slice(stringToBytes("=="), 2 - (asc.size() & 3)));
30-
31-
let u24: number;
32-
let binary = "";
33-
let r1: number;
34-
let r2: number;
35-
36-
for (let i = 0; i < asc.size(); i++) {
37-
u24 =
38-
(b64Table[string.byte(asc, i++)[0]] << 18) |
39-
(b64Table[string.byte(asc, i++)[0]] << 12) |
40-
((r1 = b64Table[string.byte(asc, i++)[0]]) << 6) |
41-
(r2 = b64Table[string.byte(asc, i++)[0]]);
42-
binary +=
43-
r1 === 64
44-
? fromCharCode((u24 >> 16) & 255)
45-
: r2 === 64
46-
? fromCharCode((u24 >> 16) & 255, (u24 >> 8) & 255)
47-
: fromCharCode((u24 >> 16) & 255, (u24 >> 8) & 255, u24 & 255);
48-
}
49-
50-
return binary;
51-
};
6+
const BASE64_CHAR = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
527

538
// Adapted from https://gist.github.com/jonleighton/958841
54-
export function atob(buf: number[]): string {
9+
export function encode(buf: number[]): string {
5510
let base64 = "";
56-
const encodings = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
5711

5812
const byteLength = buf.size();
5913
const byteRemainder = byteLength % 3;
@@ -75,10 +29,10 @@ export function atob(buf: number[]): string {
7529

7630
// Convert the raw binary segments to the appropriate ASCII encoding
7731
base64 +=
78-
string.char(string.byte(encodings, a + 1)[0]) +
79-
string.char(string.byte(encodings, b + 1)[0]) +
80-
string.char(string.byte(encodings, c + 1)[0]) +
81-
string.char(string.byte(encodings, d + 1)[0]);
32+
string.char(string.byte(BASE64_CHAR, a + 1)[0]) +
33+
string.char(string.byte(BASE64_CHAR, b + 1)[0]) +
34+
string.char(string.byte(BASE64_CHAR, c + 1)[0]) +
35+
string.char(string.byte(BASE64_CHAR, d + 1)[0]);
8236
}
8337

8438
// Deal with the remaining bytes and padding
@@ -90,7 +44,7 @@ export function atob(buf: number[]): string {
9044
// Set the 4 least significant bits to zero
9145
b = (chunk & 3) << 4;
9246

93-
base64 += string.byte(encodings, a)[0] + string.byte(encodings, b)[0] + "==";
47+
base64 += string.byte(BASE64_CHAR, a)[0] + string.byte(BASE64_CHAR, b)[0] + "==";
9448
} else if (byteRemainder === 2) {
9549
chunk = (buf[mainLength] << 8) | buf[mainLength + 1];
9650

@@ -101,11 +55,50 @@ export function atob(buf: number[]): string {
10155
c = (chunk & 15) << 2;
10256

10357
base64 +=
104-
string.char(string.byte(encodings, a + 1)[0]) +
105-
string.char(string.byte(encodings, b + 1)[0]) +
106-
string.char(string.byte(encodings, c + 1)[0]) +
58+
string.char(string.byte(BASE64_CHAR, a + 1)[0]) +
59+
string.char(string.byte(BASE64_CHAR, b + 1)[0]) +
60+
string.char(string.byte(BASE64_CHAR, c + 1)[0]) +
10761
"=";
10862
}
10963

11064
return base64;
11165
}
66+
67+
// FIXME: Ideally, you'd want to use bit math and mask off bytes and stuff,
68+
// but I'm lazy, so this logic uses string manipulation instead
69+
export function decode(base64: string): number[] {
70+
// Strip padding from base64
71+
base64 = base64.split("=")[0].gsub("%s", "")[0];
72+
73+
// Convert base64 chars to lookup table offsets
74+
const chars = [];
75+
for (let i = 1; i <= base64.size(); i++) {
76+
const char = getCharAt(base64, i);
77+
const [pos] = string.find(BASE64_CHAR, char);
78+
79+
pos !== undefined ? chars.push(pos - 1) : error("invalid base64 data");
80+
}
81+
82+
// Convert offsets to 6 bit binary numbers
83+
const bin = chars.map(toBinary);
84+
85+
// Combine all binary numbers into one
86+
let combinedBin = "";
87+
bin.forEach((b) => (combinedBin += b));
88+
89+
// Split the combined binary number into smaller ones of 8 bits each
90+
const intermediaryBin = [];
91+
while (combinedBin.size() > 0) {
92+
intermediaryBin.push(string.sub(combinedBin, 1, 8));
93+
combinedBin = string.sub(combinedBin, 9, combinedBin.size());
94+
}
95+
96+
// Convert each individual 8 bit binary number to a base 10 integer
97+
const decoded = [];
98+
for (let i = 0; i < intermediaryBin.size() - 1; i++) {
99+
const byte = tonumber(intermediaryBin[i], 2);
100+
decoded.push(byte !== undefined ? byte : error("got invalid byte while decoding base64"));
101+
}
102+
103+
return decoded;
104+
}

src/index.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
const { generatePrivateKey, generatePublicKey } = require<{
22
generatePrivateKey: () => number[];
3-
generatePublicKey: (privateKey: number[]) => number[];
3+
generatePublicKey: (privateKey: string | number[]) => number[];
44
}>("./wg.lua");
5-
const { atob } = require<{ atob: (buf: number[]) => string }>("./base64.lua");
5+
const base64 = require<{
6+
encode: (buf: number[]) => string;
7+
decode: (base64: string) => number[];
8+
}>("./base64.lua");
69

710
export interface Keypair {
811
publicKey: string;
@@ -11,7 +14,7 @@ export interface Keypair {
1114

1215
export interface Wireguard {
1316
generateKeypair(): Keypair;
14-
generatePublicKey(privateKey: number[]): string;
17+
generatePublicKey(privateKey: number[] | string): string;
1518
}
1619

1720
export const wireguard: Wireguard = {
@@ -21,14 +24,20 @@ export const wireguard: Wireguard = {
2124
? pcall<[], number[]>(() => generatePublicKey(privateKey))
2225
: error("failed to generate private key");
2326
return {
24-
publicKey: atob(publicKeyOk ? publicKey : error("failed to generate public key")),
25-
privateKey: atob(privateKey as number[]),
27+
publicKey: base64.encode(publicKeyOk ? publicKey : error("failed to generate public key")),
28+
privateKey: base64.encode(privateKey as number[]),
2629
};
2730
},
2831

2932
generatePublicKey: function (privateKey) {
33+
if (typeIs(privateKey, "string")) {
34+
privateKey = base64.decode(privateKey);
35+
}
36+
3037
const [publicKeyOk, publicKey] = pcall<[], number[]>(() => generatePublicKey(privateKey));
3138

32-
return atob(publicKeyOk ? publicKey : error("failed to generate public key"));
39+
return base64.encode(
40+
publicKeyOk ? publicKey : error("failed to generate public key %s".format(publicKey as string)),
41+
);
3342
},
3443
};

src/init.luau

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export type Keypair = {
55

66
export type Wireguard = {
77
generateKeypair: (self: {}) -> Keypair,
8-
generatePublicKey: (self: {}, privateKey: { number }) -> string,
8+
generatePublicKey: (self: {}, privateKey: { number } | string) -> string,
99
}
1010

1111
return {

src/util.ts

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,33 @@
1-
export function slice<T extends defined>(arr: T[], start: number, stop?: number): T[] {
2-
const length = arr.size();
1+
const OCTAL_LOOKUP = ["000", "001", "010", "011", "100", "101", "110", "111"];
32

4-
if (start < 0) {
5-
start = math.max(length + start, 0);
6-
}
3+
export function toBinary(int: number): string {
4+
let bin = string.format("%o", int);
5+
bin = bin.gsub(
6+
".",
7+
(b: string) =>
8+
OCTAL_LOOKUP[
9+
(() => {
10+
const [ok, val] = pcall<[], number>(() => {
11+
const res = tonumber(b);
712

8-
if (stop === undefined) {
9-
stop = length;
10-
} else if (stop < 0) {
11-
stop = math.max(length + stop, 0);
12-
}
13+
if (typeIs(res, "nil")) {
14+
error("failed to convert to binary");
15+
}
1316

14-
const result: T[] = [];
17+
return res;
18+
});
1519

16-
for (let i = start; i < stop; i++) {
17-
result.push(arr[i]);
18-
}
20+
return ok ? val : error(val);
21+
})()
22+
],
23+
)[0];
1924

20-
return result;
25+
// Pad to ensure the binary number is 6 bits
26+
bin = "0".rep(6 - bin.size()) + bin;
27+
28+
return bin;
29+
}
30+
31+
export function getCharAt(str: string, pos: number): string {
32+
return string.char(str.byte(pos)[0]);
2133
}

tests/generatePublicKey.luau

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,7 @@
11
local wg = require("../out/").wireguard
22

3-
local PRIVATE_KEY = {
4-
[1] = 208,
5-
[2] = 109,
6-
[3] = 43,
7-
[4] = 223,
8-
[5] = 41,
9-
[6] = 233,
10-
[7] = 180,
11-
[8] = 88,
12-
[9] = 228,
13-
[10] = 1,
14-
[11] = 132,
15-
[12] = 145,
16-
[13] = 79,
17-
[14] = 164,
18-
[15] = 143,
19-
[16] = 199,
20-
[17] = 134,
21-
[18] = 67,
22-
[19] = 153,
23-
[20] = 226,
24-
[21] = 151,
25-
[22] = 39,
26-
[23] = 198,
27-
[24] = 16,
28-
[25] = 30,
29-
[26] = 109,
30-
[27] = 90,
31-
[28] = 11,
32-
[29] = 22,
33-
[30] = 4,
34-
[31] = 217,
35-
[32] = 105,
36-
}
37-
local PUBLIC_KEY = "mYqWwJuiVXsXqfqXOKOKVTTZRovUXqzPkRtz1DwX1Wc="
3+
local PRIVATE_KEY = "iIWrphmeEnCLZFjdN17RQfEq8ND1MX+qAdIpRJdRhEA="
4+
local PUBLIC_KEY = "lYnVoKy9rzIapS0zPoLHskf4B+L3FouFXWwddKhRa3s="
385

396
local publicKey = wg:generatePublicKey(PRIVATE_KEY)
407

0 commit comments

Comments
 (0)