Skip to content

Commit

Permalink
Refactor Proof Interface (#252)
Browse files Browse the repository at this point in the history
Due to the limitation of Capacitor and Ionic framework, we cannot and should not use the same architecture from Starling Capture, which is based on Android framework. Thus, I have rewritten most parts regarding the data persistence. This includes:

- Merge `InformationRepository`, `SignatureRepository` and `SerializationService` into `ProofRepository` and provide the same functionality.
- Refactor `CollectorService` and the corresponding `InformationProvider`s and `SignatureProvider`s to accept newly merged `Proof` instance.
- Refactor `PublisherAlert` and the corresponding publisher `NumbersStoragePublisher` and `NumbersStorageApi` to accept newly merged `Proof` instance.
- Write unit tests to fully cover the refactored components, and thus the coverage has reached 70%.
- Implement an adapter (`old-proof-adapter`) to provide the connection between Numbers Storage Back-end.
- Rename `Information` to `Truth` and `Facts`.
- Remove deprecated sections in README.
  • Loading branch information
seanwu1105 committed Nov 26, 2020
1 parent 5ad45c9 commit df857b7
Show file tree
Hide file tree
Showing 66 changed files with 1,331 additions and 1,157 deletions.
20 changes: 1 addition & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,12 @@ Preview the app in web browser.
npm run serve
```

### Verification

The signature of proofs can be verified with [Web Crypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API). The signed message is generated by `services/serialization/serialization.service.ts#stringify()` method. A Python example for verification can be found in [the Starling Capture repository](https://github.com/numbersprotocol/starling-capture/tree/master/util/verification).

## Development

Start a local dev server for app dev/testing.

``` bash
ionic serve
npm run serve
```

Run tests.
Expand Down Expand Up @@ -110,14 +106,6 @@ The script does the same thing for you.
npm run build-android
```

### Architecture

See [the architecture of Starling Capture](https://github.com/numbersprotocol/starling-capture#architecture) for details.

### Serialization Schema

See [the serialization schema of Starling Capture](https://github.com/numbersprotocol/starling-capture#serialization-schema) for details.

### Caveat

* This app is still in the experimental stage.
Expand Down Expand Up @@ -154,9 +142,3 @@ When push to the `develop` branch with new version in the `package.json` file, G
1. Publish the app to Play Console on alpha track.
1. Upload debug apk to Google Drive.
1. Send notification to the private `reminder-releases` slack channel.

### Deploy

#### Demo App

The demo app is hosted on the GitHub Page. It would be updated when there is a new commit on the `master` branch.
11 changes: 8 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"@ngx-formly/material": "^5.0.0",
"@ngx-formly/schematics": "^5.10.3",
"async-mutex": "^0.2.4",
"image-blob-reduce": "^2.0.0",
"image-blob-reduce": "^2.1.1",
"immutable": "^4.0.0-rc.12",
"lodash": "^4.17.20",
"rxjs": "~6.6.3",
"tslib": "^2.0.1",
Expand Down
34 changes: 8 additions & 26 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,17 @@ import { Plugins } from '@capacitor/core';
import { Platform } from '@ionic/angular';
import { TranslocoService } from '@ngneat/transloco';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { map } from 'rxjs/operators';
import { concatMap } from 'rxjs/operators';
import { CameraService } from './services/camera/camera.service';
import { CollectorService } from './services/collector/collector.service';
import { CapacitorProvider } from './services/collector/information/capacitor-provider/capacitor-provider';
import { CapacitorProvider } from './services/collector/facts/capacitor-provider/capacitor-provider';
import { WebCryptoApiProvider } from './services/collector/signature/web-crypto-api-provider/web-crypto-api-provider';
import { LanguageService } from './services/language/language.service';
import { NotificationService } from './services/notification/notification.service';
import { AssetRepository } from './services/publisher/numbers-storage/data/asset/asset-repository.service';
import { NumbersStorageApi } from './services/publisher/numbers-storage/numbers-storage-api.service';
import { NumbersStoragePublisher } from './services/publisher/numbers-storage/numbers-storage-publisher';
import { AssetRepository } from './services/publisher/numbers-storage/repositories/asset/asset-repository.service';
import { PublishersAlert } from './services/publisher/publishers-alert/publishers-alert.service';
import { CaptionRepository } from './services/repositories/caption/caption-repository.service';
import { InformationRepository } from './services/repositories/information/information-repository.service';
import { ProofRepository } from './services/repositories/proof/proof-repository.service';
import { SignatureRepository } from './services/repositories/signature/signature-repository.service';
import { SerializationService } from './services/serialization/serialization.service';
import { fromExtension } from './utils/mime-type';

const { SplashScreen } = Plugins;
Expand All @@ -36,11 +31,6 @@ export class AppComponent {
private readonly platform: Platform,
private readonly collectorService: CollectorService,
private readonly publishersAlert: PublishersAlert,
private readonly serializationService: SerializationService,
private readonly proofRepository: ProofRepository,
private readonly informationRepository: InformationRepository,
private readonly signatureRepository: SignatureRepository,
private readonly captionRepository: CaptionRepository,
private readonly translocoService: TranslocoService,
private readonly notificationService: NotificationService,
private readonly numbersStorageApi: NumbersStorageApi,
Expand All @@ -60,10 +50,9 @@ export class AppComponent {

restoreAppStatus() {
this.cameraService.restoreKilledAppResult$().pipe(
map(cameraPhoto => this.collectorService.storeAndCollect(
cameraPhoto.base64String,
fromExtension(cameraPhoto.format)
)),
concatMap(cameraPhoto => this.collectorService.runAndStore({
[cameraPhoto.base64String]: { mimeType: fromExtension(cameraPhoto.format) }
})),
untilDestroyed(this)
).subscribe();
}
Expand All @@ -76,22 +65,15 @@ export class AppComponent {

initializeCollector() {
WebCryptoApiProvider.initialize$().pipe(untilDestroyed(this)).subscribe();
this.collectorService.addInformationProvider(
new CapacitorProvider(this.informationRepository, this.translocoService)
);
this.collectorService.addSignatureProvider(
new WebCryptoApiProvider(this.signatureRepository, this.serializationService)
);
this.collectorService.addFactsProvider(new CapacitorProvider());
this.collectorService.addSignatureProvider(new WebCryptoApiProvider());
}

initializePublisher() {
this.publishersAlert.addPublisher(
new NumbersStoragePublisher(
this.translocoService,
this.notificationService,
this.proofRepository,
this.signatureRepository,
this.captionRepository,
this.numbersStorageApi,
this.assetRepository
)
Expand Down
57 changes: 31 additions & 26 deletions src/app/pages/home/asset/asset.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Plugins } from '@capacitor/core';
import { TranslocoService } from '@ngneat/transloco';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { defer } from 'rxjs';
import { map, pluck, switchMap, switchMapTo, tap } from 'rxjs/operators';
import { combineLatest, defer, forkJoin, zip } from 'rxjs';
import { concatMap, map, switchMap, switchMapTo, tap } from 'rxjs/operators';
import { BlockingActionService } from 'src/app/services/blocking-action/blocking-action.service';
import { CapacitorProvider } from 'src/app/services/collector/information/capacitor-provider/capacitor-provider';
import { ConfirmAlert } from 'src/app/services/confirm-alert/confirm-alert.service';
import { AssetRepository } from 'src/app/services/publisher/numbers-storage/data/asset/asset-repository.service';
import { CaptionRepository } from 'src/app/services/repositories/caption/caption-repository.service';
import { InformationRepository } from 'src/app/services/repositories/information/information-repository.service';
import { AssetRepository } from 'src/app/services/publisher/numbers-storage/repositories/asset/asset-repository.service';
import { getOldProof } from 'src/app/services/repositories/proof/old-proof-adapter';
import { ProofRepository } from 'src/app/services/repositories/proof/proof-repository.service';
import { isNonNullable } from 'src/app/utils/rx-operators';
import { ContactSelectionDialogComponent, SelectedContact } from './contact-selection-dialog/contact-selection-dialog.component';
Expand All @@ -35,26 +33,31 @@ export class AssetPage {
switchMap(id => this.assetRepository.getById$(id)),
isNonNullable()
);
readonly proof$ = this.asset$.pipe(
switchMap(asset => this.proofRepository.getByHash$(asset.proof_hash)),
isNonNullable()
private readonly proofsWithOld$ = this.proofRepository.getAll$().pipe(
concatMap(proofs => Promise.all(proofs.map(async (proof) =>
({ proof, oldProof: await getOldProof(proof) })
)))
);
readonly base64Src$ = this.proof$.pipe(
switchMap(proof => this.proofRepository.getRawFile$(proof)),
map(rawBase64 => `data:image/png;base64,${rawBase64}`)
readonly capture$ = combineLatest([this.asset$, this.proofsWithOld$]).pipe(
map(([asset, proofsWithThumbnailAndOld]) => ({
asset,
proofWithThumbnailAndOld: proofsWithThumbnailAndOld.find(p => p.oldProof.hash === asset.proof_hash)
})),
isNonNullable()
);
readonly timestamp$ = this.proof$.pipe(pluck('timestamp'));
readonly latitude$ = this.proof$.pipe(
switchMap(proof => this.informationRepository.getByProof$(proof)),
map(informationList => informationList.find(information => information.provider === CapacitorProvider.ID && information.name === 'Current GPS Latitude')),
readonly base64Src$ = this.capture$.pipe(
map(capture => capture.proofWithThumbnailAndOld),
isNonNullable(),
pluck('value')
map(p => `data:${Object.values(p.proof.assets)[0].mimeType};base64,${Object.keys(p.proof.assets)[0]}`)
);
readonly longitude$ = this.proof$.pipe(
switchMap(proof => this.informationRepository.getByProof$(proof)),
map(informationList => informationList.find(information => information.provider === CapacitorProvider.ID && information.name === 'Current GPS Longitude')),
isNonNullable(),
pluck('value')
readonly timestamp$ = this.capture$.pipe(
map(capture => capture.proofWithThumbnailAndOld?.proof.timestamp)
);
readonly latitude$ = this.capture$.pipe(
map(capture => `${capture.proofWithThumbnailAndOld?.proof.geolocationLatitude}`)
);
readonly longitude$ = this.capture$.pipe(
map(capture => `${capture.proofWithThumbnailAndOld?.proof.geolocationLongitude}`)
);

constructor(
Expand All @@ -64,8 +67,6 @@ export class AssetPage {
private readonly confirmAlert: ConfirmAlert,
private readonly assetRepository: AssetRepository,
private readonly proofRepository: ProofRepository,
private readonly captionRepository: CaptionRepository,
private readonly informationRepository: InformationRepository,
private readonly blockingActionService: BlockingActionService,
private readonly snackBar: MatSnackBar,
private readonly dialog: MatDialog,
Expand Down Expand Up @@ -98,8 +99,12 @@ export class AssetPage {

private remove() {
const onConfirm = () => this.blockingActionService.run$(
this.asset$.pipe(
switchMap(asset => this.assetRepository.remove$(asset)),
zip(this.asset$, this.capture$).pipe(
concatMap(([asset, capture]) => forkJoin([
this.assetRepository.remove$(asset),
// tslint:disable-next-line: no-non-null-assertion
this.proofRepository.remove(capture.proofWithThumbnailAndOld!.proof)
])),
switchMapTo(defer(() => this.router.navigate(['..'])))
),
{ message: this.translocoService.translate('processing') }
Expand Down
22 changes: 4 additions & 18 deletions src/app/pages/home/asset/information/information.page.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,11 @@
<div mat-line>{{ mimeType$ | async }}</div>
</mat-list-item>

<div *ngIf="(locationInformation$ | async)?.length" mat-subheader>{{ t('location') }}</div>
<mat-list-item *ngFor="let information of (locationInformation$ | async)">
<div *ngIf="facts$ | async" mat-subheader>{{ t('information') }}</div>
<mat-list-item *ngFor="let fact of facts$ | async | keyvalue">
<mat-icon mat-list-icon>information</mat-icon>
<div mat-line>{{ information.name }}</div>
<div mat-line>{{ information.value }}</div>
</mat-list-item>

<div *ngIf="(deviceInformation$ | async)?.length" mat-subheader>{{ t('device') }}</div>
<mat-list-item *ngFor="let information of (deviceInformation$ | async)">
<mat-icon mat-list-icon>information</mat-icon>
<div mat-line>{{ information.name }}</div>
<div mat-line>{{ information.value }}</div>
</mat-list-item>

<div *ngIf="(otherInformation$ | async)?.length" mat-subheader>{{ t('other') }}</div>
<mat-list-item *ngFor="let information of (otherInformation$ | async)">
<mat-icon mat-list-icon>information</mat-icon>
<div mat-line>{{ information.name }}</div>
<div mat-line>{{ information.value }}</div>
<div mat-line>{{ fact.key }}</div>
<div mat-line>{{ fact.value }}</div>
</mat-list-item>

<div mat-subheader>{{ t('signature') }}</div>
Expand Down
68 changes: 36 additions & 32 deletions src/app/pages/home/asset/information/information.page.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { Component } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { UntilDestroy } from '@ngneat/until-destroy';
import { map, pluck, switchMap } from 'rxjs/operators';
import { WebCryptoApiProvider } from 'src/app/services/collector/signature/web-crypto-api-provider/web-crypto-api-provider';
import { AssetRepository } from 'src/app/services/publisher/numbers-storage/data/asset/asset-repository.service';
import { InformationType } from 'src/app/services/repositories/information/information';
import { InformationRepository } from 'src/app/services/repositories/information/information-repository.service';
import { combineLatest } from 'rxjs';
import { concatMap, map, switchMap } from 'rxjs/operators';
import { AssetRepository } from 'src/app/services/publisher/numbers-storage/repositories/asset/asset-repository.service';
import { getOldProof, getOldSignatures } from 'src/app/services/repositories/proof/old-proof-adapter';
import { ProofRepository } from 'src/app/services/repositories/proof/proof-repository.service';
import { SignatureRepository } from 'src/app/services/repositories/signature/signature-repository.service';
import { isNonNullable } from 'src/app/utils/rx-operators';

@UntilDestroy({ checkProperties: true })
Expand All @@ -24,41 +22,47 @@ export class InformationPage {
switchMap(id => this.assetRepository.getById$(id)),
isNonNullable()
);
readonly proof$ = this.asset$.pipe(
switchMap(asset => this.proofRepository.getByHash$(asset.proof_hash)),
isNonNullable()
private readonly proofsWithOld$ = this.proofRepository.getAll$().pipe(
concatMap(proofs => Promise.all(proofs.map(async (proof) =>
({ proof, oldProof: await getOldProof(proof) })
)))
);

readonly timestamp$ = this.proof$.pipe(pluck('timestamp'));
readonly hash$ = this.proof$.pipe(pluck('hash'));
readonly mimeType$ = this.proof$.pipe(pluck('mimeType'));

readonly locationInformation$ = this.proof$.pipe(
switchMap(proof => this.informationRepository.getByProof$(proof)),
map(informationList => informationList.filter(information => information.type === InformationType.Location))
readonly capture$ = combineLatest([this.asset$, this.proofsWithOld$]).pipe(
map(([asset, proofsWithThumbnailAndOld]) => ({
asset,
proofWithOld: proofsWithThumbnailAndOld.find(p => p.oldProof.hash === asset.proof_hash)
}))
);

readonly deviceInformation$ = this.proof$.pipe(
switchMap(proof => this.informationRepository.getByProof$(proof)),
map(informationList => informationList.filter(information => information.type === InformationType.Device))
readonly timestamp$ = this.capture$.pipe(
map(capture => capture.proofWithOld?.proof.timestamp)
);
readonly hash$ = this.capture$.pipe(
map(capture => capture.proofWithOld),
isNonNullable(),
concatMap(proofWithOld => proofWithOld.proof.getId())
);
readonly mimeType$ = this.capture$.pipe(
map(capture => capture.proofWithOld),
isNonNullable(),
map(proofWithOld => Object.values(proofWithOld.proof.assets)[0].mimeType)
);

readonly otherInformation$ = this.proof$.pipe(
switchMap(proof => this.informationRepository.getByProof$(proof)),
map(informationList => informationList.filter(information => information.type === InformationType.Other))
readonly facts$ = this.capture$.pipe(
map(capture => capture.proofWithOld),
isNonNullable(),
map(proofWithOld => Object.values(proofWithOld.proof.truth.providers)[0])
);

readonly signature$ = this.proof$.pipe(
switchMap(proof => this.signatureRepository.getByProof$(proof)),
map(signatures => signatures.find(signature => signature.provider === WebCryptoApiProvider.ID)),
isNonNullable()
readonly signature$ = this.capture$.pipe(
map(capture => capture.proofWithOld),
isNonNullable(),
concatMap(proofWithOld => getOldSignatures(proofWithOld.proof)),
map(oldSignatures => oldSignatures[0])
);

constructor(
private readonly route: ActivatedRoute,
private readonly proofRepository: ProofRepository,
private readonly informationRepository: InformationRepository,
private readonly signatureRepository: SignatureRepository,
private readonly assetRepository: AssetRepository
private readonly assetRepository: AssetRepository,
private readonly proofRepository: ProofRepository
) { }
}

0 comments on commit df857b7

Please sign in to comment.