Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 5 additions & 12 deletions packages/core-types/src/SharedPasswordProtection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ export interface ISharedPasswordProtection extends ISerializable {
passwordType: "pw" | `pin${number}`;
salt: ICoreBuffer;
passwordLocationIndicator?: number;
password?: string;
}

export class SharedPasswordProtection extends Serializable implements ISharedPasswordProtection {
Expand All @@ -22,10 +21,6 @@ export class SharedPasswordProtection extends Serializable implements ISharedPas
@serialize({ any: true })
public passwordLocationIndicator?: number;

@validate({ nullable: true })
@serialize()
public password?: string;

public static from(value: ISharedPasswordProtection): SharedPasswordProtection {
return this.fromAny(value);
}
Expand All @@ -34,18 +29,16 @@ export class SharedPasswordProtection extends Serializable implements ISharedPas
if (value === undefined || value === "") return undefined;

const splittedPasswordParts = value.split("&");
if (![2, 3, 4].includes(splittedPasswordParts.length)) {
throw new CoreError("error.core-types.invalidTruncatedReference", "The password part of a TruncatedReference must consist of 2, 3 or 4 components.");
if (![2, 3].includes(splittedPasswordParts.length)) {
throw new CoreError("error.core-types.invalidTruncatedReference", "The password part of a TruncatedReference must consist of 2 or 3 components.");
}

const passwordType = splittedPasswordParts[0] as "pw" | `pin${number}`;
const passwordLocationIndicator = splittedPasswordParts.length > 2 && splittedPasswordParts[2] !== "" ? parseInt(splittedPasswordParts[2]) : undefined;

const salt = this.parseSalt(splittedPasswordParts[1]);

const password = splittedPasswordParts.length > 3 && splittedPasswordParts[3] ? splittedPasswordParts[3] : undefined;

return SharedPasswordProtection.from({ passwordType, salt, passwordLocationIndicator, password });
return SharedPasswordProtection.from({ passwordType, salt, passwordLocationIndicator });
}

private static parseSalt(value: string): CoreBuffer {
Expand All @@ -60,8 +53,8 @@ export class SharedPasswordProtection extends Serializable implements ISharedPas
public truncate(): string {
const base = `${this.passwordType}&${this.salt.toBase64()}`;

if (this.passwordLocationIndicator === undefined && this.password === undefined) return base;
if (this.passwordLocationIndicator === undefined) return base;

return `${base}&${this.passwordLocationIndicator ?? ""}${this.password ? `&${this.password}` : ""}`;
return `${base}&${this.passwordLocationIndicator ?? ""}`;
}
}
22 changes: 1 addition & 21 deletions packages/core-types/test/references/Reference.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,27 +50,7 @@ describe("Reference", () => {
);
});

test("toUrl with a cleartext password in the passwordProtection reference fields used", () => {
const reference = Reference.from({
id: CoreId.from("ANID1234"),
backboneBaseUrl: "https://backbone.example.com",
key: CryptoSecretKey.from({
secretKey: CoreBuffer.from("lerJyX8ydJDEXowq2PMMntRXXA27wgHJYA_BjnFx55Y"),
algorithm: CryptoEncryptionAlgorithm.XCHACHA20_POLY1305
}),
passwordProtection: SharedPasswordProtection.from({
passwordType: "pw",
salt: CoreBuffer.fromUtf8("a16byteslongsalt"),
password: "aPassword"
})
});

expect(reference.toUrl()).toBe(
"https://backbone.example.com/r/ANID1234#M3xsZXJKeVg4eWRKREVYb3dxMlBNTW50UlhYQTI3d2dISllBX0JqbkZ4NTVZfHxwdyZZVEUyWW5sMFpYTnNiMjVuYzJGc2RBPT0mJmFQYXNzd29yZA"
);
});

