Skip to content

Commit

Permalink
Hardcoded licence mechanism (#310)
Browse files Browse the repository at this point in the history
  • Loading branch information
antoinejaussoin authored Oct 3, 2021
1 parent 94d5333 commit e6d37f0
Show file tree
Hide file tree
Showing 9 changed files with 143 additions and 20 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ When using the Docker deployment, your database runs from a container. But if yo

- Upgrade to MUI 5.0 (ex Material UI)
- Migration from Styled Components to Emotion (for compatibility reasons with MUI)
- Add hard-coded self-hosting licence mechanism for companies with restricted internet access
- ⏫ Upgrading dependencies

### Version 4.7.2
Expand Down
4 changes: 2 additions & 2 deletions backend/src/admin/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ const router = express.Router();
const csrfProtection = csurf();

router.get('/self-hosting', async (_, res) => {
const licenced = await isLicenced();
const licence = await isLicenced();
const payload: SelfHostingPayload = {
adminEmail: config.SELF_HOSTED_ADMIN,
selfHosted: config.SELF_HOSTED,
licenced,
licenced: !!licence,
oAuth: {
google: !!config.GOOGLE_KEY && !!config.GOOGLE_SECRET,
github: !!config.GITHUB_KEY && !!config.GITHUB_SECRET,
Expand Down
4 changes: 2 additions & 2 deletions backend/src/auth/logins/password-user.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { compare } from 'bcryptjs';
import { UserIdentityEntity } from '../../db/entities';
import { getIdentityByUsername } from '../../db/actions/users';
import { comparePassword } from '../../utils';

export default async function loginUser(
username: string,
Expand All @@ -10,6 +10,6 @@ export default async function loginUser(
if (!user || user.password === null) {
return null;
}
const isPasswordCorrect = await compare(password, user.password);
const isPasswordCorrect = await comparePassword(password, user.password);
return isPasswordCorrect ? user : null;
}
24 changes: 24 additions & 0 deletions backend/src/db/actions/licences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import LicenceEntity from '../entities/Licence';
import { v4 } from 'uuid';
import { sendSelfHostWelcome } from '../../email/emailSender';
import { LicenceRepository } from '../repositories';
import { LicenceMetadata } from 'src/types';

export async function registerLicence(
email: string | null,
Expand Down Expand Up @@ -44,3 +45,26 @@ export async function validateLicence(key: string): Promise<boolean> {
}
});
}

export async function fetchLicence(
key: string
): Promise<LicenceMetadata | null> {
return await transaction(async (manager) => {
const repository = manager.getRepository(LicenceEntity);
try {
const found = await repository.findOne({
where: { key },
});
if (found) {
return {
licence: key,
owner: found.email!,
};
}
} catch (err) {
console.log('Error while retriving the licence: ', err);
return null;
}
return null;
});
}
5 changes: 2 additions & 3 deletions backend/src/db/actions/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@ import { AccountType, FullUser } from '@retrospected/common';
import { isSelfHostedAndLicenced } from '../../security/is-licenced';
import { v4 } from 'uuid';
import UserIdentityEntity from '../entities/UserIdentity';
import { hashPassword } from '../../utils';
import { compare } from 'bcryptjs';
import { comparePassword, hashPassword } from '../../utils';

export async function getUser(userId: string): Promise<UserEntity | null> {
return await transaction(async (manager) => {
Expand Down Expand Up @@ -262,7 +261,7 @@ export async function registerAnonymousUser(
return dbUser;
}

const isPasswordCorrect = await compare(
const isPasswordCorrect = await comparePassword(
password,
existingIdentity.password
);
Expand Down
24 changes: 23 additions & 1 deletion backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import {
} from './db/actions/users';
import { isLicenced } from './security/is-licenced';
import rateLimit from 'express-rate-limit';
import { validateLicence } from './db/actions/licences';
import { fetchLicence, validateLicence } from './db/actions/licences';
import { hasField } from './security/payload-checker';
import mung from 'express-mung';
import { QueryFailedError } from 'typeorm';
Expand All @@ -81,6 +81,9 @@ isLicenced().then((hasLicence) => {
chalk`{green ----------------------------------------------- }`
);
console.log(chalk`👍 {green This software is licenced.} `);
console.log(
chalk`🔑 {green This licence belongs to ${hasLicence.owner}.} `
);
console.log(
chalk`{green ----------------------------------------------- }`
);
Expand Down Expand Up @@ -490,6 +493,7 @@ db().then(() => {
}
});

// Keep this for backward compatibility
app.post('/api/self-hosted', heavyLoadLimiter, async (req, res) => {
const payload = req.body as SelfHostedCheckPayload;
console.log('Attempting to verify self-hosted licence for ', payload.key);
Expand All @@ -508,6 +512,24 @@ db().then(() => {
}
});

app.post('/api/self-hosted-licence', heavyLoadLimiter, async (req, res) => {
const payload = req.body as SelfHostedCheckPayload;
console.log('Attempting to verify self-hosted licence for ', payload.key);
try {
const licence = await fetchLicence(payload.key);
if (licence) {
console.log(' ==> Self hosted licence granted.');
res.status(200).send(licence);
} else {
console.log(' ==> Self hosted licence INVALID.');
res.status(403).send(null);
}
} catch {
console.log(' ==> Could not check for self-hosted licence.');
res.status(500).send('Something went wrong');
}
});

setupSentryErrorHandler(app);
});

Expand Down
73 changes: 62 additions & 11 deletions backend/src/security/is-licenced.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,28 @@
import { SelfHostedCheckPayload } from '@retrospected/common';
import config from '../config';
import fetch from 'node-fetch';
import wait from '../utils';
import wait, { comparePassword, decrypt } from '../utils';
import { LicenceMetadata } from 'src/types';

let licenced: boolean | null = null;
let licenced: LicenceMetadata | null = null;

type HardcodedLicence = {
hash: string;
encryptedOwner: string;
};

const hardcodedLicences: HardcodedLicence[] = [
{
hash: '$2a$10$kt4DnxKZEwvoh052JFygru7iLiIrTzSJngcJlaYkWm.tlNzRJx/Di',
encryptedOwner: 'U2FsdGVkX18/e8sfZ3bpjz3pLQkCxloH8nuniFdU+vo=',
},
];

export function isSelfHostedAndLicenced() {
return !!licenced && config.SELF_HOSTED;
}

export async function isLicenced() {
export async function isLicenced(): Promise<LicenceMetadata | null> {
if (licenced !== null) {
return licenced;
}
Expand All @@ -19,15 +32,43 @@ export async function isLicenced() {
return result;
}

async function isLicencedBase() {
async function checkHardcodedLicence(
key: string
): Promise<LicenceMetadata | null> {
for (const hardcodedLicence of hardcodedLicences) {
const match = await comparePassword(key, hardcodedLicence.hash);
if (match) {
const decrypted = decrypt(hardcodedLicence.encryptedOwner, key);
return {
licence: key,
owner: decrypted,
};
}
}
return null;
}

// async function buildHardcodedLicence(
// licenceKey: string,
// company: string
// ): Promise<void> {
// console.log('Building hardcoded licence for: ', licenceKey);
// const hash = await hashPassword(licenceKey);
// console.log('Hash: ', hash);
// console.log('Encrypted company name: ', encrypt(company, licenceKey));
// }

async function isLicencedBase(): Promise<LicenceMetadata | null> {
if (process.env.NODE_ENV !== 'production') {
return true;
return { licence: 'dev', owner: 'dev' };
}

const licenceKey = config.LICENCE_KEY;

const payload: SelfHostedCheckPayload = { key: licenceKey };
try {
const response = await fetch(
'https://www.retrospected.com/api/self-hosted',
'https://www.retrospected.com/api/self-hosted-licence',
{
method: 'POST',
body: JSON.stringify(payload),
Expand All @@ -37,16 +78,26 @@ async function isLicencedBase() {
}
);
if (response.ok) {
const result = await response.text();
return result === 'true';
const result = (await response.json()) as LicenceMetadata;
return result;
} else {
console.error('Could not contact the licence server');
console.error(
'Could not contact the licence server. If you have a valid licence, please contact [email protected] for support.'
);
console.log(response.status, response.statusText);
}
} catch (err) {
console.error('Could not contact the licence server');
console.error(
'Could not contact the licence server. If you have a valid licence, please contact [email protected] for support.'
);
console.log(err);
}

return false;
// Checking hardcoded licence as a last resort
const hardcoded = await checkHardcodedLicence(licenceKey);
if (hardcoded) {
return hardcoded;
}

return null;
}
5 changes: 5 additions & 0 deletions backend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,8 @@ export interface BackendConfig {
RATE_LIMIT_WS_DURATION: number;
WS_MAX_BUFFER_SIZE: number;
}

export type LicenceMetadata = {
licence: string;
owner: string;
};
23 changes: 22 additions & 1 deletion backend/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Request } from 'express';
import { genSalt, hash } from 'bcryptjs';
import { compare, genSalt, hash } from 'bcryptjs';
import aes from 'crypto-js/aes';
import { stringify } from 'crypto-js/enc-utf8';
import { UserView, UserIdentityEntity } from './db/entities';
import { getUserView, getUser, getIdentity } from './db/actions/users';
import { Quota } from '@retrospected/common';
Expand Down Expand Up @@ -51,6 +53,25 @@ export async function hashPassword(clearTextPassword: string): Promise<string> {
return hashedPassword;
}

export async function comparePassword(
clearTextPassword: string,
hashedPassword: string
): Promise<boolean> {
const match = await compare(clearTextPassword, hashedPassword);
return match;
}

export function encrypt(clear: string, key: string): string {
const encrypted = aes.encrypt(clear, key).toString();
return encrypted;
}

export function decrypt(encrypted: string, key: string): string {
const bytes = aes.decrypt(encrypted, key);
const clear = stringify(bytes);
return clear;
}

export default async function wait(delay = 1000) {
return new Promise((resolve) => {
setTimeout(resolve, delay);
Expand Down

0 comments on commit e6d37f0

Please sign in to comment.