Skip to content

Commit bb06e98

Browse files
authored
Merge pull request #29 from spknetwork/unlink-have-account
Link hive account
2 parents 18737b7 + a63bdc1 commit bb06e98

15 files changed

+182
-216
lines changed

Dockerfile.local

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ FROM node:18.14.2-alpine
22
WORKDIR /usr/app
33
COPY package*.json ./
44
RUN apk add --no-cache git python3 make g++
5-
RUN npm install && npm install typescript -g
5+
RUN npm install
66
COPY . .
77
RUN npm run tsc:noemit
88
CMD ["npm", "run", "dev"]

Dockerfile.staging

+2-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ FROM node:18.14.2-alpine
22
WORKDIR /usr/app
33
COPY package*.json ./
44
RUN apk add --no-cache git python3 make g++ ffmpeg
5-
RUN npm install && npm install typescript -g
5+
RUN npm install
66
COPY . .
7-
RUN tsc
7+
RUN npm run build
88
CMD ["node","--experimental-specifier-resolution=node", "./dist/index.js"]

src/repositories/hive-chain/hive-chain.repository.spec.ts

+17-26
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,8 @@ describe('HiveRepository', () => {
1717
const blueTeamPrivateKey = PrivateKey.fromSeed(crypto.randomBytes(32).toString("hex"));
1818
const blueTeamPublicKey = blueTeamPrivateKey.createPublic();
1919
const blueTeamPublicKeyString = blueTeamPublicKey.toString();
20-
2120
const message = JSON.stringify({ ts: Date.now() });
2221
const forgedSignature = redTeamPrivateKey.sign(crypto.createHash('sha256').update(message).digest());
23-
2422
const account: ExtendedAccount = {
2523
name: 'blueteam',
2624
memo_key: blueTeamPublicKeyString,
@@ -110,16 +108,15 @@ describe('HiveRepository', () => {
110108
lifetime_market_bandwidth: '',
111109
last_market_bandwidth_update: ''
112110
};
113-
114-
// Act
115-
const result = hiveRepository.verifyHiveMessage(
116-
crypto.createHash('sha256').update('different message').digest(),
117-
forgedSignature.toString(),
118-
account
119-
);
120-
121-
// Assert
122-
expect(result).toBe(false);
111+
112+
// Act and Assert
113+
await expect(async () => {
114+
await hiveRepository.verifyHiveMessage(
115+
'different message',
116+
forgedSignature.toString(),
117+
account
118+
);
119+
}).rejects.toThrow();
123120
});
124121

125122
it('Should fail to verify an account if the signature height isn`t enough', async () => {
@@ -220,15 +217,12 @@ describe('HiveRepository', () => {
220217
last_market_bandwidth_update: ''
221218
};
222219

223-
// Act
224-
const result = hiveRepository.verifyHiveMessage(
225-
crypto.createHash('sha256').update(message).digest(),
220+
// Assert
221+
expect(async () => await hiveRepository.verifyHiveMessage(
222+
message,
226223
signature.toString(),
227224
account
228-
);
229-
230-
// Assert
231-
expect(result).toBe(false);
225+
)).rejects.toThrow();
232226
});
233227

234228
it('Should successfully verify a valid Hive message', async () => {
@@ -329,15 +323,12 @@ describe('HiveRepository', () => {
329323
last_market_bandwidth_update: ''
330324
};
331325

332-
// Act
333-
const result = hiveRepository.verifyHiveMessage(
334-
crypto.createHash('sha256').update(message).digest(),
326+
// Assert
327+
expect(async () => await hiveRepository.verifyHiveMessage(
328+
message,
335329
signature.toString(),
336330
account
337-
);
338-
339-
// Assert
340-
expect(result).toBe(true);
331+
)).not.toThrow();
341332
});
342333
})
343334

src/repositories/hive-chain/hive-chain.repository.ts

+34-21
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
1+
import { BadRequestException, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
22
import hiveJsPackage from '@hiveio/hive-js';
33
import { AuthorPerm, OperationsArray } from './types';
44
import {
@@ -8,6 +8,7 @@ import {
88
PrivateKey,
99
PublicKey,
1010
Signature,
11+
cryptoUtils,
1112
} from '@hiveio/dhive';
1213
import crypto from 'crypto';
1314
import 'dotenv/config';
@@ -154,29 +155,41 @@ export class HiveChainRepository {
154155
);
155156
}
156157

157-
verifyHiveMessage(message: Buffer, signature: string, account: ExtendedAccount): boolean {
158+
async verifyHiveMessage(
159+
message: string,
160+
signature: string,
161+
account: ExtendedAccount,
162+
): Promise<void> {
163+
const bufferMessage = cryptoUtils.sha256(message);
164+
let hasValidKey = false;
165+
158166
for (const auth of account.posting.key_auths) {
159167
const publicKey = PublicKey.fromString(auth[0].toString());
160168
if (auth[1] < account.posting.weight_threshold) continue;
161-
try {
162-
const signatureBuffer = Signature.fromBuffer(Buffer.from(signature, 'hex'));
163-
const verified = publicKey.verify(message, signatureBuffer);
164-
if (verified) {
165-
return true;
166-
}
167-
} catch (e) {
168-
this.#logger.debug(e);
169+
170+
hasValidKey = true;
171+
const signatureBuffer = Signature.fromBuffer(Buffer.from(signature, 'hex'));
172+
const verified = publicKey.verify(bufferMessage, signatureBuffer);
173+
if (verified) {
174+
return;
169175
}
170176
}
171-
return false;
177+
178+
if (!hasValidKey) {
179+
throw new UnauthorizedException('No valid keys found with sufficient weight');
180+
}
181+
182+
throw new UnauthorizedException('The message did not match the signature');
172183
}
173184

174185
async vote(options: { author: string; permlink: string; voter: string; weight: number }) {
175186
if (options.weight < -10_000 || options.weight > 10_000) {
176187
this.#logger.error(
177188
`Vote weight was out of bounds: ${options.weight}. Skipping ${options.author}/${options.permlink}`,
178189
);
179-
throw new BadRequestException('Hive vote weight out of bounds. Must be between -10000 and 10000');
190+
throw new BadRequestException(
191+
'Hive vote weight out of bounds. Must be between -10000 and 10000',
192+
);
180193
}
181194
return this._hive.broadcast.vote(
182195
options,
@@ -221,16 +234,16 @@ export class HiveChainRepository {
221234
}
222235

223236
verifyPostingAuth(account: ExtendedAccount): boolean {
224-
let doWe = false;
225-
if (Array.isArray(account.posting.account_auths)) {
226-
account.posting.account_auths.forEach(function (item) {
227-
if (item[0] === process.env.DELEGATED_ACCOUNT) {
228-
doWe = true;
229-
}
230-
});
231-
return doWe;
232-
} else {
237+
if (!Array.isArray(account.posting.account_auths)) {
233238
return false;
234239
}
240+
241+
for (const item of account.posting.account_auths) {
242+
if (item[0] === process.env.DELEGATED_ACCOUNT) {
243+
return true;
244+
}
245+
}
246+
247+
return false;
235248
}
236249
}

src/repositories/linked-accounts/linked-account.repository.ts

+7-12
Original file line numberDiff line numberDiff line change
@@ -11,28 +11,23 @@ export class LinkedAccountRepository {
1111
private readonly linkedAccountModel: Model<LinkedAccount>,
1212
) {}
1313

14-
async linkHiveAccount(user_id: string, account: string, challenge: string) {
14+
async linkHiveAccount(user_id: string, account: string) {
1515
return await this.linkedAccountModel.create({
16-
status: 'unverified',
1716
user_id,
1817
account,
18+
status: 'verified',
1919
network: 'HIVE',
20-
challenge,
21-
linked_at: new Date(),
22-
verified_at: null,
23-
type: 'native',
24-
});
25-
}
26-
27-
async findOneByChallenge(query: { challenge: LinkedAccount['challenge'] }) {
28-
return await this.linkedAccountModel.findOne(query);
20+
} satisfies LinkedAccount);
2921
}
3022

3123
async findOneByUserIdAndAccountName(query: {
3224
account: LinkedAccount['account'];
3325
user_id: LinkedAccount['user_id'];
3426
}) {
35-
return await this.linkedAccountModel.findOne(query);
27+
return await this.linkedAccountModel.findOne({
28+
...query,
29+
status: 'verified',
30+
} satisfies Partial<LinkedAccount>);
3631
}
3732

3833
async verify(_id: ObjectId) {

src/repositories/linked-accounts/schemas/linked-account.schema.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ export class LinkedAccount {
1414
@Prop({ required: true })
1515
user_id: string;
1616

17-
@Prop({ required: true })
18-
challenge: string;
17+
@Prop({ required: true, enum: ['HIVE'] })
18+
network: 'HIVE';
1919
}
2020

2121
export const LinkedAccountSchema = SchemaFactory.createForClass(LinkedAccount);

src/services/api/api.contoller.test.ts

+21-5
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { HiveChainModule } from '../../repositories/hive-chain/hive-chain.module
2323
import { EmailModule } from '../email/email.module';
2424
import * as crypto from 'crypto';
2525
import { HiveModule } from '../hive/hive.module';
26+
import { PrivateKey } from '@hiveio/dhive';
2627

2728
describe('ApiController', () => {
2829
let app: INestApplication;
@@ -138,7 +139,16 @@ describe('ApiController', () => {
138139
describe('/POST /v1/hive/linkaccount', () => {
139140
it('should link a Hive account', async () => {
140141
const jwtToken = 'test_jwt_token';
141-
const body = { username: 'test-account' };
142+
143+
const privateKey = PrivateKey.fromSeed(crypto.randomBytes(32).toString("hex"));
144+
const publicKey = privateKey.createPublic();
145+
const publicKeyString = publicKey.toString();
146+
const message = "singleton/bob/did is the owner of @starkerz";
147+
const signature = privateKey.sign(crypto.createHash('sha256').update(message).digest());
148+
149+
const body = { username: 'starkerz', proof: signature.toString() };
150+
151+
process.env.TEST_PUBLIC_KEY = publicKeyString;
142152

143153
return request(app.getHttpServer())
144154
.post('/v1/hive/linkaccount')
@@ -147,7 +157,12 @@ describe('ApiController', () => {
147157
.expect(201)
148158
.then(response => {
149159
expect(response.body).toEqual({
150-
challenge: expect.any(String),
160+
__v: 0,
161+
_id: expect.any(String),
162+
account: "starkerz",
163+
network: "HIVE",
164+
status: "verified",
165+
user_id: "singleton/bob/did",
151166
});
152167
});
153168
});
@@ -165,8 +180,9 @@ describe('ApiController', () => {
165180
expect(response.body).toEqual({
166181
id: "test_user_id",
167182
network: "did",
168-
sub: "test_user_id",
169-
username: "test",
183+
sub: "singleton/bob/did",
184+
type: "singleton",
185+
username: "test_user_id",
170186
});
171187
});
172188
});
@@ -177,7 +193,7 @@ describe('ApiController', () => {
177193
const jwtToken = 'test_jwt_token';
178194

179195
// Mock linking and verifying an account
180-
const link = await linkedAccountsRepository.linkHiveAccount('singleton/bob/did', 'test-account', 'challenge');
196+
const link = await linkedAccountsRepository.linkHiveAccount('singleton/bob/did', 'test-account');
181197
await linkedAccountsRepository.verify(link._id);
182198

183199
return request(app.getHttpServer())

0 commit comments

Comments
 (0)