CREDENTIAL TYPE MANAGEMENT WORKS
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 })),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user