test("toUrl without a cleartext password but with a passwordLocationIndicator in the passwordProtection reference fields used", () => {
test("toUrl with a passwordLocationIndicator in the passwordProtection reference fields used", () => {
const reference = Reference.from({
id: CoreId.from("ANID1234"),
backboneBaseUrl: "https://backbone.example.com",
Expand Down
3 changes: 0 additions & 3 deletions packages/runtime-types/src/dtos/transport/EmptyTokenDTO.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { PasswordProtectionDTO } from "./PasswordProtectionDTO";

export interface EmptyTokenDTO {
id: string;
expiresAt: string;
passwordProtection: PasswordProtectionDTO;
reference: {
truncated: string;
url: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ export class FillDeviceOnboardingTokenWithNewDeviceUseCase extends UseCase<FillD
protected async executeInternal(request: FillDeviceOnboardingTokenWithNewDeviceRequest): Promise<Result<TokenDTO>> {
const reference = TokenReference.from(request.reference);

const passwordProtection = reference.passwordProtection;
if (!passwordProtection?.password) throw RuntimeErrors.devices.referenceNotPointingToAnEmptyToken();

const isEmptyToken = await this.tokenController.isEmptyToken(reference);
if (!isEmptyToken) throw RuntimeErrors.devices.referenceNotPointingToAnEmptyToken();

Expand All @@ -45,8 +42,7 @@ export class FillDeviceOnboardingTokenWithNewDeviceUseCase extends UseCase<FillD
const response = await this.tokenController.updateTokenContent({
id: reference.id,
content: TokenContentDeviceSharedSecret.from({ sharedSecret }),
secretKey: reference.key,
passwordProtection: reference.passwordProtection!
secretKey: reference.key
});

return Result.ok(TokenMapper.toTokenDTO(response, true));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export class TokenMapper {
return {
id: token.id.toString(),
expiresAt: token.expiresAt.toString(),
passwordProtection: PasswordProtectionMapper.toPasswordProtectionDTO(token.passwordProtection),
reference: {
truncated: reference.truncate(),
url: reference.toUrl()
Expand Down
5 changes: 0 additions & 5 deletions packages/runtime/test/anonymous/tokens.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,6 @@ describe("Anonymous tokens", () => {
test("should create an empty token", async () => {
const result = await noLoginRuntime.anonymousServices.tokens.createEmptyToken();
expect(result).toBeSuccessful();

const token = result.value;
expect(token.passwordProtection.password).toBeDefined();
expect(token.passwordProtection.passwordIsPin).toBeUndefined();
expect(token.passwordProtection.passwordLocationIndicator).toBeUndefined();
});

test("should get a proper error when trying to load an empty token", async () => {
Expand Down
5 changes: 2 additions & 3 deletions packages/transport/src/core/types/PasswordProtection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,11 @@ export class PasswordProtection extends Serializable implements IPasswordProtect
return this.fromAny(value);
}

public toSharedPasswordProtection(includeCleartextPassword?: boolean): SharedPasswordProtection {
public toSharedPasswordProtection(): SharedPasswordProtection {
return SharedPasswordProtection.from({
passwordType: this.passwordType,
salt: this.salt,
passwordLocationIndicator: this.passwordLocationIndicator,
password: includeCleartextPassword ? this.password : undefined
passwordLocationIndicator: this.passwordLocationIndicator
});
}

Expand Down
16 changes: 5 additions & 11 deletions packages/transport/src/modules/tokens/AnonymousTokenController.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Serializable } from "@js-soft/ts-serval";
import { CoreAddress, CoreDate, CoreId, Random, RandomCharacterRange } from "@nmshd/core-types";
import { CoreAddress, CoreDate, CoreId } from "@nmshd/core-types";
import { CryptoCipher, CryptoSecretKey } from "@nmshd/crypto";
import { CoreCrypto, IConfig, ICorrelator, TransportCoreErrors } from "../../core";
import { PasswordProtection } from "../../core/types/PasswordProtection";
Expand All @@ -16,27 +16,21 @@ export class AnonymousTokenController {

public async createEmptyToken(): Promise<EmptyToken> {
const secretKey = await CoreCrypto.generateSecretKey();
const password = await Random.string(16, RandomCharacterRange.Alphanumeric + RandomCharacterRange.SpecialCharacters);

const salt = await CoreCrypto.random(16);
const passwordProtection = PasswordProtection.from({ password, passwordType: "pw", salt });

const expiresAt = CoreDate.utc().add({ minutes: 2 });

const hashedPassword = (await CoreCrypto.deriveHashOutOfPassword(password, salt)).toBase64();
const response = (await this.client.createToken({ password: hashedPassword, expiresAt: expiresAt.toISOString() })).value;
const response = (await this.client.createToken({ expiresAt: expiresAt.toISOString() })).value;

return EmptyToken.from({ id: CoreId.from(response.id), secretKey: secretKey, expiresAt, passwordProtection });
return EmptyToken.from({ id: CoreId.from(response.id), secretKey: secretKey, expiresAt });
}

public async loadPeerTokenByReference(reference: TokenReference, password?: string): Promise<Token> {
if (reference.passwordProtection && !reference.passwordProtection.password && !password) throw TransportCoreErrors.general.noPasswordProvided();
if (reference.passwordProtection && !password) throw TransportCoreErrors.general.noPasswordProvided();

const passwordProtection = reference.passwordProtection
? PasswordProtection.from({
salt: reference.passwordProtection.salt,
passwordType: reference.passwordProtection.passwordType,
password: (password ?? reference.passwordProtection.password)!,
password: password!,
passwordLocationIndicator: reference.passwordProtection.passwordLocationIndicator
})
: undefined;
Expand Down
36 changes: 8 additions & 28 deletions packages/transport/src/modules/tokens/TokenController.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ISerializable, Serializable } from "@js-soft/ts-serval";
import { log } from "@js-soft/ts-utils";
import { CoreAddress, CoreDate, CoreId, Random, RandomCharacterRange } from "@nmshd/core-types";
import { CoreAddress, CoreDate, CoreId } from "@nmshd/core-types";
import { CoreBuffer, CryptoCipher, CryptoSecretKey } from "@nmshd/crypto";
import { CoreCrypto, TransportCoreErrors } from "../../core";
import { DbCollectionName } from "../../core/DbCollectionName";
Expand Down Expand Up @@ -93,18 +93,12 @@ export class TokenController extends TransportController {
const input = SendEmptyTokenParameters.from(parameters);
const secretKey = await CoreCrypto.generateSecretKey();

const password = await Random.string(16, RandomCharacterRange.Alphanumeric + RandomCharacterRange.SpecialCharacters);
const salt = await CoreCrypto.random(16);
const hashedPassword = (await CoreCrypto.deriveHashOutOfPassword(password, salt)).toBase64();
const passwordProtection = PasswordProtection.from({ password, passwordType: "pw", salt });

const response = (await this.client.createEmptyToken({ password: hashedPassword, expiresAt: input.expiresAt.toISOString() })).value;
const response = (await this.client.createEmptyToken({ expiresAt: input.expiresAt.toISOString() })).value;

const token = EmptyToken.from({
id: CoreId.from(response.id),
secretKey: secretKey,
expiresAt: input.expiresAt,
passwordProtection
expiresAt: input.expiresAt
});

if (!input.ephemeral) {
Expand Down Expand Up @@ -135,12 +129,12 @@ export class TokenController extends TransportController {
}

public async loadPeerTokenByReference(reference: TokenReference, ephemeral: boolean, password?: string): Promise<Token> {
if (reference.passwordProtection && !reference.passwordProtection.password && !password) throw TransportCoreErrors.general.noPasswordProvided();
if (reference.passwordProtection && !password) throw TransportCoreErrors.general.noPasswordProvided();
const passwordProtection = reference.passwordProtection
? PasswordProtection.from({
salt: reference.passwordProtection.salt,
passwordType: reference.passwordProtection.passwordType,
password: (password ?? reference.passwordProtection.password)!,
password: password!,
passwordLocationIndicator: reference.passwordProtection.passwordLocationIndicator
})
: undefined;
Expand Down Expand Up @@ -233,25 +227,12 @@ export class TokenController extends TransportController {

const cipher = await CoreCrypto.encrypt(serializedTokenBuffer, input.secretKey);

const password = parameters.passwordProtection.password;
if (!password) throw TransportCoreErrors.general.noPasswordProvided();

const hashedPassword = (await CoreCrypto.deriveHashOutOfPassword(password, input.passwordProtection.salt)).toBase64();

const response = (await this.client.updateTokenContent({ id: parameters.id.toString(), newContent: cipher.toBase64(), password: hashedPassword })).value;

const passwordProtection = PasswordProtection.from({
password,
passwordType: parameters.passwordProtection.passwordType,
salt: parameters.passwordProtection.salt,
passwordLocationIndicator: parameters.passwordProtection.passwordLocationIndicator
});
const response = (await this.client.updateTokenContent({ id: parameters.id.toString(), newContent: cipher.toBase64() })).value;

const token = Token.from({
id: CoreId.from(response.id),
secretKey: input.secretKey,
isOwn: true,
passwordProtection,
createdAt: CoreDate.from(response.createdAt),
expiresAt: CoreDate.from(response.expiresAt),
createdBy: this.parent.identity.address,
Expand All @@ -263,10 +244,9 @@ export class TokenController extends TransportController {
}

public async isEmptyToken(reference: TokenReference): Promise<boolean> {
if (!reference.passwordProtection?.password) throw TransportCoreErrors.general.noPasswordProvided();
if (reference.passwordProtection) return false;

const hashedPassword = (await CoreCrypto.deriveHashOutOfPassword(reference.passwordProtection.password, reference.passwordProtection.salt)).toBase64();
const response = (await this.client.getToken(reference.id.toString(), hashedPassword)).value;
const response = (await this.client.getToken(reference.id.toString())).value;

return !response.content;
}
Expand Down
10 changes: 1 addition & 9 deletions packages/transport/src/modules/tokens/local/EmptyToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,16 @@ import { CoreDate, ICoreDate } from "@nmshd/core-types";
import { CryptoSecretKey, ICryptoSecretKey } from "@nmshd/crypto";
import { nameof } from "ts-simple-nameof";
import { CoreSynchronizable, ICoreSynchronizable } from "../../../core";
import { IPasswordProtection, PasswordProtection } from "../../../core/types/PasswordProtection";
import { TokenReference } from "../transmission/TokenReference";

export interface IEmptyToken extends ICoreSynchronizable {
secretKey: ICryptoSecretKey;
expiresAt: ICoreDate;
passwordProtection: IPasswordProtection;
}

@type("EmptyToken")
export class EmptyToken extends CoreSynchronizable implements IEmptyToken {
public override readonly technicalProperties = ["@type", "@context", nameof<EmptyToken>((r) => r.secretKey), nameof<EmptyToken>((r) => r.expiresAt)];
public override readonly userdataProperties = [nameof<EmptyToken>((r) => r.passwordProtection)];

@validate()
@serialize()
Expand All @@ -25,10 +22,6 @@ export class EmptyToken extends CoreSynchronizable implements IEmptyToken {
@serialize()
public expiresAt: CoreDate;

@validate()
@serialize()
public passwordProtection: PasswordProtection;

public static from(value: IEmptyToken): EmptyToken {
return this.fromAny(value);
}
Expand All @@ -37,8 +30,7 @@ export class EmptyToken extends CoreSynchronizable implements IEmptyToken {
return TokenReference.from({
id: this.id,
backboneBaseUrl,
key: this.secretKey,
passwordProtection: this.passwordProtection.toSharedPasswordProtection(true)
key: this.secretKey
});
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { ISerializable, Serializable, serialize, type, validate } from "@js-soft/ts-serval";
import { CoreId, ICoreId, ISharedPasswordProtection, SharedPasswordProtection } from "@nmshd/core-types";
import { CoreId, ICoreId } from "@nmshd/core-types";
import { CryptoSecretKey, ICryptoSecretKey } from "@nmshd/crypto";

export interface IUpdateTokenContentParameters extends ISerializable {
id: ICoreId;
secretKey: ICryptoSecretKey;
content: ISerializable;
passwordProtection: ISharedPasswordProtection;
}

@type("UpdateTokenContentParameters")
Expand All @@ -23,10 +22,6 @@ export class UpdateTokenContentParameters extends Serializable implements IUpdat
@serialize()
public content: Serializable;

@validate({ nullable: true })
@serialize()
public passwordProtection: SharedPasswordProtection;

public static from(value: IUpdateTokenContentParameters): UpdateTokenContentParameters {
return this.fromAny(value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,7 @@ describe("AnonymousTokenController", function () {
test("should create an empty token", async () => {
const token = await anonymousTokenController.createEmptyToken();

expect(token.passwordProtection.password).toBeDefined();
expect(token.passwordProtection.passwordLocationIndicator).toBeUndefined();
expect(token).toBeDefined();
});

test("should get a proper error when trying to load an empty token", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,6 @@ describe("TokenController", function () {
const updatedSentToken = await sender.tokens.updateTokenContent({
content: content,
id: reference.id,
passwordProtection: reference.passwordProtection!,
secretKey: reference.key
});

Expand Down
Loading