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.
This commit is contained in:
Erica Marigold 2024-03-31 15:56:34 +05:30
parent a50adb2fb9
commit 666a5f79e8
No known key found for this signature in database
GPG key ID: 2768CC0C23D245D1
5 changed files with 97 additions and 116 deletions

View file

@ -1,59 +1,13 @@
const { slice } = require<{ const { toBinary, getCharAt } = require<{
slice: <T extends defined>(arr: T[], start: number, stop?: number) => T[]; toBinary: (int: number) => string;
getCharAt: (str: string, pos: number) => string;
}>("./util.lua"); }>("./util.lua");
function stringToBytes(str: string) { const BASE64_CHAR = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const result = [];
for (let i = 0; i < str.size(); i++) {
result.push(string.byte(str, i + 1)[0]);
}
return result;
}
// Adapted from https://github.com/un-ts/ab64/blob/main/src/ponyfill.ts#L24
const _atob = (asc: string) => {
const b64CharList = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
const b64Chars = string.split(b64CharList, "");
const b64Table = b64Chars.reduce<Record<string, number>>((acc, char, index) => {
acc[char] = index;
return acc;
}, {});
const fromCharCode = string.char;
asc = string.gsub(asc, "%s+", "")[0];
asc += string.char(...slice(stringToBytes("=="), 2 - (asc.size() & 3)));
let u24: number;
let binary = "";
let r1: number;
let r2: number;
for (let i = 0; i < asc.size(); i++) {
u24 =
(b64Table[string.byte(asc, i++)[0]] << 18) |
(b64Table[string.byte(asc, i++)[0]] << 12) |
((r1 = b64Table[string.byte(asc, i++)[0]]) << 6) |
(r2 = b64Table[string.byte(asc, i++)[0]]);
binary +=
r1 === 64
? fromCharCode((u24 >> 16) & 255)
: r2 === 64
? fromCharCode((u24 >> 16) & 255, (u24 >> 8) & 255)
: fromCharCode((u24 >> 16) & 255, (u24 >> 8) & 255, u24 & 255);
}
return binary;
};
// Adapted from https://gist.github.com/jonleighton/958841 // Adapted from https://gist.github.com/jonleighton/958841
export function atob(buf: number[]): string { export function encode(buf: number[]): string {
let base64 = ""; let base64 = "";
const encodings = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
const byteLength = buf.size(); const byteLength = buf.size();
const byteRemainder = byteLength % 3; const byteRemainder = byteLength % 3;
@ -75,10 +29,10 @@ export function atob(buf: number[]): string {
// Convert the raw binary segments to the appropriate ASCII encoding // Convert the raw binary segments to the appropriate ASCII encoding
base64 += base64 +=
string.char(string.byte(encodings, a + 1)[0]) + string.char(string.byte(BASE64_CHAR, a + 1)[0]) +
string.char(string.byte(encodings, b + 1)[0]) + string.char(string.byte(BASE64_CHAR, b + 1)[0]) +
string.char(string.byte(encodings, c + 1)[0]) + string.char(string.byte(BASE64_CHAR, c + 1)[0]) +
string.char(string.byte(encodings, d + 1)[0]); string.char(string.byte(BASE64_CHAR, d + 1)[0]);
} }
// Deal with the remaining bytes and padding // Deal with the remaining bytes and padding
@ -90,7 +44,7 @@ export function atob(buf: number[]): string {
// Set the 4 least significant bits to zero // Set the 4 least significant bits to zero
b = (chunk & 3) << 4; b = (chunk & 3) << 4;
base64 += string.byte(encodings, a)[0] + string.byte(encodings, b)[0] + "=="; base64 += string.byte(BASE64_CHAR, a)[0] + string.byte(BASE64_CHAR, b)[0] + "==";
} else if (byteRemainder === 2) { } else if (byteRemainder === 2) {
chunk = (buf[mainLength] << 8) | buf[mainLength + 1]; chunk = (buf[mainLength] << 8) | buf[mainLength + 1];
@ -101,11 +55,50 @@ export function atob(buf: number[]): string {
c = (chunk & 15) << 2; c = (chunk & 15) << 2;
base64 += base64 +=
string.char(string.byte(encodings, a + 1)[0]) + string.char(string.byte(BASE64_CHAR, a + 1)[0]) +
string.char(string.byte(encodings, b + 1)[0]) + string.char(string.byte(BASE64_CHAR, b + 1)[0]) +
string.char(string.byte(encodings, c + 1)[0]) + string.char(string.byte(BASE64_CHAR, c + 1)[0]) +
"="; "=";
} }
return base64; return base64;
} }
// FIXME: Ideally, you'd want to use bit math and mask off bytes and stuff,
// but I'm lazy, so this logic uses string manipulation instead
export function decode(base64: string): number[] {
// Strip padding from base64
base64 = base64.split("=")[0].gsub("%s", "")[0];
// Convert base64 chars to lookup table offsets
const chars = [];
for (let i = 1; i <= base64.size(); i++) {
const char = getCharAt(base64, i);
const [pos] = string.find(BASE64_CHAR, char);
pos !== undefined ? chars.push(pos - 1) : error("invalid base64 data");
}
// Convert offsets to 6 bit binary numbers
const bin = chars.map(toBinary);
// Combine all binary numbers into one
let combinedBin = "";
bin.forEach((b) => (combinedBin += b));
// Split the combined binary number into smaller ones of 8 bits each
const intermediaryBin = [];
while (combinedBin.size() > 0) {
intermediaryBin.push(string.sub(combinedBin, 1, 8));
combinedBin = string.sub(combinedBin, 9, combinedBin.size());
}
// Convert each individual 8 bit binary number to a base 10 integer
const decoded = [];
for (let i = 0; i < intermediaryBin.size() - 1; i++) {
const byte = tonumber(intermediaryBin[i], 2);
decoded.push(byte !== undefined ? byte : error("got invalid byte while decoding base64"));
}
return decoded;
}

