Skip to content

Commit

Permalink
Merge pull request #111 from rndquu/feat/104
Browse files Browse the repository at this point in the history
  • Loading branch information
0x4007 authored Sep 13, 2024
2 parents 9e36a60 + 0d34345 commit bc091a5
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 1 deletion.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,32 @@ with:
post: true
debug: false
```
## How to encrypt the `evmPrivateEncrypted` parameter

Partner private key (`evmPrivateEncrypted` config param in `conversation-rewards` plugin) supports 2 formats:
1. `PRIVATE_KEY:GITHUB_OWNER_ID`
2. `PRIVATE_KEY:GITHUB_OWNER_ID:GITHUB_REPOSITORY_ID`

Here `GITHUB_OWNER_ID` can be:
1. Github organization id (if ubiquibot is used within an organization)
2. Github user id (if ubiquibot is simply installed in a user's repository)

Format `PRIVATE_KEY:GITHUB_OWNER_ID` restricts in which particular organization (or user related repositories)
this private key can be used. It can be set either in the organization wide config either in the repository wide one.

Format `PRIVATE_KEY:GITHUB_OWNER_ID:GITHUB_REPOSITORY_ID` restricts organization (or user related repositories) and a particular repository where private key is allowed to be used.

How to encrypt for you local organization for testing purposes:
1. Get your organization (or user) id
```
curl -H "Accept: application/json" -H "Authorization: token GITHUB_PAT_TOKEN" https://api.github.com/orgs/ubiquity
```
2. Open https://keygen.ubq.fi/
3. Click "Generate" to create a new `x25519_PRIVATE_KEY` (which will be used in the `conversation-rewards` plugin to decrypt encrypted wallet private key)
4. Input a string in the format `PRIVATE_KEY:GITHUB_OWNER_ID` in the `PLAIN_TEXT` UI text input where:
- `PRIVATE_KEY`: your ethereum wallet private key without the `0x` prefix
- `GITHUB_OWNER_ID`: your github organization id or user id (which you got from step 1)
5. Click "Encrypt" to get an encrypted value in the `CIPHER_TEXT` field
6. Set the encrypted text (from step 5) in the `evmPrivateEncrypted` config parameter
7. Set `X25519_PRIVATE_KEY` environment variable in github secrets of your forked instance of the `conversation-rewards` plugin
72 changes: 72 additions & 0 deletions src/parser/permit-generation-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
SupportedEvents,
TokenType,
} from "@ubiquibot/permit-generation/core";
import { decrypt, parseDecryptedPrivateKey } from "@ubiquibot/permit-generation/utils";
import Decimal from "decimal.js";
import configuration from "../configuration/config-reader";
import {
Expand Down Expand Up @@ -53,6 +54,11 @@ export class PermitGenerationModule implements Module {
console.warn("[PermitGenerationModule] Invalid env detected, skipping.");
return Promise.resolve(result);
}
const isPrivateKeyAllowed = await this._isPrivateKeyAllowed(payload.evmPrivateEncrypted, program.eventPayload.repository.owner.id, program.eventPayload.repository.id);
if (!isPrivateKeyAllowed) {
console.warn("[PermitGenerationModule] Private key is not allowed to be used in this organization/repository.");
return Promise.resolve(result);
}
const eventName = context.eventName as SupportedEvents;
const octokit = getOctokitInstance();
const logger = {
Expand Down Expand Up @@ -267,6 +273,72 @@ export class PermitGenerationModule implements Module {
}
}

/**
* Checks whether partner's private key is allowed to be used in current repository.
*
* If partner accidentally shares his encrypted private key then a malicious user
* will be able to use that leaked private key in another organization with permits
* generated from a leaked partner's wallet.
*
* Partner private key (`evmPrivateEncrypted` config param in `conversation-rewards` plugin) supports 2 formats:
* 1. PRIVATE_KEY:GITHUB_OWNER_ID
* 2. PRIVATE_KEY:GITHUB_OWNER_ID:GITHUB_REPOSITORY_ID
*
* Here `GITHUB_OWNER_ID` can be:
* 1. Github organization id (if ubiquibot is used within an organization)
* 2. Github user id (if ubiquibot is simply installed in a user's repository)
*
* Format "PRIVATE_KEY:GITHUB_OWNER_ID" restricts in which particular organization (or user related repositories)
* this private key can be used. It can be set either in the organization wide config either in the repository wide one.
*
* Format "PRIVATE_KEY:GITHUB_OWNER_ID:GITHUB_REPOSITORY_ID" restricts organization (or user related repositories) and
* a particular repository where private key is allowed to be used.
*
* @param privateKeyEncrypted Encrypted private key (with "X25519_PRIVATE_KEY") string (in any of the 2 different formats)
* @param githubContextOwnerId Github organization or used id from which the "conversation-rewards" is executed
* @param githubContextRepositoryId Github repository id from which the "conversation-rewards" is executed
* @returns Whether private key is allowed to be used in current owner/repository context
*/
async _isPrivateKeyAllowed(
privateKeyEncrypted: string,
githubContextOwnerId: number,
githubContextRepositoryId: number
): Promise<boolean> {
// decrypt private key
const privateKeyDecrypted = await decrypt(privateKeyEncrypted, process.env.X25519_PRIVATE_KEY);

// parse decrypted private key
const privateKeyParsed = parseDecryptedPrivateKey(privateKeyDecrypted);
if (!privateKeyParsed.privateKey) {
console.log("Private key could not be decrypted");
return false;
}

// private key + owner id
// Format: PRIVATE_KEY:GITHUB_OWNER_ID
if (privateKeyParsed.allowedOrganizationId && !privateKeyParsed.allowedRepositoryId) {
if (privateKeyParsed.allowedOrganizationId !== githubContextOwnerId) {
console.log(`Current organization/user id ${githubContextOwnerId} is not allowed to use this private key`);
return false;
}
return true;
}

// private key + owner id + repository id
// Format: PRIVATE_KEY:GITHUB_OWNER_ID:GITHUB_REPOSITORY_ID
if (privateKeyParsed.allowedOrganizationId && privateKeyParsed.allowedRepositoryId) {
if (privateKeyParsed.allowedOrganizationId !== githubContextOwnerId || privateKeyParsed.allowedRepositoryId !== githubContextRepositoryId) {
console.log(`Current organization/user id ${githubContextOwnerId} and repository id ${githubContextRepositoryId} are not allowed to use this private key`);
return false;
}
return true;
}

// otherwise invalid private key format
console.log("Invalid private key format");
return false;
}

get enabled(): boolean {
if (!Value.Check(permitGenerationConfigurationType, this._configuration)) {
console.warn("Invalid / missing configuration detected for PermitGenerationModule, disabling.");
Expand Down
2 changes: 1 addition & 1 deletion tests/__mocks__/results/valid-configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
},
"erc20RewardToken": "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d",
"evmNetworkId": 100,
"evmPrivateEncrypted": "kmpTKq5Wh9r9x5j3U9GqZr3NYnjK2g0HtbzeUBOuLC2y3x8ja_SKBNlB2AZ6LigXHP_HeMitftVUtzmoj8CFfVP9SqjWoL6IPku1hVTWkdTn97g1IxzmjydFxjdcf0wuDW1hvVtoq3Uw5yALABqxcQ",
"evmPrivateEncrypted": "qGslGsT9bfY0C3uc_utvFhBN_Zwggf0sONEFOfF67E_gWDXqnV3qC-LnGj2ufIHJwdHxFLz3VVzup2MrGtFtHhZ2WZZvq_8NQi48LFbr5b6OIxL-vOXooAyQYfb2mjLmcsYaxXc5eu1mUfO9rfENSAw9dFeWfi9VKQ",
"incentives": {
"contentEvaluator": {
"multipliers": [
Expand Down
117 changes: 117 additions & 0 deletions tests/parser/permit-generation-module.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,121 @@ describe("permit-generation-module.ts", () => {
expect(resultAfterFees["ubiquibot-treasury"].total).toEqual(11.11);
});
});

describe("_isPrivateKeyAllowed()", () => {
beforeEach(() => {
// set dummy X25519_PRIVATE_KEY
process.env.X25519_PRIVATE_KEY = "wrQ9wTI1bwdAHbxk2dfsvoK1yRwDc0CEenmMXFvGYgY";
});

it("Should return false if private key could not be decrypted", async () => {
const permitGenerationModule = new PermitGenerationModule();
const spyConsoleLog = jest.spyOn(console, "log");

// format: "PRIVATE_KEY"
// encrypted value: ""
const privateKeyEncrypted = "Y-29-JttQ7xNBMOQylST_9kgVjVKnvkYlyihbqsBwGDmuBi7ZlaIh1I6cTDzrMiR";
const githubContextOrganizationId = 1;
const githubContextRepositoryId = 2;

const result = await permitGenerationModule._isPrivateKeyAllowed(privateKeyEncrypted, githubContextOrganizationId, githubContextRepositoryId);

expect(result).toEqual(false);
expect(spyConsoleLog).toHaveBeenCalledWith("Private key could not be decrypted");
});

it("Should return false if private key is used in unallowed organization", async () => {
const permitGenerationModule = new PermitGenerationModule();
const spyConsoleLog = jest.spyOn(console, "log");

// format: "PRIVATE_KEY:GITHUB_ORGANIZATION_ID"
// encrypted value: "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80:1"
const privateKeyEncrypted = "fdsmuUN_jTF-VAWMe55ozcg6AuLOKiyJm8unRg1QwnY9u_fsKmczRtekx6aq59ndQ0RDJ803SkeTOlUW87cd93rDTiq57ErxkRwq4j4SKYTitChIWAZw0-LCJAd2IvRmN9qVzA7oXEdkUihXkErGGtqK";
const githubContextOrganizationId = 99;
const githubContextRepositoryId = 2;

const result = await permitGenerationModule._isPrivateKeyAllowed(privateKeyEncrypted, githubContextOrganizationId, githubContextRepositoryId);

expect(result).toEqual(false);
expect(spyConsoleLog).toHaveBeenCalledWith("Current organization/user id 99 is not allowed to use this private key");
});

it("Should return true if private key is used in allowed organization", async () => {
const permitGenerationModule = new PermitGenerationModule();
const spyConsoleLog = jest.spyOn(console, "log");

// format: "PRIVATE_KEY:GITHUB_ORGANIZATION_ID"
// encrypted value: "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80:1"
const privateKeyEncrypted = "fdsmuUN_jTF-VAWMe55ozcg6AuLOKiyJm8unRg1QwnY9u_fsKmczRtekx6aq59ndQ0RDJ803SkeTOlUW87cd93rDTiq57ErxkRwq4j4SKYTitChIWAZw0-LCJAd2IvRmN9qVzA7oXEdkUihXkErGGtqK";
const githubContextOrganizationId = 1;
const githubContextRepositoryId = 2;

const result = await permitGenerationModule._isPrivateKeyAllowed(privateKeyEncrypted, githubContextOrganizationId, githubContextRepositoryId);

expect(result).toEqual(true);
});

it("Should return false if private key is used in unallowed organization and allowed repository", async () => {
const permitGenerationModule = new PermitGenerationModule();
const spyConsoleLog = jest.spyOn(console, "log");

// format: "PRIVATE_KEY:GITHUB_ORGANIZATION_ID:GITHUB_REPOSITORY_ID"
// encrypted value: "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80:1:2"
const privateKeyEncrypted = "mgLMdW_zfTYn3oNB5O0RBvPQOU4SkE1dOnhc6IGrgTQkkEJB7tvaEHdbZS0dEnq4VK21yShd5zdaRMbl6W2B6ij5tkrODH5-NEd8Uvp4Ks-NqrG-V3GkKrCJqCz3Cci3jrXU_rdn3Uil03d41eB4xluR_g8";
const githubContextOrganizationId = 99;
const githubContextRepositoryId = 2;

const result = await permitGenerationModule._isPrivateKeyAllowed(privateKeyEncrypted, githubContextOrganizationId, githubContextRepositoryId);

expect(result).toEqual(false);
expect(spyConsoleLog).toHaveBeenCalledWith("Current organization/user id 99 and repository id 2 are not allowed to use this private key");
});

it("Should return false if private key is used in allowed organization and unallowed repository", async () => {
const permitGenerationModule = new PermitGenerationModule();
const spyConsoleLog = jest.spyOn(console, "log");

// format: "PRIVATE_KEY:GITHUB_ORGANIZATION_ID:GITHUB_REPOSITORY_ID"
// encrypted value: "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80:1:2"
const privateKeyEncrypted = "mgLMdW_zfTYn3oNB5O0RBvPQOU4SkE1dOnhc6IGrgTQkkEJB7tvaEHdbZS0dEnq4VK21yShd5zdaRMbl6W2B6ij5tkrODH5-NEd8Uvp4Ks-NqrG-V3GkKrCJqCz3Cci3jrXU_rdn3Uil03d41eB4xluR_g8";
const githubContextOrganizationId = 1;
const githubContextRepositoryId = 99;

const result = await permitGenerationModule._isPrivateKeyAllowed(privateKeyEncrypted, githubContextOrganizationId, githubContextRepositoryId);

expect(result).toEqual(false);
expect(spyConsoleLog).toHaveBeenCalledWith("Current organization/user id 1 and repository id 99 are not allowed to use this private key");
});

it("Should return true if private key is used in allowed organization and repository", async () => {
const permitGenerationModule = new PermitGenerationModule();
const spyConsoleLog = jest.spyOn(console, "log");

// format: "PRIVATE_KEY:GITHUB_ORGANIZATION_ID:GITHUB_REPOSITORY_ID"
// encrypted value: "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80:1:2"
const privateKeyEncrypted = "mgLMdW_zfTYn3oNB5O0RBvPQOU4SkE1dOnhc6IGrgTQkkEJB7tvaEHdbZS0dEnq4VK21yShd5zdaRMbl6W2B6ij5tkrODH5-NEd8Uvp4Ks-NqrG-V3GkKrCJqCz3Cci3jrXU_rdn3Uil03d41eB4xluR_g8";
const githubContextOrganizationId = 1;
const githubContextRepositoryId = 2;

const result = await permitGenerationModule._isPrivateKeyAllowed(privateKeyEncrypted, githubContextOrganizationId, githubContextRepositoryId);

expect(result).toEqual(true);
});

it("Should return false if private key format is invalid", async () => {
const permitGenerationModule = new PermitGenerationModule();
const spyConsoleLog = jest.spyOn(console, "log");

// format: "PRIVATE_KEY:GITHUB_ORGANIZATION_ID:GITHUB_REPOSITORY_ID"
// encrypted value: "ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80:0:2"
const privateKeyEncrypted = "RIocCo0h_tMvLWieOZYPWzP7l7jVnVAno_QELfyUrRwVsN8aOHlx5hw5Be41bX74_Xaqef-gwTToXfdbgbqgLhLG0fxtw-QxKkWtzVnMlqO-WA2WVuaf3BpyiGFbVyyvFyFY_Q_O9gxY_F3xBPmHNfAwCPs";
const githubContextOrganizationId = 1;
const githubContextRepositoryId = 2;

const result = await permitGenerationModule._isPrivateKeyAllowed(privateKeyEncrypted, githubContextOrganizationId, githubContextRepositoryId);

expect(result).toEqual(false);
expect(spyConsoleLog).toHaveBeenCalledWith("Invalid private key format");
});
});
});
1 change: 1 addition & 0 deletions tests/process.issue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ jest.mock("../src/parser/command-line", () => {
name: "conversation-rewards",
owner: {
login: "ubiquibot",
id: 76412717, // https://github.com/ubiquity
},
},
},
Expand Down
1 change: 1 addition & 0 deletions tests/rewards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ jest.mock("../src/parser/command-line", () => {
name: "conversation-rewards",
owner: {
login: "ubiquibot",
id: 76412717, // https://github.com/ubiquity
},
},
},
Expand Down

0 comments on commit bc091a5

Please sign in to comment.