Skip to content

Commit

Permalink
added asset validation when uploading assets to appstore
Browse files Browse the repository at this point in the history
  • Loading branch information
matthme committed Dec 11, 2023
1 parent c544478 commit 93e3512
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 5 deletions.
23 changes: 22 additions & 1 deletion src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { setupLogs } from './logs';
import { DEFAULT_APPS_DIRECTORY, ICONS_DIRECTORY } from './paths';
import { setLinkOpenHandlers } from './utils';
import { createHappWindow } from './windows';
import { APPSTORE_APP_ID } from './sharedTypes';
import { APPSTORE_APP_ID, AppHashes } from './sharedTypes';
import { nanoid } from 'nanoid';
import { APPLET_DEV_TMP_FOLDER_PREFIX, validateArgs } from './cli';
import { launch } from './launch';
Expand Down Expand Up @@ -390,6 +390,27 @@ app.whenReady().then(async () => {
await HOLOCHAIN_MANAGER!.adminWebsocket.enableApp({ installed_app_id: appId });
return appInfo;
});
ipcMain.handle('validate-happ-or-webhapp', async (_e, bytes: number[]): Promise<AppHashes> => {
const hashResult = await rustUtils.validateHappOrWebhapp(bytes);
const [happHash, uiHash, webHappHash] = hashResult.split('$');
if (uiHash) {
return {
type: 'webhapp',
sha256: webHappHash,
happ: {
sha256: happHash,
},
ui: {
sha256: uiHash,
},
};
} else {
return {
type: 'happ',
sha256: happHash,
};
}
});
ipcMain.handle(
'install-applet-bundle',
async (
Expand Down
17 changes: 17 additions & 0 deletions src/main/sharedTypes.ts
Original file line number Diff line number Diff line change
@@ -1 +1,18 @@
export const APPSTORE_APP_ID = 'AppstoreLight';

// ATTENTION: If this type is changed, the same type in src/renderer/types needs to be changed as well.
export type AppHashes =
| {
type: 'webhapp';
sha256: string;
happ: {
sha256: string;
};
ui: {
sha256: string;
};
}
| {
type: 'happ';
sha256: string;
};
1 change: 1 addition & 0 deletions src/preload/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
disableDevMode: () => ipcRenderer.invoke('disable-dev-mode'),
fetchIcon: (appActionHashB64: ActionHashB64) =>
ipcRenderer.invoke('fetch-icon', appActionHashB64),
validateHappOrWebhapp: (bytes: number[]) => ipcRenderer.invoke('validate-happ-or-webhapp', bytes),
});

