Skip to content

Add private key decrypt validation#1390

Merged
sosnovsky merged 11 commits intomasterfrom
add-private-key-decrypt-validation
Feb 23, 2022
Merged

Add private key decrypt validation#1390
sosnovsky merged 11 commits intomasterfrom
add-private-key-decrypt-validation

Conversation

@martgil
Copy link
Collaborator

@martgil martgil commented Feb 21, 2022

This PR adds a private key validation test.

close #1346 // if this PR closes an issue

issue #0000 // if it doesn't close the issue yet


Tests (delete all except exactly one):

  • Tests will be added later (issue #...)
  • Tests added or updated

To be filled by reviewers

I have reviewed that this PR... (tick whichever items you personally focused on during this review):

  • addresses the issue it closes (if any)
  • code is readable and understandable
  • is accompanied with tests, or tests are not needed
  • is free of vulnerabilities
  • is documented clearly and usefully, or doesn't need documentation

@martgil martgil requested a review from sosnovsky as a code owner February 21, 2022 08:42
@martgil martgil marked this pull request as draft February 21, 2022 08:42
@martgil martgil self-assigned this Feb 21, 2022
@martgil
Copy link
Collaborator Author

martgil commented Feb 21, 2022

Hello sir, @IvanPizhenko,

I have added the initial validation to PgpKey.decrypt(). The bad private key from the test was now flagged as an invalid key.

I also think of adding the validation to PgpKey.parse() through PgpKey.normalize() but when the throw triggered on .validate() function of the Openpgp.js the return from PgpKey.normalization kinda overrides the error. (4cd6cb6)

So If I check the error message through throwAsync it receives the json debug data from normalized and not getting the "Error: key is invalid" native error.

Lastly, I feel like I'm still missing some place where to add the validation. My goal is to add it on PgpKey.decrypt and PgpKey.parse for the moment. If I missed anywhere other than those, I appreciate any suggestion.

Many Thanks!

@tomholub
Copy link
Collaborator

On normalize you can let the error be swallowed and test for it returning no keys.

When you add to parse, does it have the same issue?

Copy link
Contributor

@IvanPizhenko IvanPizhenko left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@martgil please see my remarks

public static validateAllDecryptedPackets = async (key: Key): Promise<void> => {
const packets = key.toPacketList() as PacketList<SecretKeyPacket>;
for (const prvPacket of packets.filter(PgpKey.isPacketPrivate).filter(packet => packet.isDecrypted())) {
await (prvPacket as SecretKeyPacket).validate(); // gnu-dummy never raises an exception, invalid keys raise exceptions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be type cast to BaseSecretKeyPacket

Copy link
Collaborator Author

@martgil martgil Feb 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated, thanks!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sir, I made a recent change that would work without further typecasting but by checking if the prvPacket is an instance of SecretKeyPacket. would that be ok?

}

public static validateAllDecryptedPackets = async (key: Key): Promise<void> => {
const packets = key.toPacketList() as PacketList<SecretKeyPacket>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove type cast to PacketList<SecretKeyPacket>, not all packets will be of type SecretKeyPacket, return type of the toPacketList() is PacketList<AllowedKeyPackets>, that's what we need to operate on.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you, sir, I revert the change to use PacketList<AllowedKeyPackets> and check if the instance of prvPacket is SecretKeyPacket and if the prvPacket is already decrypted because I don't have much idea of how I could make the .filter(PgpKey.isPacketPrivate) to work.

Copy link
Contributor

@IvanPizhenko IvanPizhenko Feb 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the problem with .filter(PgpKey.isPacketPrivate)? Why doesn't it work?
This code losing checks for SecretSubkeyPacket. I assumed it should be like this:

for (const packet of (key.toPacketList().filter(PgpKey.isPacketPrivate) as BaseSecretKeyPacket[])) {
  await packet.validate();
}

or even

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure but i'm getting the following type error when doing so:

No overload matches this call.
  Overload 1 of 2, '(predicate: (value: AllowedKeyPackets, index: number, array: AllowedKeyPackets[]) => value is PrvPacket, thisArg?: any): PrvPacket[]', gave the following error.
    Argument of type '(p: AnyKeyPacket) => p is PrvPacket' is not assignable to parameter of type '(value: AllowedKeyPackets, index: number, array: AllowedKeyPackets[]) => value is PrvPacket'.
      Types of parameters 'p' and 'value' are incompatible.
        Type 'AllowedKeyPackets' is not assignable to type 'BasePublicKeyPacket'.
          Type 'SignaturePacket' is missing the following properties from type 'BasePublicKeyPacket': algorithm, getAlgorithmInfo, getFingerprint, getFingerprintBytes, and 6 more.
  Overload 2 of 2, '(predicate: (value: AllowedKeyPackets, index: number, array: AllowedKeyPackets[]) => unknown, thisArg?: any): AllowedKeyPackets[]', gave the following error.
    Argument of type '(p: AnyKeyPacket) => p is PrvPacket' is not assignable to parameter of type '(value: AllowedKeyPackets, index: number, array: AllowedKeyPackets[]) => unknown'.
      Types of parameters 'p' and 'value' are incompatible.
        Type 'AllowedKeyPackets' is not assignable to type 'BasePublicKeyPacket'.ts(2769)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, I'll try to fix this myself a bit later.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you sir, I'll have my eyes on them so that I would learn from them and digest the idea as much as I can so that I can handle it myself the next time.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just change parameter type of isPrvPacket() to AllowedKeyPackets?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@martgil I've changed parameter type of the isPacketPrivate() to AllowedKeyPackets and it worked. I've pushed this change.

@IvanPizhenko
Copy link
Contributor

IvanPizhenko commented Feb 21, 2022

I also think of adding the validation to PgpKey.parse() through PgpKey.normalize() but when the throw triggered on .validate() function of the Openpgp.js the return from PgpKey.normalization kinda overrides the error.

@martgil You can extend return type of normalize() with additional optional field called like exception or error, and store exception there if it has occurred and then inspect it in the test. Regarding parse() - it actually calls normalize(), so you can do the same - propagate error from normalize() to the object returned by parse().

@martgil
Copy link
Collaborator Author

martgil commented Feb 22, 2022

@martgil You can extend return type of normalize() with additional optional field called like exception or error, and store exception there if it has occurred and then inspect it in the test. Regarding parse() - it actually calls normalize(), so you can do the same - propagate error from normalize() to the object returned by parse().

Thank you sir, it helps a lot. I tried to apply it the better way I know of - please correct me if any of the changes look awful. Thanks again!

@martgil
Copy link
Collaborator Author

martgil commented Feb 22, 2022

On normalize you can let the error be swallowed and test for it returning no keys.

When you add to parse, does it have the same issue?

Thank you, sir, I also find this helpful with help of the additional field exception on normalize() and parse() - the test looks much clearer.

}

public static normalize = async (armored: string): Promise<{ normalized: string, keys: Key[] }> => {
public static normalize = async (armored: string): Promise<{ normalized: string, keys: Key[], exception: string }> => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be optional, i.e. exception?: string, i.e. it can be undefined.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

name exception is a bit misleading. It could be exception if you would return here the whole exception object, but only error message is returned, as I can see later in the code. So I suggest to rename it into error, (and moreover, this will be more aligned with what we already have in this codebase).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, okay sir. I've rename it appropriately to error field, making it optional and removing the error from the returning data. thanks!

}
}
return { normalized: keys.map(k => k.armor()).join('\n'), keys };
return { normalized: keys.map(k => k.armor()).join('\n'), keys, exception: '' };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to return exception here if it is declared optional

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated, thanks!

public static parse = async (armored: string): Promise<{ original: string, normalized: string, keys: KeyDetails[] }> => {
const { normalized, keys } = await PgpKey.normalize(armored);
return { original: armored, normalized, keys: await Promise.all(keys.map(PgpKey.details)) };
public static parse = async (armored: string): Promise<{ original: string, normalized: string, keys: KeyDetails[], exception: string }> => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same: exception should be optional

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated, thanks!

@IvanPizhenko
Copy link
Contributor

@martgil Some more comments, please check.

@IvanPizhenko
Copy link
Contributor

@martgil Generally looks good, I will try later today to solve this issue with isPrvPacket().

@martgil martgil marked this pull request as ready for review February 23, 2022 03:39
@sosnovsky sosnovsky merged commit 5c2bea5 into master Feb 23, 2022
@sosnovsky sosnovsky deleted the add-private-key-decrypt-validation branch February 23, 2022 08:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing private key packets validation when importing/retrieving private key

4 participants