View file

@ -1,8 +1,11 @@
const { generatePrivateKey, generatePublicKey } = require<{ const { generatePrivateKey, generatePublicKey } = require<{
generatePrivateKey: () => number[]; generatePrivateKey: () => number[];
generatePublicKey: (privateKey: number[]) => number[]; generatePublicKey: (privateKey: string | number[]) => number[];
}>("./wg.lua"); }>("./wg.lua");
const { atob } = require<{ atob: (buf: number[]) => string }>("./base64.lua"); const base64 = require<{
encode: (buf: number[]) => string;
decode: (base64: string) => number[];
}>("./base64.lua");
export interface Keypair { export interface Keypair {
publicKey: string; publicKey: string;
@ -11,7 +14,7 @@ export interface Keypair {
export interface Wireguard { export interface Wireguard {
generateKeypair(): Keypair; generateKeypair(): Keypair;
generatePublicKey(privateKey: number[]): string; generatePublicKey(privateKey: number[] | string): string;
} }
export const wireguard: Wireguard = { export const wireguard: Wireguard = {
@ -21,14 +24,20 @@ export const wireguard: Wireguard = {
? pcall<[], number[]>(() => generatePublicKey(privateKey)) ? pcall<[], number[]>(() => generatePublicKey(privateKey))
: error("failed to generate private key"); : error("failed to generate private key");
return { return {
publicKey: atob(publicKeyOk ? publicKey : error("failed to generate public key")), publicKey: base64.encode(publicKeyOk ? publicKey : error("failed to generate public key")),
privateKey: atob(privateKey as number[]), privateKey: base64.encode(privateKey as number[]),
}; };
}, },
generatePublicKey: function (privateKey) { generatePublicKey: function (privateKey) {
if (typeIs(privateKey, "string")) {
privateKey = base64.decode(privateKey);
}
const [publicKeyOk, publicKey] = pcall<[], number[]>(() => generatePublicKey(privateKey)); const [publicKeyOk, publicKey] = pcall<[], number[]>(() => generatePublicKey(privateKey));
return atob(publicKeyOk ? publicKey : error("failed to generate public key")); return base64.encode(
publicKeyOk ? publicKey : error("failed to generate public key %s".format(publicKey as string)),
);
}, },
}; };

View file

@ -5,7 +5,7 @@ export type Keypair = {
export type Wireguard = { export type Wireguard = {
generateKeypair: (self: {}) -> Keypair, generateKeypair: (self: {}) -> Keypair,
generatePublicKey: (self: {}, privateKey: { number }) -> string, generatePublicKey: (self: {}, privateKey: { number } | string) -> string,
} }
return { return {

View file

@ -1,21 +1,33 @@
export function slice<T extends defined>(arr: T[], start: number, stop?: number): T[] { const OCTAL_LOOKUP = ["000", "001", "010", "011", "100", "101", "110", "111"];
const length = arr.size();
if (start < 0) { export function toBinary(int: number): string {
start = math.max(length + start, 0); let bin = string.format("%o", int);
} bin = bin.gsub(
".",
(b: string) =>
OCTAL_LOOKUP[
(() => {
const [ok, val] = pcall<[], number>(() => {
const res = tonumber(b);
if (stop === undefined) { if (typeIs(res, "nil")) {
stop = length; error("failed to convert to binary");
} else if (stop < 0) { }
stop = math.max(length + stop, 0);
}
const result: T[] = []; return res;
});
for (let i = start; i < stop; i++) { return ok ? val : error(val);
result.push(arr[i]); })()
} ],
)[0];
return result; // Pad to ensure the binary number is 6 bits
bin = "0".rep(6 - bin.size()) + bin;
return bin;
}
export function getCharAt(str: string, pos: number): string {
return string.char(str.byte(pos)[0]);
} }

View file

@ -1,40 +1,7 @@
local wg = require("../out/").wireguard local wg = require("../out/").wireguard
local PRIVATE_KEY = { local PRIVATE_KEY = "iIWrphmeEnCLZFjdN17RQfEq8ND1MX+qAdIpRJdRhEA="
[1] = 208, local PUBLIC_KEY = "lYnVoKy9rzIapS0zPoLHskf4B+L3FouFXWwddKhRa3s="
[2] = 109,
[3] = 43,
[4] = 223,
[5] = 41,
[6] = 233,
[7] = 180,
[8] = 88,
[9] = 228,
[10] = 1,
[11] = 132,
[12] = 145,
[13] = 79,
[14] = 164,
[15] = 143,
[16] = 199,
[17] = 134,
[18] = 67,
[19] = 153,
[20] = 226,
[21] = 151,
[22] = 39,
[23] = 198,
[24] = 16,
[25] = 30,
[26] = 109,
[27] = 90,
[28] = 11,
[29] = 22,
[30] = 4,
[31] = 217,
[32] = 105,
}
local PUBLIC_KEY = "mYqWwJuiVXsXqfqXOKOKVTTZRovUXqzPkRtz1DwX1Wc="
local publicKey = wg:generatePublicKey(PRIVATE_KEY) local publicKey = wg:generatePublicKey(PRIVATE_KEY)