declare global {
Expand Down
6 changes: 6 additions & 0 deletions src/renderer/src/electron-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { encode } from '@msgpack/msgpack';
import { WeNotification } from '@lightningrodlabs/we-applet';

import { ZomeCallNapi, ZomeCallUnsignedNapi } from 'hc-we-rust-utils';
import { AppHashes } from './types';

declare global {
interface Window {
Expand All @@ -38,6 +39,7 @@ declare global {
enableDevMode: () => Promise<void>;
disableDevMode: () => Promise<void>;
fetchIcon: (appActionHashB64: ActionHashB64) => Promise<string>;
validateHappOrWebhapp: (bytes: number[]) => Promise<AppHashes>;
};
}
}
Expand Down Expand Up @@ -112,6 +114,10 @@ export async function disableDevMode(): Promise<void> {
return window.electronAPI.disableDevMode();
}

export async function validateHappOrWebhapp(bytes: number[]) {
return window.electronAPI.validateHappOrWebhapp(bytes);
}

// export async function fetchAvailableUiUpdates(): Promise<
// Record<InstalledAppId, ResourceLocatorB64>
// > {
Expand Down
40 changes: 37 additions & 3 deletions src/renderer/src/layout/views/publishing-view.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { html, LitElement, css } from 'lit';
import { customElement, query, state } from 'lit/decorators.js';
import { localized, msg } from '@lit/localize';
import { notifyError, onSubmit } from '@holochain-open-dev/elements';
import { notify, notifyError, onSubmit } from '@holochain-open-dev/elements';

import '@shoelace-style/shoelace/dist/components/card/card.js';
import '@shoelace-style/shoelace/dist/components/icon/icon.js';
Expand All @@ -27,6 +27,8 @@ import {
} from '../../processes/appstore/appstore-light.js';
import { ActionHash } from '@holochain/client';
import { resizeAndExport } from '../../utils.js';
import { AppHashes } from '../../types.js';
import { validateHappOrWebhapp } from '../../electron-api.js';

enum PageView {
Loading,
Expand Down Expand Up @@ -60,6 +62,9 @@ export class PublishingView extends LitElement {
@state()
_creatingPublisher = false;

@state()
_publishing: string | undefined = undefined;

@query('#applet-icon-file-picker')
private _appletIconFilePicker!: HTMLInputElement;

Expand Down Expand Up @@ -150,11 +155,35 @@ export class PublishingView extends LitElement {
description: string;
webhapp_url: string;
}) {
this._publishing = 'Fetching resource for validation...';
console.log('TRYING TO PUBLISH APPLETS...');
if (!this._appletIconSrc) {
notifyError('No Icon provided.');
throw new Error('Icon is required.');
}
// try to fetch (web)happ from source to verify link
let byteArray: number[];
try {
const response = await fetch(fields.webhapp_url);
byteArray = Array.from(new Uint8Array(await response.arrayBuffer()));
} catch (e) {
this._publishing = undefined;
notifyError('Failed fetch resource at the specified URL');
throw new Error(`Failed resource at the specified URL: ${e}`);
}
// verify that resource is of the right format (happ or webhapp) and compute the hashes
let hashes: AppHashes;
try {
this._publishing = 'Validating resource format and computing hashes...';
hashes = await validateHappOrWebhapp(byteArray);
} catch (e) {
this._publishing = undefined;
notifyError(
`Asset format validation failed. Make sure the URL points to a valid .webhapp or .happ file.`,
);
throw new Error(`Asset format validation failed: ${e}`);
}

const appStoreClient = this.weStore.appletBundlesStore.appstoreClient;
if (!this._myPublisher) throw new Error('No publisher registered yet.');
const source: WebHappSource = {
Expand All @@ -174,7 +203,7 @@ export class PublishingView extends LitElement {
icon_src: this._appletIconSrc,
publisher: this._myPublisher!.action,
source: JSON.stringify(source),
hashes: 'not defined yet',
hashes: JSON.stringify(hashes),
};

console.log('got payload: ', payload);
Expand All @@ -183,6 +212,8 @@ export class PublishingView extends LitElement {
this._appletIconSrc = undefined;
this._myApps = myAppsEntities;
this.view = PageView.Main;
this._publishing = undefined;
notify('Applet published.');
}

async deprecateApplet(actionHash: ActionHash) {
Expand Down Expand Up @@ -434,6 +465,7 @@ export class PublishingView extends LitElement {
}}
style="margin-bottom: 10px; width: 600px;"
></sl-input>
<div>${this._publishing}</div>
<div class="row" style="margin-top: 40px; justify-content: center;">
<sl-button
variant="danger"
Expand All @@ -449,7 +481,9 @@ export class PublishingView extends LitElement {
}}
>${msg('Cancel')}
</sl-button>
<sl-button variant="primary" type="submit">${msg('Publish')} </sl-button>
<sl-button .loading=${!!this._publishing} variant="primary" type="submit">${msg(
'Publish',
)} </sl-button>
</div>
</div>
</form>
Expand Down
16 changes: 16 additions & 0 deletions src/renderer/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,19 @@ export type AppletHash = EntryHash;
* DnaHash of a We group
*/
export type GroupDnaHash = DnaHash;

export type AppHashes =
| {
type: 'webhapp';
sha256: string;
happ: {
sha256: string;
};
ui: {
sha256: string;
};
}
| {
type: 'happ';
sha256: string;
};
2 changes: 2 additions & 0 deletions utils/rust-utils/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

export function defaultConductorConfig(adminPort: number, conductorEnvironmentPath: string, keystoreConnectionUrl: string, bootstrapServerUrl: string, signalingServerUrl: string): string
export function saveHappOrWebhapp(happOrWebHappPath: string, uisDir: string, happsDir: string): Promise<string>
/** Checks that the happ or webhapp is of the correct format */
export function validateHappOrWebhapp(happOrWebhappBytes: Array<number>): Promise<string>
export interface ZomeCallUnsignedNapi {
cellId: Array<Array<number>>
zomeName: string
Expand Down
3 changes: 2 additions & 1 deletion utils/rust-utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,9 +252,10 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`)
}

const { defaultConductorConfig, saveHappOrWebhapp, WeRustHandler, ZomeCallSigner } = nativeBinding
const { defaultConductorConfig, saveHappOrWebhapp, validateHappOrWebhapp, WeRustHandler, ZomeCallSigner } = nativeBinding

module.exports.defaultConductorConfig = defaultConductorConfig
module.exports.saveHappOrWebhapp = saveHappOrWebhapp
module.exports.validateHappOrWebhapp = validateHappOrWebhapp
module.exports.WeRustHandler = WeRustHandler
module.exports.ZomeCallSigner = ZomeCallSigner
51 changes: 51 additions & 0 deletions utils/rust-utils/src/decode_webapp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,57 @@ pub async fn save_happ_or_webhapp(
}
}

/// Checks that the happ or webhapp is of the correct format
#[napi]
pub async fn validate_happ_or_webhapp(happ_or_webhapp_bytes: Vec<u8>) -> napi::Result<String> {
let (app_bundle, maybe_ui_and_webhapp_hash) = match WebAppBundle::decode(&happ_or_webhapp_bytes)
{
Ok(web_app_bundle) => {
let mut hasher = Sha256::new();
hasher.update(happ_or_webhapp_bytes);
let web_happ_hash = hex::encode(hasher.finalize());
// extracting ui.zip bytes
let web_ui_zip_bytes = web_app_bundle.web_ui_zip_bytes().await.map_err(|e| {
napi::Error::from_reason(format!("Failed to extract ui zip bytes: {}", e))
})?;

let mut hasher = Sha256::new();
hasher.update(web_ui_zip_bytes.clone().into_owned().into_inner());
let ui_hash = hex::encode(hasher.finalize());

// extracting happ bundle
let app_bundle = web_app_bundle.happ_bundle().await.map_err(|e| {
napi::Error::from_reason(format!(
"Failed to get happ bundle from webapp bundle bytes: {}",
e
))
})?;

(app_bundle, Some((ui_hash, web_happ_hash)))
}
Err(_) => {
let app_bundle = AppBundle::decode(&happ_or_webhapp_bytes).map_err(|e| {
napi::Error::from_reason(format!("Failed to decode happ file: {}", e))
})?;
(app_bundle, None)
}
};

let mut hasher = Sha256::new();
let app_bundle_bytes = app_bundle
.encode()
.map_err(|e| napi::Error::from_reason(format!("Failed to encode happ to bytes: {}", e)))?;
hasher.update(app_bundle_bytes);
let happ_hash = hex::encode(hasher.finalize());

match maybe_ui_and_webhapp_hash {
Some((ui_hash, web_happ_hash)) => {
Ok(format!("{}${}${}", happ_hash, ui_hash, web_happ_hash))
}
None => Ok(format!("{}", happ_hash)),
}
}

pub fn path_exists(path: &PathBuf) -> bool {
std::path::Path::new(path).exists()
}
Expand Down

0 comments on commit 93e3512

Please sign in to comment.