Skip to content

Commit

Permalink
Fortify Secret.Wrapped value protection (#9)
Browse files Browse the repository at this point in the history
To improve and avoid even more cases we decided to harden the `Wrapped` value for the property wrapper so that even more accidental exposure are avoided. To access the underlying value now you must pass through the `projectedValue` that returns an `UnwrappedSecret`.
  • Loading branch information
mattia authored Dec 18, 2022
1 parent 206c5d5 commit 54570e4
Show file tree
Hide file tree
Showing 8 changed files with 229 additions and 49 deletions.
16 changes: 12 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Instead by using `Secret` you can avoid this mistakes. By changing the type defi
```swift
struct Authentication {
var username: String
@Secret var token: String
@Secret var password: String
}
```

Expand All @@ -52,7 +52,7 @@ print(auth)
Will result in this log

```
Authentication(username: "fake", password: Secret([REDACTED String]))
Authentication(username: "fake", _password: Secret([REDACTED String]))
```

Protecting you from accidental mistakes.
Expand All @@ -65,9 +65,17 @@ auth.token.wrappedValue // This will expose the underlying `String`

## Codable support

Support for `Codable` is available if the wrapped type is already `Codable`. Note that this does not guarantee that the secret is not exposed (for example by encoding it to the disk in plain text) but you can always create a custom type with a dedicated `Codable` conformance.
Support for `Encodable` is provided by the package out of the box.
To have `Decodable` support you have to provide additional information on how to redact the value. You can easily add support for your type by confirming to the `RedactableForDecodable` protocol.
For example to automatically support `Decodable` for your `Secret<String>` you can add:

Note that you can also create a custom type and make it only conform to `Decodable` to avoid accidental `Encodable` conformances.
```swift
extension String: RedactableForDecodable {
public static var redactor: Redactor<Self> { .default }
}
```

Note that this does not guarantee that the secret is not exposed (for example by encoding it to the disk in plain text) but you can always create a custom type with a dedicated `Codable` conformance.


## License
Expand Down
44 changes: 44 additions & 0 deletions Sources/Secrecy/Redactor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// MARK: - Redact

public struct Redactor<Value>: Sendable {
public var redact: @Sendable (Value) -> Value
public var redactForDescription: @Sendable (Value) -> String
public var redactForDebug: @Sendable (Value) -> String

public init(
redact: @escaping @Sendable (Value) -> Value,
redactForDescription: @escaping @Sendable (Value) -> String = { _ in "************" },
redactForDebug: @escaping @Sendable (Value) -> String = { _ in
"Secret([REDACTED \(Value.self)])"
}
) {
self.redact = redact
self.redactForDescription = redactForDescription
self.redactForDebug = redactForDebug
}
}

// MARK: - Redactor<String>

public extension Redactor where Value == String {
static let `default` = Redactor { @Sendable _ in "************" }
}

// MARK: - Redactor<Int>

public extension Redactor where Value == Int {
static let `default` = Redactor { @Sendable _ in -1 }
}

// MARK: - Redactor<Double>

public extension Redactor where Value == Double {
static let `default` = Redactor { @Sendable _ in -1.0 }
}

// MARK: - Redactor<Bool>

public extension Redactor where Value == Bool {
static let `defaultTrue` = Redactor { @Sendable _ in true }
static let `defaultFalse` = Redactor { @Sendable _ in false }
}
19 changes: 0 additions & 19 deletions Sources/Secrecy/Secret+Codable.swift

This file was deleted.

14 changes: 14 additions & 0 deletions Sources/Secrecy/Secret+Decodable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// MARK: - Decodable

extension Secret: Decodable where Wrapped: Decodable & RedactableForDecodable {
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let value = try container.decode(Wrapped.self)

self = Secret(value, redactor: Wrapped.redactor)
}
}

public protocol RedactableForDecodable {
static var redactor: Redactor<Self> { get }
}
8 changes: 8 additions & 0 deletions Sources/Secrecy/Secret+Encodable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// MARK: - Encodable

extension Secret: Encodable where Wrapped: Encodable {
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(projectedValue.value)
}
}
60 changes: 46 additions & 14 deletions Sources/Secrecy/Secret+Literal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,71 +2,92 @@

extension Secret: ExpressibleByUnicodeScalarLiteral where Wrapped == String {
public init(unicodeScalarLiteral value: Wrapped) {
self.init(value)
self.init(value, redactor: .default)
}
}

// MARK: - ExpressibleByExtendedGraphemeClusterLiteral

extension Secret: ExpressibleByExtendedGraphemeClusterLiteral where Wrapped == String {
public init(extendedGraphemeClusterLiteral value: Wrapped) {
self.init(value)
self.init(value, redactor: .default)
}
}

// MARK: - ExpressibleByStringLiteral

extension Secret: ExpressibleByStringLiteral where Wrapped == String {
public init(stringLiteral value: Wrapped) {
self.init(value)
self.init(value, redactor: .default)
}
}

// MARK: - ExpressibleByIntegerLiteral

extension Secret: ExpressibleByIntegerLiteral where Wrapped == Int {
public init(integerLiteral value: Int) {
self.init(value)
self.init(value, redactor: .default)
}
}

// MARK: - ExpressibleByFloatLiteral

extension Secret: ExpressibleByFloatLiteral where Wrapped == Double {
public init(floatLiteral value: Double) {
self.init(value)
self.init(value, redactor: .default)
}
}

