Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change generated project name to become /^(random_word)-[0-9a-z]{6}$/ #4649

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 2 additions & 11 deletions portal/src/graphql/portal/CreateProjectScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ import { ErrorParseRule, makeReasonErrorParseRule } from "../../error/parse";
import { useSimpleForm } from "../../hook/useSimpleForm";
import {
randomProjectName,
getRandom32BitsNumber,
maskNumber,
projectNameWithCompanyName,
} from "../../util/projectname";
import PrimaryButton from "../../PrimaryButton";

Expand Down Expand Up @@ -75,14 +74,6 @@ function processCompanyName(companyName: string): string {
.toLowerCase();
}

function addRandomNumberSuffix(intermediateProjectName: string): string {
return (
intermediateProjectName +
"-" +
maskNumber(getRandom32BitsNumber(), 0, 10).toString()
);
}

function CreateProjectScreenContent(props: CreateProjectScreenContentProps) {
const { numberOfApps } = props;
const navigate = useNavigate();
Expand All @@ -104,7 +95,7 @@ function CreateProjectScreenContent(props: CreateProjectScreenContentProps) {
if (typedState) {
const intermediateName = processCompanyName(typedState.company_name);
if (intermediateName !== "")
defaultState.appID = addRandomNumberSuffix(intermediateName);
defaultState.appID = projectNameWithCompanyName(intermediateName);
}
return defaultState;
}, [state]);
Expand Down
84 changes: 53 additions & 31 deletions portal/src/util/projectname.test.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,69 @@
import { describe, it, expect } from "@jest/globals";
import { maskNumber, deterministicProjectName } from "./projectname";
import {
deterministicProjectName,
extractBits,
projectNameWithCompanyName,
} from "./projectname";

