CREDENTIAL TYPE MANAGEMENT WORKS

This commit is contained in:
2026-02-14 15:15:49 -06:00
parent b7637334a6
commit cdae4d47a4
46 changed files with 7621 additions and 41 deletions
@@ -0,0 +1,18 @@
export enum ValueType {
PLAIN_TEXT = "plain_text",
PASSWORD = "password",
}
export interface CredentialTypeField {
id: string; // I.e. "clientId", "clientSecret", etc.
name: string; // I.e. "Client ID", "Client Secret", etc.
required: boolean;
secure: boolean; // Whether this field should be stored encrypted in the database
valueType: ValueType; // For future extensibility, currently all fields are strings
}
export interface CredentialField {
id: string; // CUID
fieldId: string; // I.e. "clientId", "clientSecret", etc.
value: string; // Encrypted value stored in the database
}
+56
View File
@@ -0,0 +1,56 @@
import { Collection } from "@discordjs/collection";
import { CredentialField, CredentialTypeField } from "./credentialTypeDefs";
import GenericError from "../../Errors/GenericError";
/**
* Field Validator
*
* This method will take a record of the fields being submitted and compare them against a record of the acceptable fields
* for a credential type. If any of the submitted fields do not match an acceptable field, an error will be thrown.
*
* If all the credentials pass, it will return a processed version of the submitted fields including fields that need to be
* stored securely (encrypted) and fields that do not.
*
* @param fields - The fields in object form that are being submitted.
* @param acceptableFields - The acceptable field to be compared against.
*/
export const fieldValidator = async (
fields: CredentialField[],
acceptableFields: CredentialTypeField[],
): Promise<
{
id: string;
fieldId: string;
value: string;
secure: boolean;
}[]
> => {
const afCollection = new Collection(acceptableFields.map((f) => [f.id, f]));
await Promise.all(
fields.map(async (field) => {
const matchingField = afCollection.get(field.fieldId);
if (!matchingField) {
throw new GenericError({
message: `Invalid field ID: ${field.fieldId}`,
name: "InvalidCredentialField",
cause: `No acceptable field with ID '${field.fieldId}' found.`,
status: 400,
});
}
return;
}),
);
return fields.map((field) => {
const matchingField = afCollection.get(field.fieldId)!;
return {
id: field.id,
fieldId: field.fieldId,
value: field.value,
secure: matchingField.secure,
};
});
};
@@ -0,0 +1,24 @@
import Password from "../tools/Password";
import crypto from "crypto";
import { secureValuesPublicKey } from "../../constants";
export const generateSecureValue = (content: string) => {
// Generate a hash of the content
const hash = Password.hash(content);
// Encrypt the content using the .secureValues.pub public key
const encrypted = crypto.publicEncrypt(
{
key: secureValuesPublicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256",
},
Buffer.from(content, "utf-8"),
);
// Return the encrypted content and the hash for storage
return {
encrypted: encrypted.toString("base64"),
hash,
};
};
@@ -0,0 +1,30 @@
import Password from "../tools/Password";
import crypto from "crypto";
import { secureValuesPrivateKey } from "../../constants";
export const readSecureValue = (
encryptedContent: string,
hash?: string,
): string => {
// Decrypt the content using the .secureValues.key private key
const decrypted = crypto.privateDecrypt(
{
key: secureValuesPrivateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
oaepHash: "sha256",
},
Buffer.from(encryptedContent, "base64"),
);
const content = decrypted.toString("utf-8");
// Optionally validate the hash if provided
if (hash) {
const isValid = Password.validate(content, hash);
if (!isValid) {
throw new Error("Secure value hash validation failed");
}
}
return content;
};
+5 -2
View File
@@ -10,7 +10,10 @@ export default class Password {
public static hash(password: string, options?: HashPasswordOptions): string {
const salt =
options?.overrideSalt ?? Password.generateSalt(options?.saltOpts);
options?.overrideSalt ??
(options?.saltOpts?.length
? Password.generateSalt(options?.saltOpts)
: "");
const hash = blake2sHex(`$BLAKE2s$${password}$${salt}`);
return `BLAKE2s$${hash}$${salt}`;
}
@@ -19,7 +22,7 @@ export default class Password {
const [algo, oldHash, salt] = hashed.split(/\$/g);
return crypto.timingSafeEqual(
Buffer.from(hashed),
Buffer.from(Password.hash(newPass, { overrideSalt: salt }))
Buffer.from(Password.hash(newPass, { overrideSalt: salt })),
);
}
}