// MARK: - ExpressibleByBooleanLiteral

extension Secret: ExpressibleByBooleanLiteral where Wrapped == Bool {
public init(booleanLiteral value: Bool) {
self.init(value)
self.init(value, redactor: .defaultFalse)
}
}

// MARK: - ExpressibleByArrayLiteral

extension Secret: ExpressibleByArrayLiteral where Wrapped: _WrapperForArrayLiteralElements {
extension Secret: ExpressibleByArrayLiteral where Wrapped: _InitializableByArrayLiteralElements {
public init(arrayLiteral elements: Wrapped.ArrayLiteralElement...) {
self.init(Wrapped(elements))
self.init(Wrapped(elements), redactor: Redactor{ $0.anonymizeValues() })
}
}

/// A helper protocol used to enable wrapper types to conform to `ExpressibleByArrayLiteral`.
/// Seen in https://github.com/apollographql/apollo-ios/blob/014660b14262df15f0d2c7b7e973d0a53be27b7e/Sources/ApolloAPI/GraphQLNullable.swift#L188
/// Used by ``Secret.init(arrayLiteral:)``
public protocol _WrapperForArrayLiteralElements: ExpressibleByArrayLiteral {
public protocol _InitializableByArrayLiteralElements: ExpressibleByArrayLiteral {
init(_ array: [ArrayLiteralElement])

var anonymizeValues: @Sendable () -> Self { get }
}
extension Array: _InitializableByArrayLiteralElements
where Element: InitializableByLiteralType {
public var anonymizeValues: @Sendable () -> Array<Element> {
{ @Sendable in map(Element.redactor.redact) }
}
}
extension Array: _WrapperForArrayLiteralElements {}

// MARK: - ExpressibleByDictionaryLiteral
public protocol InitializableByLiteralType {
static var redactor: Redactor<Self> { get }
}

extension String: InitializableByLiteralType {
public static var redactor: Redactor<String> { .default }
}

extension Int: InitializableByLiteralType {
public static var redactor: Redactor<Int> { .default }
}

extension Double: InitializableByLiteralType {
public static var redactor: Redactor<Double> { .default }
}

// MARK: - ExpressibleByDictionaryLiteral
extension Secret: ExpressibleByDictionaryLiteral where Wrapped: _WrapperForDictionaryLiteralElements {
public init(dictionaryLiteral elements: (Wrapped.Key, Wrapped.Value)...) {
self.init(Wrapped(elements))
self.init(Wrapped(elements), redactor: Redactor { $0.anonymizeValues() })
}
}

Expand All @@ -75,10 +96,21 @@ extension Secret: ExpressibleByDictionaryLiteral where Wrapped: _WrapperForDicti
/// Used by ``Secret.init(dictionaryLiteral:)``
public protocol _WrapperForDictionaryLiteralElements: ExpressibleByDictionaryLiteral {
init(_ dictionary: [(Key, Value)])

var anonymizeValues: @Sendable () -> Self { get }
}

extension Dictionary: _WrapperForDictionaryLiteralElements {
extension Dictionary: _WrapperForDictionaryLiteralElements
where Key: InitializableByLiteralType, Value: InitializableByLiteralType {
public init(_ elements: [(Key, Value)]) {
self.init(uniqueKeysWithValues: elements)
}

public var anonymizeValues: @Sendable () -> Dictionary<Key, Value> {
{ @Sendable in
first
.map { (key, value) in [Key.redactor.redact(key): Value.redactor.redact(value)] }
?? [:]
}
}
}
52 changes: 45 additions & 7 deletions Sources/Secrecy/Secret.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,71 @@
public struct Secret<Wrapped> {
/// The value that should be treated with care.
private let value: Wrapped
private let redactor: Redactor<Wrapped>

/// Creates a new secret by wrapping the provided value.
///
/// - Parameter wrappedValue: The value to keep secret
/// - Parameter redactor: The `Redactor` that masks the secret
///
public init(_ value: Wrapped, redactor: Redactor<Wrapped>) {
self.value = value
self.redactor = redactor
}

/// The underlying secret that we are trying to keep hidden
public var wrappedValue: Wrapped {
value
redactor.redact(value)
}

/// Creates a new secret by wrapping the provided value.
///
/// - Parameter value: The value to keep secret
public init(_ value: Wrapped) {
public var projectedValue: UnwrappedSecret<Wrapped> {
UnwrappedSecret(value)
}

public struct UnwrappedSecret<Wrapped> {
public let value: Wrapped

public init(_ value: Wrapped) {
self.value = value
}
}
}

public extension Secret {
init(_ value: Wrapped) where Wrapped == String {
self.value = value
self.redactor = .default
}

init(_ value: Wrapped) where Wrapped == Int {
self.value = value
self.redactor = .default
}

init(_ value: Wrapped) where Wrapped == Double {
self.value = value
self.redactor = .default
}

init(_ value: Wrapped) where Wrapped == Bool {
self.value = value
self.redactor = .defaultFalse
}
}

// MARK: - CustomStringConvertible

extension Secret: CustomStringConvertible {
public var description: String {
"************"
redactor.redactForDescription(value)
}
}

// MARK: - CustomDebugStringConvertible

extension Secret: CustomDebugStringConvertible {
public var debugDescription: String {
"Secret([REDACTED \(Wrapped.self)])"
redactor.redactForDebug(value)
}
}

Expand Down
Loading

0 comments on commit 54570e4

Please sign in to comment.