describe("maskNumber", () => {
it("should mask the number starting from 0 bit and get 11 bits after it", () => {
expect(maskNumber(12345678, 0, 11)).toEqual(334);
describe("extractBits", () => {
it("should handle bytes that look like signed bits", () => {
expect(
extractBits(
new Uint8Array([
0b10000000, 0b10000001, 0b10000010, 0b10000011, 0b10000100,
0b10000101,
])
)
).toEqual([0b10000000100, 0b0000110000010100000111000010010]);
});

it("should mask the number starting from 20 bit and get 5 bits after it", () => {
expect(maskNumber(12345678, 20, 5)).toEqual(11);
it("should handle bytes", () => {
expect(
extractBits(
new Uint8Array([
0b00000000, 0b00000001, 0b00000010, 0b00000011, 0b00000100,
0b00000101,
])
)
).toEqual([0b00000000000, 0b0000100000010000000110000010000]);
});
});

describe("deterministicProjectName", () => {
it("deterministicProjectName(0) is 'abandon-abandon-0'", () => {
// 0 is 0b00000000000_00000000000_0000000000
// 0b00000000000 is abandon
// 0b0000000000 is 0
// So the name is abandon-abandon-0
expect(deterministicProjectName(0)).toEqual("abandon-abandon-0");
it("deterministicProjectName([0, 0, 0, 0, 0, 0]) is 'abandon-000000'", () => {
// [0, 0, 0, 0, 0, 0] is 0b00000000000_0000000000000000000000000000000_000000
// 0b00000000000 is 'abandon'
// 0000000000000000000000000000000 is '000000'
// last 6 bits 000000 is not used
// So the name is 'abandon-000000'
const fortyEightBits = new Uint8Array([0, 0, 0, 0, 0, 0]);
expect(deterministicProjectName(fortyEightBits)).toEqual("abandon-000000");
});

it("deterministicProjectName(1) is ''", () => {
// 1 is 0b00000000000_00000000000_0000000001
// 0b00000000000 is abandon
// 0b0000000001 is 1
// So the name is abandon-abandon-1
expect(deterministicProjectName(1)).toEqual("abandon-abandon-1");
it("deterministicProjectName([0, 0, 0, 0, 1, 0]) is 'abandon-000004'", () => {
// [0, 0, 0, 0, 1, 0] is 0b00000000000_0000000000000000000000000000100_000000
// 0b00000000000 is 'abandon'
// 0000000000000000000000000000100 is '000004'
// last 6 bits 000000 is not used
// So the name is 'abandon-000000'
const fortyEightBits = new Uint8Array([0, 0, 0, 0, 1, 0]);
expect(deterministicProjectName(fortyEightBits)).toEqual("abandon-000004");
});

it("deterministicProjectName(87878787) is ''", () => {
// 87878787 is 0b00000101001_11100111011_0010000011
// 0b00000101001 is ahead
// 0b11100111011 is trash
// 0b0010000011 is 131
// So the name is ahead-trash-131
expect(deterministicProjectName(87878787)).toEqual("ahead-trash-131");
it("deterministicProjectName([87, 87, 87, 87, 87, 87]) is 'firm-pwldul' ", () => {
// [87, 87, 87, 87, 87, 87] is 0b01010111010_1011101010111010101110101011101_010111
// 0b0b01010111010 is 'firm'
// 1011101010111010101110101011101 is 'pwldul'
// last 6 bits 010111 is not used
// So the name is 'firm-000000'
const fortyEightBits = new Uint8Array([87, 87, 87, 87, 87, 87]);
expect(deterministicProjectName(fortyEightBits)).toEqual("firm-pwldul");
});
});

it("deterministicProjectName(4294967295) is ''", () => {
// 4294967295 (2^32 - 1) is 0b11111111111_11111111111_1111111111
// 0b11111111111 is zoo
// 0b1111111111 is 1023
// So the name is zoo-zoo-1023
expect(deterministicProjectName(4294967295)).toEqual("zoo-zoo-1023");
describe("projectNameWithCompanyName", () => {
it("projectNameWithCompanyName('authgear') starts with 'authgear-` and ends with 6 lowercase-alphanumeric characters", () => {
const authgearProjectName = projectNameWithCompanyName("authgear");
expect(authgearProjectName).toMatch(/^authgear-[a-z0-9]{6}$/);
});
});
85 changes: 72 additions & 13 deletions portal/src/util/projectname.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,86 @@ function determineWord(index: number): string {
return wordlist[index];
}

export function getRandom32BitsNumber(): number {
const randomBuffer = new Uint32Array(1);
export function getRandom48Bits(): Uint8Array {
const randomBuffer = new Uint8Array(6);
window.crypto.getRandomValues(randomBuffer);
return randomBuffer[0];
return randomBuffer;
}

export function maskNumber(num: number, startAt: number, bits: number): number {
return (num >> startAt) & ((1 << bits) - 1);
/**
* This function take 48 bits, and split into 2 numbers, bit_0_10 and bit_11_41, dropping the last 6 bits
*
* @export
* @param {Uint8Array} fortyEightBits
* @returns {[number, number]}
*/
export function extractBits(fortyEightBits: Uint8Array): [number, number] {
const bit_0_7 = fortyEightBits[0];
const bit_8_15 = fortyEightBits[1];
const bit_16_23 = fortyEightBits[2];
const bit_24_31 = fortyEightBits[3];
const bit_32_39 = fortyEightBits[4];
const bit_40_47 = fortyEightBits[5];

// 11 bits are needed, there are 2^11 = 2048 words in the wordlist
const bit_0_10 = (bit_0_7 << 3) | (bit_8_15 >>> 5);

const bit_11_15 = bit_8_15 & 0b00011111;
const bit_40_41 = bit_40_47 >>> 6;

// 31 bits are needed. We need 6 lowercase-alphanumeric characters.
// There are 36 lowercase-alphanumeric characters in total.
// log36(2^31) = 5.996...
const bit_11_41 =
(bit_11_15 << 26) |
(bit_16_23 << 18) |
(bit_24_31 << 10) |
(bit_32_39 << 2) |
bit_40_41;

return [bit_0_10, bit_11_41];
}

export function deterministicAlphanumericString(bits: number): string {
const string = bits.toString(36);

if (string.length > 6) {
throw new Error("number of bits must be less than 31");
}

const zeroPaddedString = string.padStart(6, "0");
return zeroPaddedString;
}

export function deterministicProjectName(num: number): string {
const randomNumber = maskNumber(num, 0, 10);
const secondRandomStringIndex = maskNumber(num, 10, 11);
const firstRandomStringIndex = maskNumber(num, 21, 11);
/**
* This function take 48 bits
* The 1st - 11th bits bit_0_10 are used to determine the word
* The 12th - 42nd bits bit_11_41 are used to determine the alphanumeric string
* The 43rd - 48th bits bit_42_47 are not used
*
* @export
* @param {Uint8Array} fortyEightBits Uint8Array of length 6
* @returns {string}
*/
export function deterministicProjectName(fortyEightBits: Uint8Array): string {
if (fortyEightBits.length !== 6) {
throw new Error("fortyEightBits must be 6 bytes");
}
const [bit_0_10, bit_11_41] = extractBits(fortyEightBits);
const firstRandomString = determineWord(bit_0_10);

const firstRandomString = determineWord(firstRandomStringIndex);
const secondRandomString = determineWord(secondRandomStringIndex);
const alphaNumericString = deterministicAlphanumericString(bit_11_41);

return `${firstRandomString}-${secondRandomString}-${randomNumber}`;
return `${firstRandomString}-${alphaNumericString}`;
}

export function randomProjectName(): string {
return deterministicProjectName(getRandom32BitsNumber());
return deterministicProjectName(getRandom48Bits());
}

export function projectNameWithCompanyName(companyName: string): string {
const randomBits = getRandom48Bits();
const [_, thirtyOneBits] = extractBits(randomBits);
const alphaNumericString = deterministicAlphanumericString(thirtyOneBits);
return `${companyName}-${alphaNumericString}`;
}
Loading